diff --git a/.changeset/trusted-releases.md b/.changeset/trusted-releases.md new file mode 100644 index 0000000..d7e1b1b --- /dev/null +++ b/.changeset/trusted-releases.md @@ -0,0 +1,5 @@ +--- +"@su-engineering/heic": patch +--- + +Document the initial public release and the automated Changesets release process with npm trusted publishing. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2b5ab35..db85bd1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,8 +1,8 @@ name: CI on: - push: - branches: [master] + workflow_call: + workflow_dispatch: pull_request: permissions: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..fe95e2b --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,71 @@ +name: Release + +on: + push: + branches: [master] + workflow_dispatch: + +permissions: {} + +concurrency: + group: release-master + cancel-in-progress: false + +jobs: + validate: + uses: ./.github/workflows/ci.yml + permissions: + contents: read + + release: + if: github.ref == 'refs/heads/master' + needs: validate + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: write + pull-requests: write + actions: write + id-token: write + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 + with: + node-version: 24 + registry-url: https://registry.npmjs.org + - name: Install OIDC-capable npm + run: npm install --global npm@11 + - run: pnpm install --frozen-lockfile + - run: pnpm build + - run: pnpm test:package + - name: Ensure this is still the current release commit + id: current + env: + GH_TOKEN: ${{ github.token }} + EXPECTED_SHA: ${{ github.sha }} + run: | + current_sha=$(gh api "repos/$GITHUB_REPOSITORY/commits/master" --jq .sha) + if [ "$current_sha" = "$EXPECTED_SHA" ]; then + echo "matches=true" >> "$GITHUB_OUTPUT" + else + echo "A newer master commit exists; its release run will handle publication." + fi + - name: Create version PR or publish via trusted publishing + if: steps.current.outputs.matches == 'true' + id: changesets + uses: changesets/action@a45c4d594aa4e2c509dc14a9f2b3b67ba3780d0d # v1 + with: + version: pnpm exec changeset version + publish: pnpm release + title: 'Release: update package version' + commit: 'chore: version package' + # GITHUB_TOKEN-created PRs do not trigger pull_request workflows. + # Explicit dispatch validates the generated version branch without a PAT. + - name: Run CI on the generated version PR + if: steps.changesets.outputs.pullRequestNumber != '' + env: + GH_TOKEN: ${{ github.token }} + run: gh workflow run ci.yml --ref changeset-release/master diff --git a/.gitignore b/.gitignore index f510361..d6ec8ea 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -node_modules/ +node_modules dist/ .DS_Store *.log diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a3130f7..f40abd7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,7 +17,7 @@ pnpm test:package pnpm test:unit ``` -The package can be used before registry publication by building it and importing `dist/index.js`, or by installing a tarball created with `pnpm pack --pack-destination /tmp/heic-pack`. The WASM entry point is `dist/wasm.js`; its optional peer dependency must be available in the consuming project. +A source checkout can be used by building it and importing `dist/index.js`, or by installing a tarball created with `pnpm pack --pack-destination /tmp/heic-pack`. The WASM entry point is `dist/wasm.js`; its optional peer dependency must be available in the consuming project. ## Repository map @@ -81,3 +81,9 @@ Do not commit `dist/`, browser reports, `node_modules/`, or the private photo co ## Community expectations Be respectful, explain disagreements with evidence, and focus reviews on the work. Do not harass contributors or disclose private information. Report conduct concerns privately to hello@su.engineering. Use the [security policy](SECURITY.md) for vulnerabilities. + +## Releases + +Package changes use Changesets. Once merged into `master`, the release workflow +creates a version/changelog PR. Merging that PR publishes its new version after +validation using npm trusted publishing. See [the release guide](docs/releasing.md). diff --git a/README.md b/README.md index b7c1ead..1652d80 100644 --- a/README.md +++ b/README.md @@ -15,17 +15,17 @@ HEIC uploads need not force every visitor to download a software codec. This Typ The project is at **0.1.0**. Test it with representative files and target devices before production use. The repository is named `heic-web`; the npm package name is `@su-engineering/heic`. -[API reference](docs/api.md) · [Compatibility and limitations](docs/compatibility.md) · [Contributing](CONTRIBUTING.md) · [Security](SECURITY.md) +[API reference](docs/api.md) · [Compatibility and limitations](docs/compatibility.md) · [Contributing](CONTRIBUTING.md) · [Security](SECURITY.md) · [Release process](docs/releasing.md) ## Installation -When the package has been published to your registry: +Install the package: ```sh npm install @su-engineering/heic ``` -For a checkout before publication, see [local development](CONTRIBUTING.md#local-development). Decoding runs in a browser or browser worker. Node.js can run the container parser, but this package does not provide a Node.js pixel decoder. +For a source checkout, see [local development](CONTRIBUTING.md#local-development). Decoding runs in a browser or browser worker. Node.js can run the container parser, but this package does not provide a Node.js pixel decoder. ## Quick start diff --git a/docs/releasing.md b/docs/releasing.md index 321b73e..ba8c947 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -1,42 +1,57 @@ -# Release checklist +# Releasing -Releases are maintainer-operated. The repository does not automatically publish to npm on push. Changesets is configured for public package publishing with `master` as the base branch. +The initial package is `@su-engineering/heic@0.1.0`. The `/wasm` export ships in the same package. Ongoing releases use Changesets, validated GitHub Actions, and npm trusted publishing (OIDC); no npm publishing secret is required. -## Before making the repository public +## One-time npm configuration -- Review the complete Git history and tracked files for credentials, private photos, sensitive metadata, and code/assets you cannot redistribute. Ignoring a file now does not remove historical copies. -- Confirm the MIT license and ownership; review optional codec license notices separately. -- Set the GitHub description, documentation homepage, and relevant topics. Keep visibility private until the owner chooses to open-source it. -- Enable issues and configure private vulnerability reporting if desired; ensure the security contact is monitored. -- Configure branch protection or a ruleset requiring the CI `check` and `browser` jobs, with appropriate maintainer access. -- Verify CI on the release commit and manually test platform HEVC and released Safari using representative, consented Apple photos, including grids. +In the npm package settings for `@su-engineering/heic`, add a GitHub Actions trusted publisher: -## Prepare a version +| Field | Value | +| --- | --- | +| Organization or user | `su-engineering` | +| Repository | `heic-web` | +| Workflow filename | `release.yml` | +| Environment | Leave blank; this workflow does not use a GitHub environment. | +| Allowed actions | Enable direct `npm publish`. Stage-only permission does not support this workflow. | -```sh -pnpm install --frozen-lockfile -pnpm test:all -pnpm exec changeset -pnpm exec changeset version -``` +The workflow file must exist on the default branch. The setting requires an interactive npm account/2FA operation. Never paste publishing credentials into an issue, pull request, or source file. Revoke the temporary initial-publish token after the launch. + +See [npm's trusted-publishing documentation](https://docs.npmjs.com/trusted-publishers/). Publishing uses npm 11 on Node.js 24 and GitHub-hosted runners, with `id-token: write` scoped to the release job. The publish script explicitly invokes npm, rather than depending on a package manager's bundled publishing client. + +GitHub Actions also needs permission to create pull requests. Under repository Settings → Actions → General, enable **Allow GitHub Actions to create and approve pull requests**. The workflow does not approve or auto-merge its own PRs. + +## Normal release flow -Review version/changelog changes and commit them. For an initial release already at the intended version, avoid an accidental extra version bump. Inspect the package before publishing: +1. Include a changeset with package changes: `pnpm exec changeset`. Choose patch, minor, or major and describe the observable change. Documentation/test-only changes can omit a changeset when no package release is needed. +2. Merge the contribution PR into `master` after CI passes. +3. `Release` runs the shared typecheck/build/package/unit and three-engine browser checks. If they pass, Changesets creates or updates a **Release: update package version** PR containing version/changelog changes. +4. The workflow explicitly dispatches CI on `changeset-release/master`. This is necessary because PRs created by `GITHUB_TOKEN` do not automatically trigger PR workflows. Verify the generated branch's successful checks before merging it. +5. Merge the version PR when ready. The release workflow validates that commit, publishes its new version publicly with provenance, and creates a `vX.Y.Z` tag and GitHub release. + +Only the generated version PR determines a new package version; ordinary commits do not each publish. Never edit an already published version in place. Choose a new version through a changeset. + +`tools/publish.mjs` queries the exact registry version and exits without publishing if it already exists. Unexpected registry errors fail closed. A newer `master` commit causes an older queued release to skip publishing so its successor can handle the release. + +## Validation and package review ```sh +pnpm install --frozen-lockfile +pnpm test:all mkdir -p /tmp/heic-pack pnpm pack --pack-destination /tmp/heic-pack ``` -Install the tarball into a clean consumer project. Verify the ESM and declaration entry points, the `heic.global.js` browser bundle, and lazy fallback integration with the supported libheif build. The package should contain built assets, docs, README, security policy, and license, without test photographs or local dependencies. +Install the tarball in a clean consumer. Verify ESM/types, the `heic.global.js` browser bundle, and lazy WASM integration. Published files must include built assets and documentation without private test photos or development dependencies. -`pnpm test:package` checks declared build targets and executes the standalone bundle in a sandbox. `prepublishOnly` checks type safety, build targets, and unit tests; it does not run the browser suite. Browser checks and device testing remain release requirements. +`pnpm test:package` validates declared targets and creates a bundled browser consumer for default-adapter tests. `prepublishOnly` checks type safety, build/package targets, and unit tests; the release workflow runs the browser matrix before permitting publication. -## Publish +CI does not establish released-Safari or hardware HEVC coverage. Test representative consented Apple photos, tiled grids, orientations, and color behavior on target devices before claiming device support. Record skipped tests. -Confirm npm organization access and your authenticated publishing identity. When explicitly authorized to publish: +## Recovery -```sh -pnpm release -``` +- If npm rejects OIDC authentication, confirm the trusted-publisher fields exactly match this repository and `release.yml`, direct publishing is allowed, and the workflow uses a GitHub-hosted runner with Node/npm versions meeting npm's requirements. +- If npm publishing succeeds but tagging/release creation fails, do not republish or unpublish the version. Verify the registry's `gitHead`, create the missing `vX.Y.Z` tag on that commit, and create the GitHub release from its changelog. +- If a release run is interrupted before publishing, rerun `Release` on `master`. Already published versions are skipped. +- Rotate/revoke any fallback manual publishing token and keep it outside the repository. Trusted publishing needs no persistent npm token. -This invokes `changeset publish`. Review the published version and registry access, then push any generated tags and create release notes summarizing behavior changes and known compatibility limits. Add provenance/trusted publishing through a separately reviewed release workflow if needed; never commit registry tokens. +For an explicitly authorized emergency manual release, authenticate interactively and run `npm publish --access public` on the validated version commit. Create its tag/release afterward. Do not use the automated release script outside GitHub Actions. diff --git a/package.json b/package.json index 0c36ac7..e8a0d49 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "dump": "node --experimental-strip-types tools/dump.ts", "validate-corpus": "node --experimental-strip-types tools/validate-corpus.ts", "prepublishOnly": "pnpm typecheck && pnpm build && pnpm test:package && pnpm test:unit", - "release": "changeset publish", + "release": "node tools/publish.mjs", "test:package": "node tools/check-package.mjs" }, "keywords": [ diff --git a/tools/publish.mjs b/tools/publish.mjs new file mode 100644 index 0000000..d6374f8 --- /dev/null +++ b/tools/publish.mjs @@ -0,0 +1,32 @@ +/** Publish this single-package repository with npm's OIDC-capable CLI. */ +import { readFileSync } from 'node:fs'; +import { execFileSync, spawnSync } from 'node:child_process'; + +const pkg = JSON.parse(readFileSync('package.json', 'utf8')); +const registry = 'https://registry.npmjs.org'; +const endpoint = `${registry}/${encodeURIComponent(pkg.name)}/${encodeURIComponent(pkg.version)}`; +const response = await fetch(endpoint, { signal: AbortSignal.timeout(30_000) }); +if (response.ok) { + console.log(`${pkg.name}@${pkg.version} is already published; no release needed.`); + process.exit(0); +} +if (response.status !== 404) { + throw new Error(`Registry check failed with HTTP ${response.status}; refusing to publish.`); +} +const tag = `v${pkg.version}`; +const existingTag = spawnSync('git', ['rev-parse', '--verify', `refs/tags/${tag}`], { encoding: 'utf8' }); +if (existingTag.status === 0) { + throw new Error(`${tag} already exists but the registry version is missing; investigate before publishing.`); +} +if (process.argv.includes('--dry-run')) { + console.log(`Would publish ${pkg.name}@${pkg.version} publicly with provenance, then create ${tag}.`); + process.exit(0); +} +if (!process.env.CI || !process.env.ACTIONS_ID_TOKEN_REQUEST_URL) { + throw new Error('Automated releases require GitHub Actions OIDC. For a manual release, use npm publish.'); +} +const result = spawnSync('npm', ['publish', '--access', 'public', '--provenance'], { stdio: 'inherit' }); +if (result.status !== 0) process.exit(result.status ?? 1); +execFileSync('git', ['tag', tag]); +// Changesets Action recognizes this marker and pushes the tag/creates a release. +console.log(`New tag: ${tag}`);