diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..4fe8d60 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,103 @@ +name: publish + +on: + release: + types: [published] + +permissions: + contents: read + id-token: write + attestations: write + artifact-metadata: write + +jobs: + check: + name: check npm package versions + 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 + + publish: + name: publish to npmjs + needs: check + if: needs.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 + cache: npm + + - name: install appdmg dependencies + run: npm ci + + - name: install cli dependencies + run: npm ci --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: 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..a02685b --- /dev/null +++ b/docs/npm-publishing.md @@ -0,0 +1,69 @@ +# 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`; +- 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 with + `tehpsalmist/npm-publish-status-action` pinned to + `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`; +- 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, 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`. If either package version is already +published, the workflow exits without publishing either package. + +## 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 +```