Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 123 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
name: publish

on:
release:
types: [published]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Restrict publish trigger to non-prerelease releases

Using release.types: [published] will also fire for GitHub prereleases, so this workflow can run npm publish for prerelease tags. Because npm publish here does not set a non-latest dist-tag, a prerelease run can unintentionally promote prerelease packages to normal consumers. Add a guard (for example, checking github.event.release.prerelease == false) or explicit prerelease tagging logic.

Useful? React with 👍 / 👎.


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
Comment on lines +113 to +114

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Make package publish steps rerun-safe

The workflow publishes @appdmg/appdmg and then @appdmg/cli in one linear job without checking whether a version is already on npm. If the first publish succeeds but the second fails (for example due to a transient npm outage), rerunning this workflow for the same release will fail on the first npm publish with “version already exists,” blocking recovery for the CLI package without creating a new release/version.

Useful? React with 👍 / 👎.

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 }}
4 changes: 4 additions & 0 deletions docs/node24-migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
61 changes: 61 additions & 0 deletions docs/npm-publishing.md
Original file line number Diff line number Diff line change
@@ -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
```