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
5 changes: 5 additions & 0 deletions .changeset/trusted-releases.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@su-engineering/heic": patch
---

Document the initial public release and the automated Changesets release process with npm trusted publishing.
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
name: CI

on:
push:
branches: [master]
workflow_call:
workflow_dispatch:
pull_request:

permissions:
Expand Down
71 changes: 71 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
node_modules/
node_modules
dist/
.DS_Store
*.log
Expand Down
8 changes: 7 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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).
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
65 changes: 40 additions & 25 deletions docs/releasing.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down
32 changes: 32 additions & 0 deletions tools/publish.mjs
Original file line number Diff line number Diff line change
@@ -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}`);
Loading