From a79b43fc9ea4c09e5983c2dcf0a2638b61efee2a Mon Sep 17 00:00:00 2001 From: Danielku15 Date: Sat, 17 Oct 2026 12:23:40 +0200 Subject: [PATCH 1/2] ci: publish npm packages via trusted publishing Authenticate npm publishes through GitHub OIDC instead of the NPMJS_AUTH_TOKEN secret. Nightlies publish directly with the alpha tag, releases are staged and require a maintainer approval with 2FA. - grant id-token: write to the web publish jobs - stage release publishes (npm stage publish, npm 11.15+) - add a dry_run dispatch input that runs npm with --dry-run for all web packages and skips the dotnet and kotlin jobs - match repository.url casing to CoderLine/alphaTab as required by provenance validation Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/publish.yml | 12 ++++++++++++ .github/workflows/release.yml | 15 ++++++++++++--- .github/workflows/~publish_web.yml | 14 +++++++++++--- packages/alphatab/package.json | 2 +- packages/lsp/package.json | 2 +- packages/monaco/package.json | 2 +- packages/vite/package.json | 2 +- packages/webpack/package.json | 2 +- scripts/nightly.mts | 14 ++++++++++++-- 9 files changed, 52 insertions(+), 13 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 9e2f8a23cf..52d976f82c 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -4,23 +4,35 @@ on: schedule: - cron: "10 0 * * *" workflow_dispatch: + inputs: + dry_run: + type: boolean + description: Run npm publish with --dry-run for all web packages, skip dotnet and kotlin + default: false jobs: web: uses: ./.github/workflows/~publish_web.yml + permissions: + contents: read + id-token: write secrets: inherit with: version: alpha ${{github.run_number}} npm_tag: alpha + force: ${{ inputs.dry_run == true }} + dry_run: ${{ inputs.dry_run == true }} dotnet: uses: ./.github/workflows/~publish_dotnet.yml secrets: inherit + if: ${{ !inputs.dry_run }} with: version: alpha ${{github.run_number}} kotlin: uses: ./.github/workflows/~publish_kotlin.yml secrets: inherit + if: ${{ !inputs.dry_run }} with: version: alpha ${{github.run_number}} \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 44492ba0f0..a53e33c8a4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,22 +16,31 @@ on: release_kotlin_android: type: boolean description: Release Android packages - default: true + default: true + dry_run: + type: boolean + description: Run npm stage publish with --dry-run for all web packages, skip dotnet and kotlin + default: false jobs: web: uses: ./.github/workflows/~publish_web.yml + permissions: + contents: read + id-token: write secrets: inherit if: (github.event_name == 'push') || (github.event_name == 'workflow_dispatch' && github.event.inputs.release_web == 'true') with: version: ${{github.run_number}} npm_tag: latest force: true + stage: true + dry_run: ${{ inputs.dry_run == true }} dotnet: uses: ./.github/workflows/~publish_dotnet.yml secrets: inherit - if: (github.event_name == 'push') || (github.event_name == 'workflow_dispatch' && github.event.inputs.release_csharp == 'true') + if: (github.event_name == 'push') || (github.event_name == 'workflow_dispatch' && github.event.inputs.release_csharp == 'true' && !inputs.dry_run) with: version: ${{github.run_number}} force: true @@ -39,7 +48,7 @@ jobs: kotlin: uses: ./.github/workflows/~publish_kotlin.yml secrets: inherit - if: (github.event_name == 'push') || (github.event_name == 'workflow_dispatch' && github.event.inputs.release_kotlin_android == 'true') + if: (github.event_name == 'push') || (github.event_name == 'workflow_dispatch' && github.event.inputs.release_kotlin_android == 'true' && !inputs.dry_run) with: version: ${{github.run_number}} force: true \ No newline at end of file diff --git a/.github/workflows/~publish_web.yml b/.github/workflows/~publish_web.yml index 45f8374fa3..c52fc40b7b 100644 --- a/.github/workflows/~publish_web.yml +++ b/.github/workflows/~publish_web.yml @@ -10,6 +10,12 @@ on: npm_tag: type: string required: true + stage: + type: boolean + default: false + dry_run: + type: boolean + default: false jobs: @@ -32,6 +38,9 @@ jobs: needs: check if: needs.check.outputs.has_changes == 'true' runs-on: ubuntu-latest + permissions: + contents: read + id-token: write steps: - uses: actions/checkout@v7 with: @@ -49,6 +58,5 @@ jobs: - run: npm run build-monaco - run: npm run nightly-pack -- --platform web --force ${{ inputs.force }} - - run: npm run nightly-publish -- --platform web --force ${{ inputs.force }} --tag ${{ inputs.npm_tag }} - env: - NODE_AUTH_TOKEN: ${{ secrets.NPMJS_AUTH_TOKEN }} + - run: npm install -g npm@11 + - run: npm run nightly-publish -- --platform web --force ${{ inputs.force }} --tag ${{ inputs.npm_tag }} --stage ${{ inputs.stage }} --dry-run ${{ inputs.dry_run }} diff --git a/packages/alphatab/package.json b/packages/alphatab/package.json index 119d0d88a5..3fe5b76b4e 100644 --- a/packages/alphatab/package.json +++ b/packages/alphatab/package.json @@ -18,7 +18,7 @@ "license": "MPL-2.0", "repository": { "type": "git", - "url": "git+https://github.com/coderline/alphaTab.git" + "url": "git+https://github.com/CoderLine/alphaTab.git" }, "type": "module", "main": "dist/alphaTab.js", diff --git a/packages/lsp/package.json b/packages/lsp/package.json index e2f9e913b0..37aee88d18 100644 --- a/packages/lsp/package.json +++ b/packages/lsp/package.json @@ -23,7 +23,7 @@ "license": "MPL-2.0", "repository": { "type": "git", - "url": "git+https://github.com/coderline/alphaTab.git" + "url": "git+https://github.com/CoderLine/alphaTab.git" }, "scripts": { "clean": "rimraf dist", diff --git a/packages/monaco/package.json b/packages/monaco/package.json index a6c0fd8644..a42d17988b 100644 --- a/packages/monaco/package.json +++ b/packages/monaco/package.json @@ -20,7 +20,7 @@ "license": "MPL-2.0", "repository": { "type": "git", - "url": "git+https://github.com/coderline/alphaTab.git" + "url": "git+https://github.com/CoderLine/alphaTab.git" }, "scripts": { "clean": "rimraf dist", diff --git a/packages/vite/package.json b/packages/vite/package.json index c14ab261a4..be6b564257 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -19,7 +19,7 @@ "license": "MPL-2.0", "repository": { "type": "git", - "url": "git+https://github.com/coderline/alphaTab.git" + "url": "git+https://github.com/CoderLine/alphaTab.git" }, "type": "module", "exports": { diff --git a/packages/webpack/package.json b/packages/webpack/package.json index 5c491c3287..24d25966f1 100644 --- a/packages/webpack/package.json +++ b/packages/webpack/package.json @@ -19,7 +19,7 @@ "license": "MPL-2.0", "repository": { "type": "git", - "url": "git+https://github.com/coderline/alphaTab.git" + "url": "git+https://github.com/CoderLine/alphaTab.git" }, "type": "module", "exports": { diff --git a/scripts/nightly.mts b/scripts/nightly.mts index ab59d22f71..b8093421c8 100644 --- a/scripts/nightly.mts +++ b/scripts/nightly.mts @@ -16,6 +16,12 @@ const options: ParseArgsOptionsConfig = { }, tag: { type: 'string' + }, + stage: { + type: 'string' + }, + 'dry-run': { + type: 'string' } }; @@ -92,14 +98,18 @@ function execDryRun(command: string, options: ExecSyncOptionsWithBufferEncoding) function publish(platform: string, packages: Set) { switch (platform) { - case 'web': + case 'web': { + const publishCommand = args.stage === 'true' ? 'npm stage publish' : 'npm publish'; + // verbose logging surfaces the outcome of the OIDC token exchange + const dryRunArgs = args['dry-run'] === 'true' ? ' --dry-run --loglevel verbose' : ''; for (const packageName of packages) { - execDryRun(`npm publish --access public --tag ${args.tag}`, { + execDryRun(`${publishCommand} --access public --tag ${args.tag}${dryRunArgs}`, { stdio: 'inherit', cwd: resolve(import.meta.dirname, '../packages', packageName) }); } break; + } case 'dotnet': const dotnetPackages = ['AlphaTab', 'AlphaTab.Windows']; for (const pkg of dotnetPackages) { From e3f48359532e09c4d7419dfd23668a307ebd62f2 Mon Sep 17 00:00:00 2001 From: Danielku15 Date: Sat, 17 Oct 2026 12:37:24 +0200 Subject: [PATCH 2/2] ci: support dry_run for dotnet and kotlin publishing The dry_run dispatch input now applies to all platforms instead of skipping dotnet and kotlin: - web: npm publish / npm stage publish with --dry-run - dotnet: build and pack, list the nupkgs that would be pushed - kotlin: build and sign, publish to the local DistPath repository instead of Maven Central Replaces the DRY_RUN environment variable, which only printed the commands, with the --dry-run argument. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/publish.yml | 10 +++-- .github/workflows/release.yml | 10 +++-- .github/workflows/~publish_dotnet.yml | 5 ++- .github/workflows/~publish_kotlin.yml | 5 ++- scripts/nightly.mts | 62 ++++++++++++++------------- 5 files changed, 52 insertions(+), 40 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 52d976f82c..80fd9f99b8 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -7,7 +7,7 @@ on: inputs: dry_run: type: boolean - description: Run npm publish with --dry-run for all web packages, skip dotnet and kotlin + description: Build and verify all packages without publishing them default: false jobs: @@ -26,13 +26,15 @@ jobs: dotnet: uses: ./.github/workflows/~publish_dotnet.yml secrets: inherit - if: ${{ !inputs.dry_run }} with: version: alpha ${{github.run_number}} + force: ${{ inputs.dry_run == true }} + dry_run: ${{ inputs.dry_run == true }} kotlin: uses: ./.github/workflows/~publish_kotlin.yml secrets: inherit - if: ${{ !inputs.dry_run }} with: - version: alpha ${{github.run_number}} \ No newline at end of file + version: alpha ${{github.run_number}} + force: ${{ inputs.dry_run == true }} + dry_run: ${{ inputs.dry_run == true }} \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a53e33c8a4..1bcf0ef9f7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,7 +19,7 @@ on: default: true dry_run: type: boolean - description: Run npm stage publish with --dry-run for all web packages, skip dotnet and kotlin + description: Build and verify all selected packages without publishing them default: false jobs: @@ -40,15 +40,17 @@ jobs: dotnet: uses: ./.github/workflows/~publish_dotnet.yml secrets: inherit - if: (github.event_name == 'push') || (github.event_name == 'workflow_dispatch' && github.event.inputs.release_csharp == 'true' && !inputs.dry_run) + if: (github.event_name == 'push') || (github.event_name == 'workflow_dispatch' && github.event.inputs.release_csharp == 'true') with: version: ${{github.run_number}} force: true + dry_run: ${{ inputs.dry_run == true }} kotlin: uses: ./.github/workflows/~publish_kotlin.yml secrets: inherit - if: (github.event_name == 'push') || (github.event_name == 'workflow_dispatch' && github.event.inputs.release_kotlin_android == 'true' && !inputs.dry_run) + if: (github.event_name == 'push') || (github.event_name == 'workflow_dispatch' && github.event.inputs.release_kotlin_android == 'true') with: version: ${{github.run_number}} - force: true \ No newline at end of file + force: true + dry_run: ${{ inputs.dry_run == true }} \ No newline at end of file diff --git a/.github/workflows/~publish_dotnet.yml b/.github/workflows/~publish_dotnet.yml index de857f0f2f..59f69d64f2 100644 --- a/.github/workflows/~publish_dotnet.yml +++ b/.github/workflows/~publish_dotnet.yml @@ -7,6 +7,9 @@ on: force: type: boolean default: false + dry_run: + type: boolean + default: false jobs: check: @@ -45,6 +48,6 @@ jobs: - run: npm run build-csharp - run: npm run nightly-pack -- --platform dotnet --force ${{ inputs.force }} - - run: npm run nightly-publish -- --platform dotnet --force ${{ inputs.force }} + - run: npm run nightly-publish -- --platform dotnet --force ${{ inputs.force }} --dry-run ${{ inputs.dry_run }} env: NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} diff --git a/.github/workflows/~publish_kotlin.yml b/.github/workflows/~publish_kotlin.yml index eeb96a0077..b3ed784e9e 100644 --- a/.github/workflows/~publish_kotlin.yml +++ b/.github/workflows/~publish_kotlin.yml @@ -7,6 +7,9 @@ on: force: type: boolean default: false + dry_run: + type: boolean + default: false jobs: check: @@ -46,7 +49,7 @@ jobs: - run: npm run build-kotlin - run: npm run nightly-pack -- --platform kotlin --force ${{ inputs.force }} - - run: npm run nightly-publish -- --platform kotlin --force ${{ inputs.force }} + - run: npm run nightly-publish -- --platform kotlin --force ${{ inputs.force }} --dry-run ${{ inputs.dry_run }} env: ORG_GRADLE_PROJECT_mavenCentralUsername: ${{secrets.OSSRH_USERTOKEN_USERNAME}} ORG_GRADLE_PROJECT_mavenCentralPassword: ${{secrets.OSSRH_USERTOKEN_PASSWORD}} diff --git a/scripts/nightly.mts b/scripts/nightly.mts index b8093421c8..0cb70cba6c 100644 --- a/scripts/nightly.mts +++ b/scripts/nightly.mts @@ -1,4 +1,4 @@ -import { type ExecSyncOptionsWithBufferEncoding, execSync } from 'node:child_process'; +import { execSync } from 'node:child_process'; import { appendFileSync, existsSync, readdirSync, readFileSync } from 'node:fs'; import { resolve } from 'node:path'; import { exit } from 'node:process'; @@ -86,48 +86,50 @@ function getChangedPackages(force: boolean) { return packages; } -const DRY_RUN = process.env.DRY_RUN; -function execDryRun(command: string, options: ExecSyncOptionsWithBufferEncoding) { - console.info(`executing command ${command} in ${options.cwd}`); - if (DRY_RUN) { - console.warn('nothing executed, dry run active'); - } else { - execSync(command, options); - } +function exec(command: string, cwd: string) { + console.info(`executing command ${command} in ${cwd}`); + execSync(command, { stdio: 'inherit', cwd }); } -function publish(platform: string, packages: Set) { +function publish(platform: string, packages: Set, dryRun: boolean) { switch (platform) { case 'web': { const publishCommand = args.stage === 'true' ? 'npm stage publish' : 'npm publish'; // verbose logging surfaces the outcome of the OIDC token exchange - const dryRunArgs = args['dry-run'] === 'true' ? ' --dry-run --loglevel verbose' : ''; + const dryRunArgs = dryRun ? ' --dry-run --loglevel verbose' : ''; for (const packageName of packages) { - execDryRun(`${publishCommand} --access public --tag ${args.tag}${dryRunArgs}`, { - stdio: 'inherit', - cwd: resolve(import.meta.dirname, '../packages', packageName) - }); + exec( + `${publishCommand} --access public --tag ${args.tag}${dryRunArgs}`, + resolve(import.meta.dirname, '../packages', packageName) + ); } break; } - case 'dotnet': - const dotnetPackages = ['AlphaTab', 'AlphaTab.Windows']; - for (const pkg of dotnetPackages) { - execDryRun( - `dotnet nuget push ${pkg}/bin/Release/*.nupkg -k ${process.env.NUGET_API_KEY} -s https://api.nuget.org/v3/index.json`, - { - stdio: 'inherit', - cwd: resolve(import.meta.dirname, '../packages/csharp/src') + case 'dotnet': { + const cwd = resolve(import.meta.dirname, '../packages/csharp/src'); + for (const pkg of ['AlphaTab', 'AlphaTab.Windows']) { + if (dryRun) { + // dotnet nuget push has no dry-run mode + const nupkgs = readdirSync(resolve(cwd, pkg, 'bin/Release')).filter(f => f.endsWith('.nupkg')); + if (nupkgs.length === 0) { + throw new Error(`No nupkg found for ${pkg}`); } - ); + console.info(`dry run, skipping push of ${pkg}:`, nupkgs); + } else { + exec( + `dotnet nuget push ${pkg}/bin/Release/*.nupkg -k ${process.env.NUGET_API_KEY} -s https://api.nuget.org/v3/index.json`, + cwd + ); + } } break; - case 'kotlin': - execDryRun(`./gradlew publishToMavenCentral`, { - stdio: 'inherit', - cwd: resolve(import.meta.dirname, '../packages/kotlin/src/') - }); + } + case 'kotlin': { + // DistPath is a local maven repository: builds and signs the publications without uploading them + const task = dryRun ? 'publishAllPublicationsToDistPathRepository' : 'publishToMavenCentral'; + exec(`./gradlew ${task}`, resolve(import.meta.dirname, '../packages/kotlin/src/')); break; + } } } @@ -190,7 +192,7 @@ switch (args.mode) { pack(args.platform as string, packages); break; case 'publish': - publish(args.platform as string, packages); + publish(args.platform as string, packages, args['dry-run'] === 'true'); break; case 'check': check(args.platform as string, packages);