# Release pipeline

[semantic-release](https://semantic-release.gitbook.io/) handles releases. On
each push to `main`, the `release` job in
[`.github/workflows/ci.yml`](../.github/workflows/ci.yml) checks the merged
commit messages. A `feat:`, `fix:`, `perf:`, `refactor:`, `build:`,
`remove:`, `docs(README):`, or `BREAKING CHANGE:` tells it to choose the next SemVer
version and run these steps:

1. Waits for the workflow's validation, full shell suite, strict Go-engine checks, Linux cross-compile smoke, Windows and macOS Go tests.
2. Regenerates `CHANGELOG.md` from the commits.
3. Syncs the new version into `package.json` and the README status line (`scripts/sync-version.sh`), then stages only those known overlays plus the generated changelog so the release index owns their bytes.
4. Builds `dist/devrites-v<version>.tar.gz` and `dist/install.sh`, each with an exact-filename SHA-256 sidecar, via `scripts/build-release-tarball.sh`. Payload paths and bytes are materialized from the repository-root Git index; the build fails outside that index and never copies a divergent live worktree file. A stdlib Go packager sorts member names and normalizes ownership, modes, timestamps, and the gzip header, so identical indexed sources produce identical archives. `SOURCE_DATE_EPOCH` selects the canonical timestamp and defaults to `0`. The bundle includes the checked-in host-native Claude/Codex artifacts under `pack/generated/`; symlinks and other non-regular payload entries are rejected.
5. Cross-compiles five `devrites-engine` release binaries (macOS arm64/amd64, Linux arm64/amd64, Windows amd64) plus a SHA-256 sidecar for each.
6. Publishes the `devrites` package to npm through `@semantic-release/npm` with
   provenance attestation. The job requests OIDC (`id-token: write`) so a
   future npm Trusted Publisher can take over, and today authenticates with
   `secrets.NPM_TOKEN` because npm's OIDC exchange still returns
   "package not found" until that Trusted Publisher is configured for
   `devrites`. This is what `npx devrites@latest` resolves. `npm pack`
   regenerates the same `pack/generated/` artifacts during `prepack`; there
   is no `postpack` cleanup step.
7. Commits the version bump + changelog as `chore(release): <version> [skip ci]`, creates tag `v<version>`, and publishes a GitHub Release with the tarball, verified installer, binaries, and every checksum sidecar attached. The release job then attests the artifacts with build provenance (`actions/attest-build-provenance`), independent of npm provenance.

Package prepack normally owns host artifact generation; the release archive
consumes the validated generated files from the same Git index as the rest of
its payload. Package and release installs validate and copy
`pack/generated/{claude,codex}`. When a shell install or update shim runs from a
source checkout whose generated payload is incomplete, the shim may regenerate
the missing host payload before handing that local candidate to the engine. The
engine itself only validates and copies host payloads; it never generates them.
The npm entrypoint, verified release installer, and direct engine updater acquire
only an exact-SemVer release bundle or platform binary with its mandatory
exact-filename SHA-256 sidecar.

Verify a downloaded engine binary from a GitHub Release:

```bash
curl -LO "https://github.com/ViktorsBaikers/DevRites/releases/download/v<version>/devrites-<os>-<arch>[.exe]"
curl -LO "https://github.com/ViktorsBaikers/DevRites/releases/download/v<version>/devrites-<os>-<arch>[.exe].sha256"
( cd "$(dirname "$0")" && shasum -a 256 -c devrites-<os>-<arch>[.exe].sha256 )
```

Every release artifact also carries a GitHub artifact attestation (build
provenance generated inside the release job, bound to the artifact digest).
Verify it, pinning the signer workflow so an attestation minted from any other
workflow path in this repository is rejected — provenance proves where a build
ran, not that the publishing step was the intended one:

```bash
gh attestation verify devrites-<os>-<arch>[.exe] -R ViktorsBaikers/DevRites \
  --signer-workflow ViktorsBaikers/DevRites/.github/workflows/ci.yml@refs/heads/main
```

Releases with binaries also attach CycloneDX SBOMs: `devrites-npm.cdx.json`
for the npm dependency tree (`npm sbom`) and
`devrites-engine-linux-amd64.cdx.json` for the shipped engine binary
(Anchore Syft). The engine itself is stdlib-only, so the binary SBOM is
nominally just the module plus the Go toolchain — it exists so a consumer's
vulnerability or license tooling has a machine-readable input.

Treat attestations as independent of npm provenance, not as a safety signal:
the 2026 TanStack and `@redhat-cloud-services` incidents published malicious
packages with valid SLSA provenance. Verify the artifact digest; provenance
only proves where a build ran.

npm packages published from `main` set `publishConfig.provenance: true` in
`package.json`. The release job keeps `id-token: write` for OIDC and wires
`NPM_TOKEN: ${{ secrets.NPM_TOKEN }}` as the current publish credential until
an npm Trusted Publisher is configured for this package/workflow. Provenance
attests build origin, not package safety — still pin versions and audit the
dependency tree. Do not commit token values; only the Actions secret name.

Every redirect hop remains HTTPS; downloads use private temporary directories
and fixed in-stream byte ceilings. The bootstrap first streams archive metadata,
then paths, aborting the producer on a type, count, expanded-size, containment,
or path breach before extraction (at most 10,000 members, 4,096-byte paths, and
256 MiB expanded files). Metadata is capped at 1 MiB, sidecars at 4 KiB,
archives/binaries at 64 MiB, and the Node adapter follows at most five redirects.
There is no raw, source-archive, tag, or default-branch acquisition fallback;
exact-release guarantees begin at the checksummed release `install.sh` asset.
The Go release boundary may acquire the latest stable candidate; the downloaded
engine then supplies local paths to the manifest-owned update core. Engine
`update --check` resolves release metadata but downloads no assets. `--to` and
`--pre` release selection is not accepted.

`bash scripts/validate.sh` is the single repository-validation authority. It
performs strict recursive pack JSON parsing and render-to-temporary parity for
tracked generated host artifacts along with the other canonical checks. Release-only
host instruction files are regenerated and covered by generator/package tests. CI and
`scripts/release-check.sh` call that validator; they do not maintain duplicate
JSON or parity implementations, and the installed engine does not validate the
source tree.

Run `npm run release:dry` to see the proposed version and notes without
publishing. Maintainers can also run `bash scripts/release-check.sh` to build an
evidence packet for generated artifacts, checksums, package smoke tests,
install/update/uninstall, reproducible release archives, pack validation, the
behavioral eval schema, and npm distribution. This manual preflight is not a
hidden semantic-release step.
DevRites does not ship through Claude or Codex plugin stores. The release job
waits for CI, so a broken `main` does not ship.

Generated version sections in `CHANGELOG.md` are the release-note authority.
Contributors **MUST NOT** maintain a parallel manual `Unreleased` section;
semantic-release derives the next section from the accepted commits on `main`.

`scripts/check-npm-audit.mjs` re-audits the live npm graph. Temporary entries in
`scripts/npm-audit-exceptions.json` must remain exact-range, exact-node,
owner-bound, justified, sourced, and near-term expiring; stale, broadened,
unmatched, expired, or inside-the-7-day-refresh-horizon entries fail
validation. `osv-scanner.toml` `ignoreUntil` dates must match those expiries.
Do not extend an expiry silently: refresh the ancestor (today, pin
`@semantic-release/npm`'s bundled `npm` to `11.19.1` via `overrides`) or
remove the exception.

## Authoring commits that trigger releases

| Commit prefix | Bump |
| --- | --- |
| `feat:` | **minor** (`0.1.0` → `0.2.0`) |
| `remove:` | **minor**; grouped under Removed in release notes |
| `fix:` / `perf:` / `refactor:` / `build:` / `docs(README):` | **patch** (`0.1.0` → `0.1.1`) |
| Any type with `BREAKING CHANGE:` footer or `!` after type (e.g. `feat!:`) | **major** (`0.1.0` → `1.0.0`) |
| `chore:` / `ci:` / `test:` / `docs:` (non-README) | no release |
| Any scope `(no-release)` (e.g. `feat(no-release): …`) | no release |

Husky + commitlint reject non-conventional messages at commit time, so you can't accidentally bypass the rules.

## Dependency updates

Dependabot watches the `npm` and `github-actions` ecosystems and opens grouped
PRs each week for routine patch and minor updates (see
[`.github/dependabot.yml`](../.github/dependabot.yml)). The
[`dependabot-auto-merge.yml`](../.github/workflows/dependabot-auto-merge.yml)
workflow approves those PRs and enables squash auto-merge after the required
checks pass. Major updates stay open with a `needs-review` label so a human can
read the changelog before merging.

> GitHub does not allow Dependabot to push directly to `main`: a PR is always opened. Auto-merge is the closest equivalent. Required CI checks still gate the merge.
