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 +```