Skip to content

ci: a change that reaches a user has a version - #1213

Open
swapnilpaliwal-sd wants to merge 1 commit into
ci/github-actionsfrom
ci/version-gate
Open

swapnilpaliwal-sd wants to merge 1 commit into
ci/github-actionsfrom
ci/version-gate

Conversation

@swapnilpaliwal-sd

Copy link
Copy Markdown
Contributor

Stacked on #418 rather than pushed into it, so it can be looked at on its own. Merges into
ci/github-actions and goes out with the rest of the OSS setup; cherry-pick it across instead if
you'd rather keep #418 a single commit.

What it gates

npm will not republish a version, so anything inside the tarball can only reach anyone through a
new one — and README.md is inside it, named by the files allowlist. A PR that improves the
README and leaves the version alone never delivers the improvement, and nothing says so.

The inverse error costs as much: a bump demanded for a CI tweak or a test fixture teaches people
to bump without asking why, and a version that moves for reasons users cannot observe stops
carrying information.

So the gate asks one question — could this change reach a user? — and answers it from
package.json's own files declaration rather than a second list kept by hand, because a second
list drifts. The negated entries (!graph/test/, !plugins/axiomcode/validate/,
!plugins/**/__pycache__/) are read out of the file; .github/ is added, since npm never packs
it. Everything else is assumed to reach a user, so a new top-level directory is gated by
default rather than exempt by default.

Not the engine packages

@axiomcode/engine-<os>-<cpu> is named by ENGINE_ID — a sha256 over the Soufflé version and
the rule text alone (graph/pipeline/run-souffle.sh). A README cannot move it, and a rule change
moves it whether or not anyone bumps anything. That gate is already correct and this script does
not second-guess it. Two artefacts, two different notions of "changed", which is the whole point.

Behaviour

Pull requests only, where there is a base to compare against. The hygiene job's checkout takes
fetch-depth: 0 for the merge base. VERSION_GATE=off skips it — which is what a branch that is
not publishing yet wants, and worth knowing since @axiomcode/code-graph is not on the registry
today.

Verified against a fixed base:

change version result
README.md alone unchanged fails
graph/test/ alone unchanged passes
.github/ alone unchanged passes
README.md patch bump passes
graph/pipeline/engine.conf unchanged fails

Uses a while read loop rather than mapfile, since bash 3.2 is still what a macOS laptop runs
and the repo already carries portable-stat.sh for the same reason.

One thing to decide

The root package is at 0.1.0 and unpublished. Until the first publish the gate is advisory —
nothing is unreachable yet, because nothing is reachable. If you'd rather it stay quiet until
then, set VERSION_GATE=off in the workflow env and drop it when the first publish lands.

🤖 Generated with Claude Code

npm will not republish a version. Anything inside the tarball can therefore only
reach anyone through a new one — and README.md is inside it, named by the `files`
allowlist. A pull request that improves the README and leaves the version alone
is not a small omission: the improvement is never delivered, and nothing says so.
The next person reads the old text on the registry and cannot tell it apart from
a README nobody has written.

The inverse error costs as much. A bump demanded for a CI tweak or a test fixture
teaches people to bump without asking why, and a version that moves for reasons
users cannot observe stops carrying information.

So the gate asks one question — could this change reach a user? — and answers it
from package.json's own `files` declaration rather than a second list kept by
hand, because a second list drifts. The negated entries (`!graph/test/`,
`!plugins/axiomcode/validate/`, `!plugins/**/__pycache__/`) are read out of the
file; `.github/` is added, since npm never packs it and no `files` entry would
name it. Everything else is assumed to reach a user, so a new top-level directory
is gated by default rather than exempt by default.

The engine packages are deliberately untouched. `@axiomcode/engine-<os>-<cpu>` is
named by ENGINE_ID, a sha256 over the Soufflé version and the rule text alone, so
a README cannot move it and a rule change moves it whether or not anyone bumps
anything. That gate is already correct.

Runs on pull requests only, where there is a base to compare against; the job's
checkout takes fetch-depth 0 for the merge base. `VERSION_GATE=off` skips it,
which is what a branch that is not publishing yet wants.

Verified on five cases against a fixed base: README alone without a bump fails;
`graph/test/` alone, `.github/` alone, and README with a patch bump pass; a change
to graph/pipeline/engine.conf without a bump fails.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
swapnilpaliwal-sd added a commit that referenced this pull request Sep 25, 2026
…m on publish

- version.mjs sets and checks the version in all ten places that carry it
  (root, its engine pins, parser, gemini and the three plugin manifests).
