diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..64e5b57 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,176 @@ +name: Publish packages + +# Manual release. npm authenticates this workflow by OIDC (trusted publishing), +# so there is no NPM_TOKEN anywhere in this repository and no maintainer 2FA +# prompt in the loop. Each package must have a trusted publisher configured on +# npmjs.com pointing at `ShiplightAI/quality` and this workflow file name. +# `AGENTS.md` ("Publishing") carries the procedure and the version rule. +# +# Unlike shiplight-cli's publish workflows, this one does NOT compute or push a +# version bump. Versions are decided in a reviewed `chore: release` commit on +# main, because `packages/quality-tools/package-size.json` records a +# version-specific `approvedIncrease` that only a human maintainer may write. A +# workflow that bumped the version by itself would invalidate that approval on +# every run. +on: + workflow_dispatch: + inputs: + dry_run: + description: "Run every gate and `pnpm publish --dry-run`, but publish nothing" + type: boolean + default: false + +# One release at a time. The already-on-npm skip below is a check, not a lock: +# two simultaneous dispatches would both read "not published yet" and then race, +# and the loser dies mid-release with a 403 having published some packages but +# not others. No cancel-in-progress — interrupting a publish is worse than +# making the second dispatch wait. +concurrency: + group: publish-npm + +permissions: + contents: read + +jobs: + publish: + runs-on: ubuntu-latest + permissions: + contents: read + # REQUIRED. npm authenticates this workflow by OIDC and there is no + # NPM_TOKEN to fall back on, so without an id-token every publish step + # fails with a bare ENEEDAUTH at the very end of the run. + id-token: write + # Every action below is SHA-pinned, unlike ci.yml: a tag can be force-moved + # at the upstream org, and this is the only job in the repository whose + # credential can push code to every consumer. Bump them deliberately. + steps: + # Releases come from main only. The version being published is whatever + # the reviewed release commit put in package.json. + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: main + + - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 + + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 24 + cache: pnpm + # Writes the registry into .npmrc. No NODE_AUTH_TOKEN: pnpm mints a + # short-lived npm credential from the job's OIDC token instead. + registry-url: https://registry.npmjs.org + + # Catches the commonest misconfiguration — a job without `id-token: write` + # — before the gates below spend their time, instead of letting it surface + # as an unexplained auth failure after the whole suite has run. + - name: Assert the OIDC id-token is available to this job + id: preflight + run: | + set -euo pipefail + if [ -z "${ACTIONS_ID_TOKEN_REQUEST_URL:-}" ]; then + printf '::%s::%s\n' error "No OIDC id-token in this job — trusted publishing needs 'permissions: id-token: write'" + exit 1 + fi + sha=$(git rev-parse HEAD) + echo "OIDC id-token endpoint present. Releasing $sha." + echo "sha=$sha" >> "$GITHUB_OUTPUT" + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + # The same three gates CI runs on every pull request, repeated here + # because a release publishes main as it stands now, not as it stood when + # the last pull request was green. + - name: Build + run: pnpm build + + - name: Typecheck + run: pnpm typecheck + + - name: Test + run: pnpm test + + - name: Check quality-tools package size + run: pnpm --filter @shiplightai/quality-tools check:size + + - name: Check quality-ui package size + run: pnpm --filter @shiplightai/quality-ui check:size + + # Dependency order: quality-map, then quality-core, then the two packages + # that consume it. pnpm rewrites the `workspace:` ranges to real versions + # while packing, so publishing a consumer before its dependency would put + # a tarball on npm that nobody can install until the next step finishes. + # + # A package whose repo version is already on npm is skipped, not failed: + # most releases move some of these four and leave the rest alone, and a + # re-run after a mid-release failure has to be able to finish the job. + - name: Publish + id: publish + env: + DRY_RUN: ${{ inputs.dry_run }} + run: | + set -euo pipefail + + published=() + skipped=() + + for dir in packages/quality-map packages/core packages/quality-tools packages/ui; do + name=$(node -p "require('./$dir/package.json').name") + version=$(node -p "require('./$dir/package.json').version") + + if npm view "$name@$version" version > /dev/null 2>&1; then + echo "Skipping $name@$version — already on npm." + skipped+=("$name@$version") + continue + fi + + # --no-git-checks: actions/checkout leaves a detached HEAD, which + # pnpm's publish-branch check rejects. The branch is pinned by the + # `ref: main` checkout above instead. + # + # No --provenance. pnpm's OIDC path already attaches a provenance + # attestation on its own: it only consults the flag when one was + # passed, and otherwise asks npm whether the package is public and + # signs when it is (both are, and this repository is public). The + # flag would only change the failure mode — without it pnpm warns + # and publishes when visibility cannot be read, with it the publish + # dies. + if [ "$DRY_RUN" = "true" ]; then + echo "Dry run: $name@$version" + (cd "$dir" && pnpm publish --dry-run --no-git-checks --access public) + else + echo "Publishing $name@$version" + (cd "$dir" && pnpm publish --no-git-checks --access public) + fi + published+=("$name@$version") + done + + if [ ${#published[@]} -eq 0 ]; then + if [ "$DRY_RUN" = "true" ]; then + printf '::%s::%s\n' notice "Every package version on main is already on npm. The gates and the npm credential were still exercised." + else + printf '::%s::%s\n' error "Every package version on main is already on npm — nothing to publish. Land a release commit first." + exit 1 + fi + fi + + { + echo "published=${published[*]}" + echo "skipped=${skipped[*]-}" + } >> "$GITHUB_OUTPUT" + + - name: Step summary + if: always() && steps.preflight.outcome == 'success' + env: + RELEASE_SHA: ${{ steps.preflight.outputs.sha }} + DRY_RUN: ${{ inputs.dry_run }} + PUBLISHED: ${{ steps.publish.outputs.published }} + SKIPPED: ${{ steps.publish.outputs.skipped }} + run: | + { + echo "## Release from \`$RELEASE_SHA\`" + echo "" + echo "- Dry run: \`$DRY_RUN\`" + echo "- Published: \`${PUBLISHED:-(none — the run ended before publishing)}\`" + echo "- Already on npm, skipped: \`${SKIPPED:-(none)}\`" + } >> "$GITHUB_STEP_SUMMARY" diff --git a/AGENTS.md b/AGENTS.md index f7f5111..c4421b7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,6 +23,42 @@ Quality evaluates evidence independently of the systems that produce it. Do not introduce dependencies from the engine into evidence producers or from open-source packages into the Shiplight platform monorepo. +## Version bumps + +Bump the **patch** version by default: if `0.3.0` is published, the next release +is `0.3.1`. This holds even when the release adds API. Use a minor or major only +when the maintainer says so for that release. + +Count from the **published** version, never from what `package.json` currently +says. Between releases the manifest sits on the last published number with +unreleased work on top, so it is not the base — and a number that was set in the +repo but never published (`quality-core` `0.2.0`) is a dead end that nothing +counts from. Read the base with `npm view @shiplightai/ version`. + +A bump moves the size approval with it: `approvedIncrease.version` in +`packages/quality-tools/package-size.json` must equal the new `package.json` +version, or the gate rejects the recorded approval and the build fails. + +## Publishing + +Releases go out through the **Publish packages** workflow +(`.github/workflows/publish.yml`), run manually from the Actions tab against +`main`. npm authenticates it by OIDC (trusted publishing), so this repository +holds no npm token and a maintainer's 2FA never enters the loop. Each package +carries a trusted publisher on npmjs.com naming `ShiplightAI/quality` and that +workflow file; renaming the file breaks publishing until the npm side is +updated to match. + +The workflow does not decide versions. It publishes exactly what `package.json` +says on `main` and skips any package already at that version on npm, so the +release is whatever the reviewed `chore: release` commit landed. This is +deliberate: a workflow that bumped versions itself would move +`approvedIncrease.version` away from the number a human approved. + +Publishing by hand is the fallback, not the path. It needs a long-lived npm +token or an interactive 2FA prompt, and it publishes a working tree rather than +a reviewed commit. + ## Release size gate The `quality-tools` release artifact may grow by at most 1% in both packed and diff --git a/packages/core/package.json b/packages/core/package.json index e7910fc..3b56e00 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@shiplightai/quality-core", - "version": "0.3.0", + "version": "0.3.1", "type": "module", "description": "Deterministic analysis engine for evidence-backed software quality maps.", "license": "MIT", diff --git a/packages/quality-tools/package-size.json b/packages/quality-tools/package-size.json index 06cc600..65c745f 100644 --- a/packages/quality-tools/package-size.json +++ b/packages/quality-tools/package-size.json @@ -1,8 +1,8 @@ { "maxIncreasePercent": 1, "approvedIncrease": { - "version": "0.3.2", - "packedBytes": 46290, + "version": "0.3.3", + "packedBytes": 46291, "unpackedBytes": 168548, "approvedBy": "Feng Qian", "reason": "new features" diff --git a/packages/quality-tools/package.json b/packages/quality-tools/package.json index ea1b283..d98e72a 100644 --- a/packages/quality-tools/package.json +++ b/packages/quality-tools/package.json @@ -1,6 +1,6 @@ { "name": "@shiplightai/quality-tools", - "version": "0.3.2", + "version": "0.3.3", "type": "module", "description": "Quality graph analysis and canonical workflow-observation tools.", "license": "MIT", diff --git a/packages/ui/package.json b/packages/ui/package.json index 735b3b3..931ddd4 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@shiplightai/quality-ui", - "version": "0.1.0", + "version": "0.1.1", "description": "Shared React/Mantine presentation for Quality Explorer and Shiplight Quality Center.", "license": "MIT", "type": "module",