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
176 changes: 176 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -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"
36 changes: 36 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<package> 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
Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
4 changes: 2 additions & 2 deletions packages/quality-tools/package-size.json
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
2 changes: 1 addition & 1 deletion packages/quality-tools/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down