- The version gate (from #1213) now also requires every manifest to agree, a
  bump to move forward, and the new version not to be tagged already. It only
  demands a bump once the base version is tagged, so work before the first
  release joins 0.1.0.
- release.yml: a push to main whose version has no tag gets one, plus a draft
  release with generated notes. Nothing is published.
- publish-npm.yml runs when that draft is published: checks tag == manifests,
  builds the engines, publishes them and then @axiomcode/code-graph (which was
  never published before), prereleases under `next`, skips versions already on
  the registry, and attaches the tarballs to the release.
- .github/RELEASING.md is the runbook.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
swapnilpaliwal-sd added a commit that referenced this pull request Sep 25, 2026
…m on publish

- version.mjs sets and checks the version in all ten places that carry it
  (root, its engine pins, parser, gemini and the three plugin manifests).
- The version gate (from #1213) now also requires every manifest to agree, a
  bump to move forward, and the new version not to be tagged already. It only
  demands a bump once the base version is tagged, so work before the first
  release joins 0.1.0.
- release.yml: a push to main whose version has no tag gets one, plus a draft
  release with generated notes. Nothing is published.
- publish-npm.yml runs when that draft is published: checks tag == manifests,
  builds the engines, publishes them and then @axiomcode/code-graph (which was
  never published before), prereleases under `next`, skips versions already on
  the registry, and attaches the tarballs to the release.
- .github/RELEASING.md is the runbook.
swapnilpaliwal-sd added a commit that referenced this pull request Sep 25, 2026
…m on publish

- version.mjs sets and checks the version in all ten places that carry it
  (root, its engine pins, parser, gemini and the three plugin manifests).
- The version gate (from #1213) now also requires every manifest to agree, a
  bump to move forward, and the new version not to be tagged already. It only
  demands a bump once the base version is tagged, so work before the first
  release joins 0.1.0.
- release.yml: a push to main whose version has no tag gets one, plus a draft
  release with generated notes. Nothing is published.
- publish-npm.yml runs when that draft is published: checks tag == manifests,
  builds the engines, publishes them and then @axiomcode/code-graph (which was
  never published before), prereleases under `next`, skips versions already on
  the registry, and attaches the tarballs to the release.
- .github/RELEASING.md is the runbook.
swapnilpaliwal-sd added a commit that referenced this pull request Sep 25, 2026
…t release per bump; npm on publish (#1318)

* ci: gate main on build, repo invariants and all five language suites

Carries #418 onto current main as files rather than a rebase: its branch merged
engine-prebuilt, whose other changes main already has, so only .github/,
graph/test/tools/lib-coverage.sh and the java suite's --no-torture flag were new.

Beyond #418:
- C# joins the gate: a suite leg scored against the Roslyn oracle (.NET 8), its
  staging map and its decls-vs-parser-schema check.
- C# joins build-engines, so the engine packages carry all five languages.
- protect-main.sh points at the renamed repository and also makes v* tags
  immutable (creatable, never moved or deleted).

* release: one version everywhere, a tag and draft release per bump, npm on publish

- version.mjs sets and checks the version in all ten places that carry it
  (root, its engine pins, parser, gemini and the three plugin manifests).
- The version gate (from #1213) now also requires every manifest to agree, a
  bump to move forward, and the new version not to be tagged already. It only
  demands a bump once the base version is tagged, so work before the first
  release joins 0.1.0.
- release.yml: a push to main whose version has no tag gets one, plus a draft
  release with generated notes. Nothing is published.
- publish-npm.yml runs when that draft is published: checks tag == manifests,
  builds the engines, publishes them and then @axiomcode/code-graph (which was
  never published before), prereleases under `next`, skips versions already on
  the registry, and attaches the tarballs to the release.
- .github/RELEASING.md is the runbook.

* test/java: the oracle-agreement golden counts case 60

Case 60 (#1269) added a case both ground-truth oracles compare, without
re-recording the agreement golden, so `run-tests.sh --oracle` on main aborted
before running any case: 45 compared, 45 agreeing, against a golden of 44/44.
No disagreement changed.

* release: publish with the CLI_BINARY_PUBLISH secret, the npm token already set on the repo

* build: darwin-arm64 on the hosted macos-15 runner, every run

A public repository gets GitHub's standard runners free, Apple Silicon macOS
included, so the self-hosted runner and the macos switch that skipped it are
gone: CI and every publish build all four platforms.

* ci: no approval required; only the admin role merges into main

GitHub never lets an author approve their own pull request, so a required
approval would block the sole maintainer. The PR and CI rules still bind
everyone; a separate ruleset restricts updates to main to the admin role,
through a pull request only.

* ci: run on pushes to dev, which takes direct pushes

* release: merge main back into dev after every push to main

main squash-merges, so a promotion lands as a commit dev lacks and the next
dev->main PR would repeat it. A clean merge is pushed to dev; a conflict (a
hotfix that touched lines dev changed) pushes nothing and opens one issue with
the commands to resolve it by hand.

* Linux: an apostrophe in a # comment, and a sandbox PATH that kept /bin

Two faults only a Linux runner shows, found on the first CI run:

- Soufflé on Linux preprocesses with mcpp, which tokenises the text of a
  trailing # comment; "every on's listener" is an unterminated character
  constant there, so every TypeScript solve and the engine generate step
  failed. macOS's clang preprocessor accepts it. Reworded; all five
  languages' programs now pass mcpp.
- engine-id-test.sh hid souffle by dropping its directory from PATH, compared
  logically. On merged-/usr Ubuntu /bin -> /usr/bin survives that, and dropping
  /usr/bin itself would take bash with it. Directories are now compared with
  pwd -P and souffle's is replaced by a shadow holding everything else.

* test/tools: one souffle-hiding helper, correct on merged-/usr Linux

engine-package-test.sh carried the same PATH sandbox as engine-id-test.sh
and failed the same way on Ubuntu: /bin -> /usr/bin kept souffle visible, so
"no souffle" runs found it. Both now source hide-souffle.sh, which compares
directories with pwd -P and shadows souffle's directory instead of dropping
it. Proven on a simulated merged-/usr layout: the old block leaves souffle
visible, the helper hides it and keeps sh.

* ci: a docs-only change runs build and hygiene only; platform engines only when what they compile changed

The workflow still starts on every event, so the required CI check always
reports; a changes job classifies the diff and the engine suites and the
platform build skip themselves. The CI job accepts a skip only where changes
asked for one. Markdown under graph/ and parser/ still counts as code, since
graph/bundle/SCHEMA.md is generated and checked. Pushes to main, merge queues
and manual runs run everything.

* ci: dev is the default branch, a nightly from scratch, engines cached by ENGINE_ID

- dev takes every pull request: build, hygiene and the five suites. The
  four-platform engine build runs on the way into main (a promotion PR or a
  push to main) and in the nightly, not on every change to dev.
- protect-main.sh names refs/heads/main instead of ~DEFAULT_BRANCH, so main's
  protection stays on main now that dev is the default, and dev gets a
  ruleset that only forbids deleting it.
- nightly.yml: on nights dev changed, CI with fresh=true (no restored
  engines, every platform), then e2e-install.sh packs the tarballs, installs
  them into an empty project without Souffle and runs axiomcode in four
  languages, then npm publish --dry-run for every package. Publishes
  nothing; a failure opens an issue and the next green night closes it.
- The suite cache never hit: the driver writes to ~/.cache/axiomcode/souffle
  while CI saved .souffle-cache. The suites now point the driver there and
  key it by the language's ENGINE_ID.
- build-engines restores the previous engines per platform and recompiles
  only languages whose ENGINE_ID changed (about 3 min each at -O3); fresh
  (nightly, publish) restores nothing. Its Souffle download is now checked
  against the same SHA-512 as ci.yml.

* ci: cache keys cover the compile flags and, for -march=native, the CPU

ENGINE_ID hashes the rules and the Souffle version, not how the binary is
compiled, so a flag change reused binaries built with the old flags.
- build-engines: the hash of build-engines.yml (which holds the flags)
  prefixes both the key and the restore prefix, so a flag change restores
  nothing. Computed in generate, since the build jobs never check out.
- suites: the key adds the hash of run-souffle.sh (the driver's flags) and
  the runner's CPU model: the driver compiles with -march=native, and a
  binary built on one CPU can die with an illegal instruction on another.

* nightly: dev's daily status, published under the nightly dist-tag when green

- Runs every night, commits or not: the status is daily, and with no lock
  file a dependency release can break a fresh install with nothing committed.
- A green night publishes every package as <next>-nightly.<date>.g<sha>
  under `nightly`, never `latest`. The version is computed in the run and
  never committed or tagged; the g keeps an all-digit sha a valid semver
  identifier. Skipped when that commit is already the nightly, and until a
  first release exists, since npm makes a first publish `latest`.
- README: the static "engines: not yet published" and "nightly: not yet
  enabled" badges become the live npm version and nightly status.

* test/csharp: one compiled engine per run, not one per case; admins can merge

- devrun.sh caches its compiled engine beside the work dir it is given, and
  run-tests.sh hands every case a fresh one (rm -rf "$w"), so each of the 20
  cases and the cross-service cases recompiled the same engine, about 70s
  each: the suite took 32-38 min in CI. run-tests.sh now exports one
  AXIOM_CS_DEV_CACHE for the run (in CI, inside the saved engine cache). The
  cache is content-addressed by the rule text, so sharing it is safe.
  Locally: three cases, one compile and two reuses, 95s in total.
- main-merge-admins bypass is `always`: in `pull_request` mode GitHub refused
  the merge itself ("Cannot update this protected ref"), even for admins.
  protect-main still has no bypass, so PR and CI bind admins too.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant