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
34 changes: 34 additions & 0 deletions .github/scripts/next-version.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Compute the next release version (DEVOPS.md — npm releases, standing
// procedure). Usage:
//
// node .github/scripts/next-version.mjs <baseline> [latest-published]
//
// Prints the version to publish:
//
// - the baseline (package.json's version) when nothing is published yet or
// the baseline is greater than the latest published version — this is how
// a deliberate minor/major bump landed on main takes effect;
// - otherwise the latest published version with its patch component
// incremented.
//
// Either way the result is strictly greater than every previously published
// version. Only plain x.y.z versions are accepted; anything else (prerelease
// tags, garbage from a failed registry lookup) fails loudly rather than
// publishing a surprise.

const [baseline, latest] = process.argv.slice(2);

const parse = (v) => {
const m = /^(\d+)\.(\d+)\.(\d+)$/.exec(v ?? "");
if (!m) throw new Error(`next-version: not a plain x.y.z version: ${JSON.stringify(v)}`);
return m.slice(1).map(Number);
};

const b = parse(baseline);
if (!latest) {
console.log(baseline);
} else {
const l = parse(latest);
const gt = b[0] !== l[0] ? b[0] > l[0] : b[1] !== l[1] ? b[1] > l[1] : b[2] > l[2];
console.log(gt ? baseline : `${l[0]}.${l[1]}.${l[2] + 1}`);
}
85 changes: 85 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Automated npm releases (DEVOPS.md — npm releases, standing procedure).
#
# Every push to main whose CI run is green is published to the public npm
# registry as @modularcloud/xspec, with a strictly increasing version, and the
# released commit is tagged vX.Y.Z. No human is involved per release.
#
# Trigger completion of the CI workflow for a push to main. Publishing
# hangs off CI's own result, so the never-release-with-red-CI
# rule is enforced mechanically, and the job checks out exactly
# the commit CI validated (workflow_run.head_sha).
# Version next-version.mjs: package.json's version when it is greater
# than the latest published version (deliberate bumps land on
# main as ordinary package.json changes), else latest published
# with the patch component incremented. The registry and the
# vX.Y.Z tags are the record of released versions; the bump is
# not committed back to main.
# Credential the NPM_TOKEN repository secret (granular npm automation
# token). Missing secret fails loudly with instructions — a
# release is never silently skipped. Re-running a failed run is
# safe: the registry refuses to republish an existing version.
name: Release

on:
workflow_run:
workflows: [CI]
types: [completed]
branches: [main]

permissions:
contents: write
id-token: write

# Queue releases rather than cancelling one mid-publish.
concurrency:
group: npm-release
cancel-in-progress: false

jobs:
publish:
name: Publish @modularcloud/xspec to npm
runs-on: ubuntu-24.04
timeout-minutes: 15
if: >-
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'push' &&
github.event.workflow_run.head_branch == 'main'
steps:
- name: Require the NPM_TOKEN repository secret
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
if [ -z "$NPM_TOKEN" ]; then
echo "::error::The NPM_TOKEN repository secret is not set, so this green main commit was NOT published. Complete the one-time setup in specs/DEVOPS.md (npm releases — credentials), then re-run this workflow run to publish this commit."
exit 1
fi
- uses: actions/checkout@v4
with:
ref: ${{ github.event.workflow_run.head_sha }}
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
registry-url: https://registry.npmjs.org
- run: npm ci
- name: Build the product
run: npm run build
- name: Compute the release version (strictly increasing)
id: version
run: |
NAME="$(node -p "require('./package.json').name")"
BASELINE="$(node -p "require('./package.json').version")"
LATEST="$(npm view "$NAME" version 2>/dev/null || true)"
NEXT="$(node .github/scripts/next-version.mjs "$BASELINE" "$LATEST")"
echo "Package $NAME — baseline $BASELINE, latest published ${LATEST:-<none>}, releasing $NEXT"
echo "next=$NEXT" >> "$GITHUB_OUTPUT"
- name: Publish to npm
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
npm version "${{ steps.version.outputs.next }}" --no-git-tag-version
npm publish --provenance --access public
- name: Tag the released commit
run: |
git tag "v${{ steps.version.outputs.next }}" "${{ github.event.workflow_run.head_sha }}"
git push origin "v${{ steps.version.outputs.next }}"
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 20 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,27 @@
{
"name": "xspec",
"version": "0.0.0",
"description": "xspec — the product program of this repository; the test harness under test/ is a separate program.",
"name": "@modularcloud/xspec",
"version": "0.1.0",
"description": "Requirement traceability for specifications written in MDX: typed spec modules, reference validation, dependency policy, coverage, impact analysis, and staged reviews.",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/modularcloud/sdg-claude.git"
"url": "git+https://github.com/modularcloud/xspec.git"
},
"homepage": "https://github.com/modularcloud/xspec#readme",
"bugs": {
"url": "https://github.com/modularcloud/xspec/issues"
},
"keywords": [
"specification",
"requirements",
"traceability",
"mdx",
"typescript",
"coverage",
"spec-driven"
],
"publishConfig": {
"access": "public"
},
"type": "module",
"engines": {
Expand Down
29 changes: 14 additions & 15 deletions specs/DEVOPS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,26 @@ This document defines how release, merge, and deploy actions are performed for t

- Never merge with red CI. A release candidate is a commit at which the full test suite and every CI job are green.
- Never rewrite pushed history. Ordinary commits only.
- External release artifacts — version tags, GitHub Releases, package publishes, deploys — always require explicit Developer authorization. Absent that authorization, the default is deferral: take no external action and record the release candidate instead.
- External release artifacts require explicit Developer authorization. Two standing authorizations exist, granted by Developer on 2026-07-28: publishing `@modularcloud/xspec` to the public npm registry from green `main` commits of this repository, and the `vX.Y.Z` git tags marking those publishes. Any other kind of external artifact (deploys, GitHub Releases, publishes elsewhere) still requires fresh authorization; absent it, the default is deferral take no external action and record the release candidate instead.

## Initial build on `modularcloud/sdg-claude` (this repository)
## This repository is the product's home

This repository is the SDG process's home, not the product's. The `xspec` product was built here on branch `sdg/initial-build` (PR #1), and Developer will personally relocate the work to a dedicated xspec repository after the initial build completes. A release artifact created here (tag, GitHub Release, package publish) would brand the wrong repository as the product's home and would outlive the branch it points at — so none are created.
`modularcloud/xspec` is the xspec product's dedicated repository, and `main` is its released line. (The product was originally built in `modularcloud/sdg-claude`, the SDG process's home, where release was deliberately deferred so no artifact would brand that repository as the product's home; Developer relocated the work here on 2026-07-27, superseding that deferral and its recorded release candidate `9316048`.)

Release procedure for the initial build — **deferred**, by explicit Developer decision:
The normal Release-phase flow for a completed patch is:

- Take no external release action from this repository.
- Do not merge PR #1. It intentionally carries a `specs/GOALS.md` merge conflict and stays open, untouched. Do not merge `main` into the branch.
- Do not create version tags, GitHub Releases, or package publishes.
- Leave branch `sdg/initial-build` and PR #1 exactly as they are.
- The deliverable is the green branch head, ready for Developer's switch-over to the dedicated xspec repository. Release candidate: commit `9316048` — full suite green locally (485/485), all three CI jobs green. Commits after it on the branch are process bookkeeping only (spec-document edits, no product code).

## Future runs in the product's own repository
1. Merge the patch's PR once CI is green on the PR head (never with red CI).
2. Nothing further. The standing npm release procedure below publishes the merged commit automatically; there is no per-release tagging, publishing, or Developer step.

Once xspec lives in its own repository, the normal release flow for a completed patch is:
## npm releases (standing procedure)

1. Merge the patch's PR once CI is green on the PR head (never with red CI).
2. With explicit Developer authorization, tag the release commit; a GitHub Release and/or package publish additionally requires Developer-provided credentials.
3. Absent authorization for any external artifact, defer that artifact and record the release candidate, per the standing rules.
- **Artifact.** The public npm package `@modularcloud/xspec`. `package.json` must always identify the package by that name, with this repository as `repository`/`homepage`/`bugs`, and carry `publishConfig.access: public`.
- **Trigger.** Every push to `main` (a PR merge is a push). The `Release` workflow (`.github/workflows/release.yml`) runs when the `CI` workflow completes for a `main` push.
- **Gate.** The publish job runs only when that CI run concluded successfully, and it checks out exactly the commit CI validated — the never-release-with-red-CI rule is enforced mechanically. Nothing beyond green CI gates a release.
- **Versioning.** Strictly increasing, computed by `.github/scripts/next-version.mjs` with no Developer involvement: if `package.json`'s version is greater than the latest published version (or nothing is published yet), that version is released — a deliberate minor/major bump is made by landing the `package.json` change on `main`; otherwise the latest published version's patch component is incremented. The registry and the `vX.Y.Z` tags are the record of released versions; the computed bump is not committed back to `main`, so the in-repo version is the floor, not the record.
- **Tags.** Each successful publish pushes the lightweight tag `vX.Y.Z` on the released commit, using the workflow's own repository token.
- **Credentials.** Publishing authenticates with the `NPM_TOKEN` GitHub repository secret — a granular npm automation token with read/write access to the package (or the `@modularcloud` scope), created under Developer's npm account. Credentials live only in that secret, never in the repository. If the token is missing or expired, recreate it on npmjs.com and set it again with `gh secret set NPM_TOKEN`.
- **Failure handling.** A missing/invalid secret or any publish error fails the Release run loudly — a release is never silently skipped. Re-running a failed Release run publishes the commit it was gated on and is always safe: the registry refuses to republish an existing version, so duplicate attempts fail rather than corrupt the sequence. A failed run never blocks later landings, which release independently.

## Post-update actions

Expand Down
Loading
Loading