From 20d8b8ae994b3e58dd0b1d8d4abc42b0fb82ffeb Mon Sep 17 00:00:00 2001 From: Jozef Izso Date: Sat, 2 May 2026 13:30:55 +0200 Subject: [PATCH 1/5] Add npm provenance publish workflow (#17) ## Summary - add a release-triggered workflow that publishes @appdmg/appdmg and @appdmg/cli - require macOS integration tests before publishing - pack and attest both npm tarballs, then publish those exact artifacts with provenance enabled - document npm Trusted Publisher settings and release order ## Verification - ruby YAML parse for publish workflow - npm test - npm test --prefix packages/cli - npm audit --audit-level=moderate - npm audit --audit-level=moderate --prefix packages/cli - npm ls --omit=dev --all - npm ls --omit=dev --all --prefix packages/cli - npm pack --dry-run - npm pack --dry-run --prefix packages/cli - npm run test:integration --- .github/workflows/publish.yml | 123 ++++++++++++++++++++++++++++++++++ docs/node24-migration.md | 4 ++ docs/npm-publishing.md | 61 +++++++++++++++++ 3 files changed, 188 insertions(+) create mode 100644 .github/workflows/publish.yml create mode 100644 docs/npm-publishing.md diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..dcec603 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,123 @@ +name: publish + +on: + release: + types: [published] + +permissions: + contents: read + id-token: write + attestations: write + artifact-metadata: write + +jobs: + integration: + name: integration node v24 macos-15-intel + runs-on: macos-15-intel + + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version: 24 + cache: npm + - run: npm ci + - run: npm test + - run: npm ci --prefix packages/cli + - run: npm test --prefix packages/cli + - run: npm run test:integration + + publish: + name: publish to npmjs + runs-on: ubuntu-latest + needs: integration + + steps: + - name: checkout + uses: actions/checkout@v6 + + - name: setup node 24 + uses: actions/setup-node@v6 + with: + node-version: 24 + registry-url: https://registry.npmjs.org + cache: npm + + - name: verify publish toolchain + run: | + node --version + npm --version + node -e "const version = require('node:child_process').execFileSync('npm', ['--version'], { encoding: 'utf8' }).trim(); const [major, minor, patch] = version.split('.').map(Number); if (major < 11 || (major === 11 && (minor < 5 || (minor === 5 && patch < 1)))) throw new Error('npm 11.5.1 or newer is required for trusted publishing')" + + - name: install appdmg dependencies + run: npm ci + + - name: install cli dependencies + run: npm ci --prefix packages/cli + + - name: verify cli dependency contract + run: | + node -e "const root = require('./package.json'); const cli = require('./packages/cli/package.json'); if (cli.dependencies['@appdmg/appdmg'] !== root.version) throw new Error('@appdmg/cli must depend on the exact @appdmg/appdmg package version')" + + - name: test appdmg + run: npm test + + - name: test cli + run: npm test --prefix packages/cli + + - name: audit appdmg dependencies + run: npm audit --audit-level=moderate + + - name: audit cli dependencies + run: npm audit --audit-level=moderate --prefix packages/cli + + - name: verify appdmg runtime dependency tree + run: npm ls --omit=dev --all + + - name: verify cli runtime dependency tree + run: npm ls --omit=dev --all --prefix packages/cli + + - name: pack appdmg package + id: pack_appdmg + run: | + mkdir -p dist/appdmg + npm pack --json --pack-destination dist/appdmg > dist/appdmg/pack.json + tarball="$(find dist/appdmg -maxdepth 1 -name '*.tgz' -print -quit)" + test -n "$tarball" + echo "tarball=$tarball" >> "$GITHUB_OUTPUT" + + - name: pack cli package + id: pack_cli + run: | + mkdir -p dist/cli + ( + cd packages/cli + npm pack --json --pack-destination ../../dist/cli > ../../dist/cli/pack.json + ) + tarball="$(find dist/cli -maxdepth 1 -name '*.tgz' -print -quit)" + test -n "$tarball" + echo "tarball=$tarball" >> "$GITHUB_OUTPUT" + + - name: attest npm package artifacts + uses: actions/attest@v4 + with: + subject-path: dist/**/*.tgz + + - name: upload npm package artifacts + uses: actions/upload-artifact@v4 + with: + name: npm-packages + path: dist/**/*.tgz + if-no-files-found: error + + - name: publish appdmg package + run: npm publish "$TARBALL" --provenance --access public + env: + TARBALL: ${{ steps.pack_appdmg.outputs.tarball }} + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: publish cli package + run: npm publish "$TARBALL" --provenance --access public + env: + TARBALL: ${{ steps.pack_cli.outputs.tarball }} + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/docs/node24-migration.md b/docs/node24-migration.md index fe90a9d..a42d712 100644 --- a/docs/node24-migration.md +++ b/docs/node24-migration.md @@ -162,6 +162,10 @@ Before publishing the final CLI to npm, verify that each scoped helper package is available from npm at the intended version and that a clean install of `@appdmg/cli` resolves only npm-published artifacts. +The npm release process is documented in +[npm-publishing.md](npm-publishing.md). Packages must be published from GitHub +Actions with provenance and package artifact attestations. + ## Test coverage The rewrite is backed by AVA tests. diff --git a/docs/npm-publishing.md b/docs/npm-publishing.md new file mode 100644 index 0000000..5b1ac55 --- /dev/null +++ b/docs/npm-publishing.md @@ -0,0 +1,61 @@ +# npm publishing with provenance + +All `@appdmg/*` packages are published from GitHub Actions. Local npm publish +is not part of the release process. + +## Required npm package settings + +Prefer npm Trusted Publishing for every package. Configure the npm package +Trusted Publisher as GitHub Actions with these values: + +| Package | GitHub repository | Workflow filename | Environment | +| --- | --- | --- | --- | +| `@appdmg/bplist-creator` | `appdmg/bplist-creator` | `publish.yml` | blank | +| `@appdmg/tn1150` | `appdmg/tn1150` | `publish.yml` | blank | +| `@appdmg/macos-alias` | `appdmg/macos-alias` | `publish.yml` | blank | +| `@appdmg/ds-store` | `appdmg/ds-store` | `publish.yml` | blank | +| `@appdmg/appdmg` | `appdmg/appdmg-cli` | `publish.yml` | blank | +| `@appdmg/cli` | `appdmg/appdmg-cli` | `publish.yml` | blank | + +The repositories and packages must stay public for npm provenance to be +generated and shown by npm. + +If npm cannot configure Trusted Publishing before a first package publish, add +a granular `NPM_TOKEN` repository secret with publish permission and 2FA bypass. +The workflows still publish from GitHub Actions with `npm publish --provenance +--access public`, so the package gets npm provenance. Remove the token path once +Trusted Publishing works. + +## What the workflows attest + +Each publish workflow: + +- runs on a GitHub-hosted runner with `id-token: write`; +- installs with Node.js 24; +- runs tests, audit, and runtime dependency checks; +- creates the exact npm package tarball with `npm pack`; +- creates a GitHub artifact attestation for that `.tgz`; +- uploads the `.tgz` as a workflow artifact; +- publishes the same `.tgz` to npm with provenance enabled. + +The appdmg CLI repository publishes two packages from one workflow. It publishes +`@appdmg/appdmg` first, then `@appdmg/cli`, and verifies that the CLI depends on +the exact library package version. + +## Release order + +Publish packages bottom-up: + +1. `@appdmg/bplist-creator` +2. `@appdmg/tn1150` +3. `@appdmg/macos-alias` +4. `@appdmg/ds-store` +5. `@appdmg/appdmg` and `@appdmg/cli` + +After publishing, verify from a clean project: + +```sh +npm install @appdmg/cli +npm audit signatures +npx appdmg-cli --version +``` From 0e659504f15a627f867147ef2b09d212fbda3dbb Mon Sep 17 00:00:00 2001 From: Jozef Izso Date: Sat, 2 May 2026 13:45:48 +0200 Subject: [PATCH 2/5] Address publish workflow review feedback (#18) ## Summary - skip npm publishing for GitHub prereleases so prerelease releases cannot publish the default latest dist-tag - check whether @appdmg/appdmg and @appdmg/cli exact versions already exist on npm - skip pack, attestation, artifact upload, and publish steps per package when already published, allowing reruns after partial publish failures - document prerelease and rerun behavior ## Verification - YAML parse for publish workflow --- .github/workflows/publish.yml | 36 +++++++++++++++++++++++++++++++++++ docs/npm-publishing.md | 5 +++++ 2 files changed, 41 insertions(+) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index dcec603..b840a5a 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -13,6 +13,7 @@ permissions: jobs: integration: name: integration node v24 macos-15-intel + if: github.event.release.prerelease == false runs-on: macos-15-intel steps: @@ -29,6 +30,7 @@ jobs: publish: name: publish to npmjs + if: github.event.release.prerelease == false runs-on: ubuntu-latest needs: integration @@ -77,7 +79,36 @@ jobs: - name: verify cli runtime dependency tree run: npm ls --omit=dev --all --prefix packages/cli + - name: check appdmg npm package version + id: npm_appdmg + run: | + package_name="$(node -p "require('./package.json').name")" + package_version="$(node -p "require('./package.json').version")" + + if npm view "${package_name}@${package_version}" version --json >/dev/null 2>&1; then + echo "exists=true" >> "$GITHUB_OUTPUT" + else + echo "exists=false" >> "$GITHUB_OUTPUT" + fi + + echo "package=${package_name}@${package_version}" >> "$GITHUB_OUTPUT" + + - name: check cli npm package version + id: npm_cli + run: | + package_name="$(node -p "require('./packages/cli/package.json').name")" + package_version="$(node -p "require('./packages/cli/package.json').version")" + + if npm view "${package_name}@${package_version}" version --json >/dev/null 2>&1; then + echo "exists=true" >> "$GITHUB_OUTPUT" + else + echo "exists=false" >> "$GITHUB_OUTPUT" + fi + + echo "package=${package_name}@${package_version}" >> "$GITHUB_OUTPUT" + - name: pack appdmg package + if: steps.npm_appdmg.outputs.exists == 'false' id: pack_appdmg run: | mkdir -p dist/appdmg @@ -87,6 +118,7 @@ jobs: echo "tarball=$tarball" >> "$GITHUB_OUTPUT" - name: pack cli package + if: steps.npm_cli.outputs.exists == 'false' id: pack_cli run: | mkdir -p dist/cli @@ -99,11 +131,13 @@ jobs: echo "tarball=$tarball" >> "$GITHUB_OUTPUT" - name: attest npm package artifacts + if: steps.npm_appdmg.outputs.exists == 'false' || steps.npm_cli.outputs.exists == 'false' uses: actions/attest@v4 with: subject-path: dist/**/*.tgz - name: upload npm package artifacts + if: steps.npm_appdmg.outputs.exists == 'false' || steps.npm_cli.outputs.exists == 'false' uses: actions/upload-artifact@v4 with: name: npm-packages @@ -111,12 +145,14 @@ jobs: if-no-files-found: error - name: publish appdmg package + if: steps.npm_appdmg.outputs.exists == 'false' run: npm publish "$TARBALL" --provenance --access public env: TARBALL: ${{ steps.pack_appdmg.outputs.tarball }} NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - name: publish cli package + if: steps.npm_cli.outputs.exists == 'false' run: npm publish "$TARBALL" --provenance --access public env: TARBALL: ${{ steps.pack_cli.outputs.tarball }} diff --git a/docs/npm-publishing.md b/docs/npm-publishing.md index 5b1ac55..a2fa0fb 100644 --- a/docs/npm-publishing.md +++ b/docs/npm-publishing.md @@ -31,8 +31,13 @@ Trusted Publishing works. Each publish workflow: - runs on a GitHub-hosted runner with `id-token: write`; +- ignores GitHub prereleases so prerelease tags cannot publish the npm `latest` + dist-tag by accident; - installs with Node.js 24; - runs tests, audit, and runtime dependency checks; +- checks whether the exact package version already exists on npm so rerunning a + partially successful release can continue with the remaining unpublished + packages; - creates the exact npm package tarball with `npm pack`; - creates a GitHub artifact attestation for that `.tgz`; - uploads the `.tgz` as a workflow artifact; From fbc82f818d6c8fe6101fdf6bf7990d773c156cab Mon Sep 17 00:00:00 2001 From: Jozef Izso Date: Sat, 2 May 2026 13:57:13 +0200 Subject: [PATCH 3/5] Simplify npm publish workflow --- .github/workflows/publish.yml | 95 +++++++++++++++++++---------------- docs/npm-publishing.md | 12 +++-- 2 files changed, 60 insertions(+), 47 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index b840a5a..a7205bd 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -11,9 +11,56 @@ permissions: artifact-metadata: write jobs: + check: + name: check npm package versions + if: github.event.release.prerelease == false + runs-on: ubuntu-latest + outputs: + should_publish: ${{ steps.npm_versions.outputs.should_publish }} + + steps: + - name: checkout + uses: actions/checkout@v6 + + - name: setup node 24 + uses: actions/setup-node@v6 + with: + node-version: 24 + registry-url: https://registry.npmjs.org + + - name: check npm package versions + id: npm_versions + run: | + appdmg_name="$(node -p "require('./package.json').name")" + appdmg_version="$(node -p "require('./package.json').version")" + cli_name="$(node -p "require('./packages/cli/package.json').name")" + cli_version="$(node -p "require('./packages/cli/package.json').version")" + + appdmg_exists=false + cli_exists=false + + if npm view "${appdmg_name}@${appdmg_version}" version --json >/dev/null 2>&1; then + echo "${appdmg_name}@${appdmg_version} is already published." + appdmg_exists=true + fi + + if npm view "${cli_name}@${cli_version}" version --json >/dev/null 2>&1; then + echo "${cli_name}@${cli_version} is already published." + cli_exists=true + fi + + if [ "$appdmg_exists" = true ] || [ "$cli_exists" = true ]; then + echo "At least one package version is already published; exiting without publishing." + echo "should_publish=false" >> "$GITHUB_OUTPUT" + else + echo "${appdmg_name}@${appdmg_version} and ${cli_name}@${cli_version} are not published yet; continuing." + echo "should_publish=true" >> "$GITHUB_OUTPUT" + fi + integration: name: integration node v24 macos-15-intel - if: github.event.release.prerelease == false + needs: check + if: needs.check.outputs.should_publish == 'true' runs-on: macos-15-intel steps: @@ -30,9 +77,11 @@ jobs: publish: name: publish to npmjs - if: github.event.release.prerelease == false + needs: + - check + - integration + if: needs.check.outputs.should_publish == 'true' runs-on: ubuntu-latest - needs: integration steps: - name: checkout @@ -45,12 +94,6 @@ jobs: registry-url: https://registry.npmjs.org cache: npm - - name: verify publish toolchain - run: | - node --version - npm --version - node -e "const version = require('node:child_process').execFileSync('npm', ['--version'], { encoding: 'utf8' }).trim(); const [major, minor, patch] = version.split('.').map(Number); if (major < 11 || (major === 11 && (minor < 5 || (minor === 5 && patch < 1)))) throw new Error('npm 11.5.1 or newer is required for trusted publishing')" - - name: install appdmg dependencies run: npm ci @@ -79,36 +122,7 @@ jobs: - name: verify cli runtime dependency tree run: npm ls --omit=dev --all --prefix packages/cli - - name: check appdmg npm package version - id: npm_appdmg - run: | - package_name="$(node -p "require('./package.json').name")" - package_version="$(node -p "require('./package.json').version")" - - if npm view "${package_name}@${package_version}" version --json >/dev/null 2>&1; then - echo "exists=true" >> "$GITHUB_OUTPUT" - else - echo "exists=false" >> "$GITHUB_OUTPUT" - fi - - echo "package=${package_name}@${package_version}" >> "$GITHUB_OUTPUT" - - - name: check cli npm package version - id: npm_cli - run: | - package_name="$(node -p "require('./packages/cli/package.json').name")" - package_version="$(node -p "require('./packages/cli/package.json').version")" - - if npm view "${package_name}@${package_version}" version --json >/dev/null 2>&1; then - echo "exists=true" >> "$GITHUB_OUTPUT" - else - echo "exists=false" >> "$GITHUB_OUTPUT" - fi - - echo "package=${package_name}@${package_version}" >> "$GITHUB_OUTPUT" - - name: pack appdmg package - if: steps.npm_appdmg.outputs.exists == 'false' id: pack_appdmg run: | mkdir -p dist/appdmg @@ -118,7 +132,6 @@ jobs: echo "tarball=$tarball" >> "$GITHUB_OUTPUT" - name: pack cli package - if: steps.npm_cli.outputs.exists == 'false' id: pack_cli run: | mkdir -p dist/cli @@ -131,13 +144,11 @@ jobs: echo "tarball=$tarball" >> "$GITHUB_OUTPUT" - name: attest npm package artifacts - if: steps.npm_appdmg.outputs.exists == 'false' || steps.npm_cli.outputs.exists == 'false' uses: actions/attest@v4 with: subject-path: dist/**/*.tgz - name: upload npm package artifacts - if: steps.npm_appdmg.outputs.exists == 'false' || steps.npm_cli.outputs.exists == 'false' uses: actions/upload-artifact@v4 with: name: npm-packages @@ -145,14 +156,12 @@ jobs: if-no-files-found: error - name: publish appdmg package - if: steps.npm_appdmg.outputs.exists == 'false' run: npm publish "$TARBALL" --provenance --access public env: TARBALL: ${{ steps.pack_appdmg.outputs.tarball }} NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - name: publish cli package - if: steps.npm_cli.outputs.exists == 'false' run: npm publish "$TARBALL" --provenance --access public env: TARBALL: ${{ steps.pack_cli.outputs.tarball }} diff --git a/docs/npm-publishing.md b/docs/npm-publishing.md index a2fa0fb..397d0d5 100644 --- a/docs/npm-publishing.md +++ b/docs/npm-publishing.md @@ -33,19 +33,23 @@ Each publish workflow: - runs on a GitHub-hosted runner with `id-token: write`; - ignores GitHub prereleases so prerelease tags cannot publish the npm `latest` dist-tag by accident; +- checks whether the exact package version already exists on npm before running + tests, package creation, attestation, or publishing; - installs with Node.js 24; - runs tests, audit, and runtime dependency checks; -- checks whether the exact package version already exists on npm so rerunning a - partially successful release can continue with the remaining unpublished - packages; - creates the exact npm package tarball with `npm pack`; - creates a GitHub artifact attestation for that `.tgz`; - uploads the `.tgz` as a workflow artifact; - publishes the same `.tgz` to npm with provenance enabled. +If the exact package version is already published, the publish path exits +without running the package, attestation, upload, or npm publish steps. + The appdmg CLI repository publishes two packages from one workflow. It publishes `@appdmg/appdmg` first, then `@appdmg/cli`, and verifies that the CLI depends on -the exact library package version. +the exact library package version. If either package version is already +published, the workflow exits without publishing either package. That keeps the +release contract simple and avoids mixing old and new artifacts for one release. ## Release order From a85f0dc2abf01b36f476e36bfac3aa03f5386f76 Mon Sep 17 00:00:00 2001 From: Jozef Izso Date: Sat, 2 May 2026 14:16:50 +0200 Subject: [PATCH 4/5] Use pinned npm publish status action --- .github/workflows/publish.yml | 131 +++++++++++++++++++++++++++++----- docs/npm-publishing.md | 6 +- 2 files changed, 117 insertions(+), 20 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index a7205bd..516b275 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -16,7 +16,7 @@ jobs: if: github.event.release.prerelease == false runs-on: ubuntu-latest outputs: - should_publish: ${{ steps.npm_versions.outputs.should_publish }} + should_publish: ${{ steps.appdmg_publish_status.outputs.exists == '0' && steps.cli_publish_status.outputs.exists == '0' }} steps: - name: checkout @@ -28,33 +28,48 @@ jobs: node-version: 24 registry-url: https://registry.npmjs.org - - name: check npm package versions - id: npm_versions + - name: check appdmg npm package version + id: appdmg_publish_status + uses: tehpsalmist/npm-publish-status-action@01cb25946b194a7a5468f22c8e74db04c283f121 + + - name: prepare cli package metadata run: | - appdmg_name="$(node -p "require('./package.json').name")" - appdmg_version="$(node -p "require('./package.json').version")" - cli_name="$(node -p "require('./packages/cli/package.json').name")" - cli_version="$(node -p "require('./packages/cli/package.json').version")" + cp package.json package.appdmg.json + cp packages/cli/package.json package.json + + - name: check cli npm package version + id: cli_publish_status + uses: tehpsalmist/npm-publish-status-action@01cb25946b194a7a5468f22c8e74db04c283f121 - appdmg_exists=false - cli_exists=false + - name: report npm package version status + run: | + appdmg_name="$(node -p "require('./package.appdmg.json').name")" + appdmg_version="$(node -p "require('./package.appdmg.json').version")" + cli_name="$(node -p "require('./package.json').name")" + cli_version="$(node -p "require('./package.json').version")" - if npm view "${appdmg_name}@${appdmg_version}" version --json >/dev/null 2>&1; then + if [ "${{ steps.appdmg_publish_status.outputs.exists }}" = "1" ]; then echo "${appdmg_name}@${appdmg_version} is already published." - appdmg_exists=true + elif [ "${{ steps.appdmg_publish_status.outputs.exists }}" = "0" ]; then + echo "${appdmg_name}@${appdmg_version} is not published yet." + else + echo "::error::Unexpected appdmg npm publish status: ${{ steps.appdmg_publish_status.outputs.exists }}" + exit 1 fi - if npm view "${cli_name}@${cli_version}" version --json >/dev/null 2>&1; then + if [ "${{ steps.cli_publish_status.outputs.exists }}" = "1" ]; then echo "${cli_name}@${cli_version} is already published." - cli_exists=true + elif [ "${{ steps.cli_publish_status.outputs.exists }}" = "0" ]; then + echo "${cli_name}@${cli_version} is not published yet." + else + echo "::error::Unexpected cli npm publish status: ${{ steps.cli_publish_status.outputs.exists }}" + exit 1 fi - if [ "$appdmg_exists" = true ] || [ "$cli_exists" = true ]; then + if [ "${{ steps.appdmg_publish_status.outputs.exists }}" = "1" ] || [ "${{ steps.cli_publish_status.outputs.exists }}" = "1" ]; then echo "At least one package version is already published; exiting without publishing." - echo "should_publish=false" >> "$GITHUB_OUTPUT" else - echo "${appdmg_name}@${appdmg_version} and ${cli_name}@${cli_version} are not published yet; continuing." - echo "should_publish=true" >> "$GITHUB_OUTPUT" + echo "Both package versions are not published yet; continuing." fi integration: @@ -75,8 +90,8 @@ jobs: - run: npm test --prefix packages/cli - run: npm run test:integration - publish: - name: publish to npmjs + verify: + name: verify npm packages needs: - check - integration @@ -122,6 +137,84 @@ jobs: - name: verify cli runtime dependency tree run: npm ls --omit=dev --all --prefix packages/cli + prepublish_check: + name: check npm package versions before publish + needs: verify + if: github.event.release.prerelease == false + runs-on: ubuntu-latest + outputs: + should_publish: ${{ steps.appdmg_publish_status.outputs.exists == '0' && steps.cli_publish_status.outputs.exists == '0' }} + + steps: + - name: checkout + uses: actions/checkout@v6 + + - name: setup node 24 + uses: actions/setup-node@v6 + with: + node-version: 24 + registry-url: https://registry.npmjs.org + + - name: check appdmg npm package version + id: appdmg_publish_status + uses: tehpsalmist/npm-publish-status-action@01cb25946b194a7a5468f22c8e74db04c283f121 + + - name: prepare cli package metadata + run: | + cp package.json package.appdmg.json + cp packages/cli/package.json package.json + + - name: check cli npm package version + id: cli_publish_status + uses: tehpsalmist/npm-publish-status-action@01cb25946b194a7a5468f22c8e74db04c283f121 + + - name: report npm package version status + run: | + appdmg_name="$(node -p "require('./package.appdmg.json').name")" + appdmg_version="$(node -p "require('./package.appdmg.json').version")" + cli_name="$(node -p "require('./package.json').name")" + cli_version="$(node -p "require('./package.json').version")" + + if [ "${{ steps.appdmg_publish_status.outputs.exists }}" = "1" ]; then + echo "${appdmg_name}@${appdmg_version} was published while this workflow was verifying." + elif [ "${{ steps.appdmg_publish_status.outputs.exists }}" = "0" ]; then + echo "${appdmg_name}@${appdmg_version} is still not published." + else + echo "::error::Unexpected appdmg npm publish status: ${{ steps.appdmg_publish_status.outputs.exists }}" + exit 1 + fi + + if [ "${{ steps.cli_publish_status.outputs.exists }}" = "1" ]; then + echo "${cli_name}@${cli_version} was published while this workflow was verifying." + elif [ "${{ steps.cli_publish_status.outputs.exists }}" = "0" ]; then + echo "${cli_name}@${cli_version} is still not published." + else + echo "::error::Unexpected cli npm publish status: ${{ steps.cli_publish_status.outputs.exists }}" + exit 1 + fi + + if [ "${{ steps.appdmg_publish_status.outputs.exists }}" = "1" ] || [ "${{ steps.cli_publish_status.outputs.exists }}" = "1" ]; then + echo "At least one package version is already published; exiting without publishing." + else + echo "Both package versions are still not published; continuing." + fi + + publish: + name: publish to npmjs + needs: prepublish_check + if: needs.prepublish_check.outputs.should_publish == 'true' + runs-on: ubuntu-latest + + steps: + - name: checkout + uses: actions/checkout@v6 + + - name: setup node 24 + uses: actions/setup-node@v6 + with: + node-version: 24 + registry-url: https://registry.npmjs.org + - name: pack appdmg package id: pack_appdmg run: | diff --git a/docs/npm-publishing.md b/docs/npm-publishing.md index 397d0d5..519f945 100644 --- a/docs/npm-publishing.md +++ b/docs/npm-publishing.md @@ -34,9 +34,13 @@ Each publish workflow: - ignores GitHub prereleases so prerelease tags cannot publish the npm `latest` dist-tag by accident; - checks whether the exact package version already exists on npm before running - tests, package creation, attestation, or publishing; + verification work; - installs with Node.js 24; - runs tests, audit, and runtime dependency checks; +- re-checks the exact package version with + `tehpsalmist/npm-publish-status-action` pinned to + `01cb25946b194a7a5468f22c8e74db04c283f121` immediately before the publish + job; - creates the exact npm package tarball with `npm pack`; - creates a GitHub artifact attestation for that `.tgz`; - uploads the `.tgz` as a workflow artifact; From cce6e102669b5bf429b5fccee8c5dda7fc7ab127 Mon Sep 17 00:00:00 2001 From: Jozef Izso Date: Sat, 2 May 2026 14:27:05 +0200 Subject: [PATCH 5/5] Simplify npm publish workflow --- .github/workflows/publish.yml | 162 +--------------------------------- docs/npm-publishing.md | 21 ++--- 2 files changed, 10 insertions(+), 173 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 516b275..4fe8d60 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -41,61 +41,10 @@ jobs: id: cli_publish_status uses: tehpsalmist/npm-publish-status-action@01cb25946b194a7a5468f22c8e74db04c283f121 - - name: report npm package version status - run: | - appdmg_name="$(node -p "require('./package.appdmg.json').name")" - appdmg_version="$(node -p "require('./package.appdmg.json').version")" - cli_name="$(node -p "require('./package.json').name")" - cli_version="$(node -p "require('./package.json').version")" - - if [ "${{ steps.appdmg_publish_status.outputs.exists }}" = "1" ]; then - echo "${appdmg_name}@${appdmg_version} is already published." - elif [ "${{ steps.appdmg_publish_status.outputs.exists }}" = "0" ]; then - echo "${appdmg_name}@${appdmg_version} is not published yet." - else - echo "::error::Unexpected appdmg npm publish status: ${{ steps.appdmg_publish_status.outputs.exists }}" - exit 1 - fi - - if [ "${{ steps.cli_publish_status.outputs.exists }}" = "1" ]; then - echo "${cli_name}@${cli_version} is already published." - elif [ "${{ steps.cli_publish_status.outputs.exists }}" = "0" ]; then - echo "${cli_name}@${cli_version} is not published yet." - else - echo "::error::Unexpected cli npm publish status: ${{ steps.cli_publish_status.outputs.exists }}" - exit 1 - fi - - if [ "${{ steps.appdmg_publish_status.outputs.exists }}" = "1" ] || [ "${{ steps.cli_publish_status.outputs.exists }}" = "1" ]; then - echo "At least one package version is already published; exiting without publishing." - else - echo "Both package versions are not published yet; continuing." - fi - - integration: - name: integration node v24 macos-15-intel + publish: + name: publish to npmjs needs: check if: needs.check.outputs.should_publish == 'true' - runs-on: macos-15-intel - - steps: - - uses: actions/checkout@v6 - - uses: actions/setup-node@v6 - with: - node-version: 24 - cache: npm - - run: npm ci - - run: npm test - - run: npm ci --prefix packages/cli - - run: npm test --prefix packages/cli - - run: npm run test:integration - - verify: - name: verify npm packages - needs: - - check - - integration - if: needs.check.outputs.should_publish == 'true' runs-on: ubuntu-latest steps: @@ -115,106 +64,6 @@ jobs: - name: install cli dependencies run: npm ci --prefix packages/cli - - name: verify cli dependency contract - run: | - node -e "const root = require('./package.json'); const cli = require('./packages/cli/package.json'); if (cli.dependencies['@appdmg/appdmg'] !== root.version) throw new Error('@appdmg/cli must depend on the exact @appdmg/appdmg package version')" - - - name: test appdmg - run: npm test - - - name: test cli - run: npm test --prefix packages/cli - - - name: audit appdmg dependencies - run: npm audit --audit-level=moderate - - - name: audit cli dependencies - run: npm audit --audit-level=moderate --prefix packages/cli - - - name: verify appdmg runtime dependency tree - run: npm ls --omit=dev --all - - - name: verify cli runtime dependency tree - run: npm ls --omit=dev --all --prefix packages/cli - - prepublish_check: - name: check npm package versions before publish - needs: verify - if: github.event.release.prerelease == false - runs-on: ubuntu-latest - outputs: - should_publish: ${{ steps.appdmg_publish_status.outputs.exists == '0' && steps.cli_publish_status.outputs.exists == '0' }} - - steps: - - name: checkout - uses: actions/checkout@v6 - - - name: setup node 24 - uses: actions/setup-node@v6 - with: - node-version: 24 - registry-url: https://registry.npmjs.org - - - name: check appdmg npm package version - id: appdmg_publish_status - uses: tehpsalmist/npm-publish-status-action@01cb25946b194a7a5468f22c8e74db04c283f121 - - - name: prepare cli package metadata - run: | - cp package.json package.appdmg.json - cp packages/cli/package.json package.json - - - name: check cli npm package version - id: cli_publish_status - uses: tehpsalmist/npm-publish-status-action@01cb25946b194a7a5468f22c8e74db04c283f121 - - - name: report npm package version status - run: | - appdmg_name="$(node -p "require('./package.appdmg.json').name")" - appdmg_version="$(node -p "require('./package.appdmg.json').version")" - cli_name="$(node -p "require('./package.json').name")" - cli_version="$(node -p "require('./package.json').version")" - - if [ "${{ steps.appdmg_publish_status.outputs.exists }}" = "1" ]; then - echo "${appdmg_name}@${appdmg_version} was published while this workflow was verifying." - elif [ "${{ steps.appdmg_publish_status.outputs.exists }}" = "0" ]; then - echo "${appdmg_name}@${appdmg_version} is still not published." - else - echo "::error::Unexpected appdmg npm publish status: ${{ steps.appdmg_publish_status.outputs.exists }}" - exit 1 - fi - - if [ "${{ steps.cli_publish_status.outputs.exists }}" = "1" ]; then - echo "${cli_name}@${cli_version} was published while this workflow was verifying." - elif [ "${{ steps.cli_publish_status.outputs.exists }}" = "0" ]; then - echo "${cli_name}@${cli_version} is still not published." - else - echo "::error::Unexpected cli npm publish status: ${{ steps.cli_publish_status.outputs.exists }}" - exit 1 - fi - - if [ "${{ steps.appdmg_publish_status.outputs.exists }}" = "1" ] || [ "${{ steps.cli_publish_status.outputs.exists }}" = "1" ]; then - echo "At least one package version is already published; exiting without publishing." - else - echo "Both package versions are still not published; continuing." - fi - - publish: - name: publish to npmjs - needs: prepublish_check - if: needs.prepublish_check.outputs.should_publish == 'true' - runs-on: ubuntu-latest - - steps: - - name: checkout - uses: actions/checkout@v6 - - - name: setup node 24 - uses: actions/setup-node@v6 - with: - node-version: 24 - registry-url: https://registry.npmjs.org - - name: pack appdmg package id: pack_appdmg run: | @@ -241,13 +90,6 @@ jobs: with: subject-path: dist/**/*.tgz - - name: upload npm package artifacts - uses: actions/upload-artifact@v4 - with: - name: npm-packages - path: dist/**/*.tgz - if-no-files-found: error - - name: publish appdmg package run: npm publish "$TARBALL" --provenance --access public env: diff --git a/docs/npm-publishing.md b/docs/npm-publishing.md index 519f945..a02685b 100644 --- a/docs/npm-publishing.md +++ b/docs/npm-publishing.md @@ -33,27 +33,22 @@ Each publish workflow: - runs on a GitHub-hosted runner with `id-token: write`; - ignores GitHub prereleases so prerelease tags cannot publish the npm `latest` dist-tag by accident; -- checks whether the exact package version already exists on npm before running - verification work; -- installs with Node.js 24; -- runs tests, audit, and runtime dependency checks; -- re-checks the exact package version with +- checks whether the exact package version already exists on npm with `tehpsalmist/npm-publish-status-action` pinned to - `01cb25946b194a7a5468f22c8e74db04c283f121` immediately before the publish - job; + `01cb25946b194a7a5468f22c8e74db04c283f121`; +- installs with Node.js 24; +- installs dependencies with `npm ci`; - creates the exact npm package tarball with `npm pack`; - creates a GitHub artifact attestation for that `.tgz`; -- uploads the `.tgz` as a workflow artifact; - publishes the same `.tgz` to npm with provenance enabled. If the exact package version is already published, the publish path exits -without running the package, attestation, upload, or npm publish steps. +without running the package, attestation, or npm publish steps. Build, test, +audit, and runtime dependency checks remain in the normal build/CI workflows. The appdmg CLI repository publishes two packages from one workflow. It publishes -`@appdmg/appdmg` first, then `@appdmg/cli`, and verifies that the CLI depends on -the exact library package version. If either package version is already -published, the workflow exits without publishing either package. That keeps the -release contract simple and avoids mixing old and new artifacts for one release. +`@appdmg/appdmg` first, then `@appdmg/cli`. If either package version is already +published, the workflow exits without publishing either package. ## Release order