Skip to content

ci + release: gate main on all five suites; one version, tag and draft release per bump; npm on publish - #1318

Merged
swapnilpaliwal-sd merged 15 commits into
mainfrom
ci/oss-release
Sep 25, 2026
Merged

swapnilpaliwal-sd merged 15 commits into
mainfrom
ci/oss-release

Conversation

@swapnilpaliwal-sd

@swapnilpaliwal-sd swapnilpaliwal-sd commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Replaces #418 and #1213 with one PR built on current main. Sets up CI, releases, the dev/main branch model and a nightly.

Branches

  • dev is the default branch. Every PR lands there, and CI runs build, repo checks and the five language suites. A docs-only PR runs only build and repo checks, about a minute. The only rule on dev is that it can't be deleted.
  • main moves only by promotion (a PR dev → main). That PR also builds the engines for all four platforms. main requires a green CI, and only an admin can merge. Releases are tags on main.
  • sync-dev merges main back into dev after every push to main. If it conflicts, it pushes nothing and opens an issue with the commands to resolve it by hand.

CI (ci.yml)

  • Always starts, so the required CI check always reports. A changes job classifies the diff, and the expensive jobs skip themselves when it's safe.
  • Suites: java, typescript, python, javascript and csharp. C# is new here, scored against the Roslyn oracle.
  • Engine caches are keyed by the language's ENGINE_ID (a hash of its rules), the compile-flags file and, for suites, the runner's CPU. Unchanged rules reuse the compiled engine; the platform build recompiles only the languages whose rules changed.

Releases

  • version.mjs set X.Y.Z writes all ten places that carry the version.
  • The version gate makes a PR that reaches users bump the version: forward only, never to an already-tagged version, with every manifest agreeing.
  • release.yml: a version bump landing on main gets a tag and a draft release.
  • publish-npm.yml: publishing the draft builds fresh engines for all four platforms and publishes the engine packages, then @axiomcode/code-graph, then attaches the tarballs.
  • Rulesets: protect-main.sh applies them. Release tags are immutable.

Nightly (nightly.yml), dev's daily status

Every night:

  • build dev from scratch: the five suites and all four platforms;
  • install the tarballs into an empty project without Soufflé and run axiomcode in four languages;
  • dry-run the npm publish.

If green, it publishes to npm under the nightly dist-tag as <next>-nightly.<date>.g<sha>, never as latest. No nightly is published until v0.1.0 exists. If red, it opens an issue; the next green night closes it. The README badge shows the status.

Fixes that only a Linux runner showed

  • An apostrophe in a # comment in value-flow.dl broke mcpp, Soufflé's preprocessor on Linux. Every TypeScript solve failed, and so did the engine generate step.
  • The Soufflé-hiding PATH sandbox in engine-id-test.sh and engine-package-test.sh kept /bin → /usr/bin on Ubuntu. Both now use graph/test/tools/hide-souffle.sh.
  • The Java oracle-agreement golden was stale after java: a class header does not see the class's own member types (#1244) #1269.

Verified

  • CI is green on all five suites and all four platform engine builds.
  • e2e-install.sh passed locally for Java, Python and JavaScript, and it fails on an engine package built from different rules.
  • The version gate is correct in 8 scenarios, and the diff classification in 18.

On merge

  • v0.1.0 is tagged and a draft release opens.
  • sync-dev brings these workflows to dev.

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).
…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.
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.
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.
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.
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.
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.
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.
…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.
… 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.
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.
…n 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.
@swapnilpaliwal-sd
swapnilpaliwal-sd marked this pull request as ready for review September 25, 2026 04:43
@swapnilpaliwal-sd
swapnilpaliwal-sd force-pushed the ci/oss-release branch 2 times, most recently from b63957d to d793c68 Compare September 25, 2026 04:47
@swapnilpaliwal-sd
swapnilpaliwal-sd enabled auto-merge (squash) September 25, 2026 05:01
…n 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.
@swapnilpaliwal-sd
swapnilpaliwal-sd merged commit a96a7f3 into main Sep 25, 2026
14 checks passed
@swapnilpaliwal-sd
swapnilpaliwal-sd deleted the ci/oss-release branch September 25, 2026 05:36
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