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
4 changes: 3 additions & 1 deletion .github/workflows/build-engines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -185,10 +185,12 @@ jobs:
with:
path: engines
key: engines-${{ matrix.target.platform }}-${{ needs.generate.outputs.flags }}-${{ needs.generate.outputs.key }}
# publish-npm ships these binaries from the release's CI run, whenever the draft is published
- uses: actions/upload-artifact@v4
with:
name: engines-${{ matrix.target.platform }}
path: engines
retention-days: 90
if-no-files-found: error

build-macos:
Expand Down Expand Up @@ -248,4 +250,4 @@ jobs:
path: engines
key: engines-${{ matrix.target.platform }}-${{ needs.generate.outputs.flags }}-${{ needs.generate.outputs.key }}
- uses: actions/upload-artifact@v4
with: { name: 'engines-${{ matrix.target.platform }}', path: engines, if-no-files-found: error }
with: { name: 'engines-${{ matrix.target.platform }}', path: engines, retention-days: 90, if-no-files-found: error }
50 changes: 42 additions & 8 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,20 @@ jobs:
echo "code=true" >> "$GITHUB_OUTPUT"; echo "engines=false" >> "$GITHUB_OUTPUT"
echo "push to dev: suites run, platform engines wait for main"; exit 0
fi
# A push to main is a release when its version has no tag yet: that commit, and only that one,
# builds every platform and runs the five-platform e2e, and release.yml drafts from it once
# it is green. A push whose version is already tagged released nothing new and builds nothing.
if [ "${{ github.event_name }}" = push ] && [ "${{ github.ref }}" = refs/heads/main ]; then
v="$(node .github/scripts/version.mjs get)"
if git ls-remote --exit-code --tags origin "refs/tags/v$v" >/dev/null; then
echo "code=true" >> "$GITHUB_OUTPUT"; echo "engines=false" >> "$GITHUB_OUTPUT"
echo "push to main at v$v, already tagged: suites run, nothing to release"
else
echo "code=true" >> "$GITHUB_OUTPUT"; echo "engines=true" >> "$GITHUB_OUTPUT"
echo "push to main at v$v, not yet tagged: the release build runs on every platform"
fi
exit 0
fi
if [ "${{ github.event_name }}" != pull_request ]; then
echo "code=true" >> "$GITHUB_OUTPUT"; echo "engines=true" >> "$GITHUB_OUTPUT"
echo "${{ github.event_name }} on ${{ github.ref }}: everything runs"; exit 0
Expand All @@ -98,8 +112,9 @@ jobs:
-e '\.dl$' -e '^graph/pipeline/' -e '^packaging/' -e 'scripts/dl_program\.py$' -e '^\.github/scripts/query-smoke\.sh$' \
-e '^\.github/workflows/build-engines\.yml$' -e '^package\.json$' -e '^\.github/scripts/e2e-' -e '^\.github/e2e/' || true)"
[ -n "$code" ] && echo "code=true" >> "$GITHUB_OUTPUT" || echo "code=false" >> "$GITHUB_OUTPUT"
# Only a pull request INTO main builds the platform engines; into dev they wait.
[ "${{ github.base_ref }}" = main ] || engines=""
# No pull request builds the platform engines: a release builds them once, on the push that
# lands it on main, and publishes exactly that build (#1350).
engines=""
[ -n "$engines" ] && echo "engines=true" >> "$GITHUB_OUTPUT" || echo "engines=false" >> "$GITHUB_OUTPUT"
echo "suites: $([ -n "$code" ] && echo run || echo skip) platform engines: $([ -n "$engines" ] && echo run || echo skip)"

Expand Down Expand Up @@ -206,6 +221,25 @@ jobs:
if: github.event_name == 'pull_request'
run: bash .github/scripts/version-gate.sh "origin/${{ github.base_ref }}"

# A push to main builds and releases only when its version is new (#1350). The gate above already
# refuses a pull request that changes what users get without a new version; a CI or docs change
# may keep main's version and then builds nothing when it lands. What is left to refuse here is a
# new version that was already released: its tag exists, so the push would build nothing and the
# change would never ship.
- name: a pull request into main does not reuse a released version
if: github.event_name == 'pull_request' && github.base_ref == 'main'
run: |
set -euo pipefail
head="$(node .github/scripts/version.mjs get)"
base="$(git show origin/main:package.json | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>console.log(JSON.parse(s).version))')"
if [ "$head" = "$base" ]; then
echo "main stays at $base: nothing in this change is released, and landing it builds nothing"; exit 0
fi
if git ls-remote --exit-code --tags origin "refs/tags/v$head" >/dev/null; then
echo "::error::v$head is already released — pick the next version"; exit 1
fi
echo "main $base -> $head: landing this builds every platform and drafts v$head"

engine:
name: engine (${{ matrix.lang }})
needs: [changes]
Expand Down Expand Up @@ -385,10 +419,9 @@ jobs:
AXIOM_SOUFFLE_CACHE: ${{ github.workspace }}/.souffle-cache
run: bash .github/scripts/run-suite.sh ${{ matrix.lang }} ${{ matrix.oracle }}

# Every language's engine, every platform: the reusable build that publish-npm
# ships from, run here WITHOUT publishing. A rule that solves on Ubuntu but does
# not compile with MSVC, or that no longer generates for a language, fails the
# gate here rather than at release time.
# Every language's engine, every platform, built once per release: on the push that
# lands a new version on main (and in the nightly). publish-npm ships these very
# artifacts, so what the e2e below tested is what users install (#1350).
engines:
name: engines build on every platform
needs: [build, changes]
Expand All @@ -401,7 +434,7 @@ jobs:
# job only proves each binary compiles and starts; the suites run from the checkout. Neither
# installs the packages, so a missing `files` entry, an engine package the CLI does not find,
# a query program that needs Soufflé, or a verb that only breaks on Windows reached users.
# Runs wherever the platform engines are built: on the way into main, and in the nightly.
# Runs wherever the platform engines are built: on the push that lands a release on main, and in the nightly.
pack:
name: pack @axiomcode/code-graph
needs: [build, changes]
Expand All @@ -416,8 +449,9 @@ jobs:
- run: npm install --no-audit --no-fund
# --ignore-scripts: `prepare` already built it; the tarball carries what the build produced
- run: mkdir -p tgz && npm pack --ignore-scripts --pack-destination tgz && ls -la tgz
# kept as long as the engines: publish-npm ships this exact tarball, whenever the draft is published
- uses: actions/upload-artifact@v4
with: { name: code-graph-tgz, path: tgz, retention-days: 3, if-no-files-found: error }
with: { name: code-graph-tgz, path: tgz, retention-days: 90, if-no-files-found: error }

e2e:
name: e2e on ${{ matrix.target.platform }}
Expand Down
60 changes: 47 additions & 13 deletions .github/workflows/publish-npm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,17 +67,16 @@ jobs:
echo "dist_tag=$dist_tag" >> "$GITHUB_OUTPUT"
echo "publishing $version as '$dist_tag'"

engines:
needs: check
uses: ./.github/workflows/build-engines.yml
with:
fresh: true # what ships is compiled from scratch, never restored

# Nothing is compiled here. The push that landed this version on main built every platform's
# engines, packed @axiomcode/code-graph and ran the five-platform e2e on exactly those files;
# release.yml drafted this release only after that run was green. Publishing ships that build,
# so what was tested is what users install, and a release costs one build, not three (#1350).
publish:
needs: [check, engines]
needs: [check]
runs-on: ubuntu-24.04
permissions:
contents: write # attach the tarballs to the release
actions: read # the release's CI run and its artifacts
env:
VERSION: ${{ needs.check.outputs.version }}
DIST_TAG: ${{ needs.check.outputs.dist_tag }}
Expand All @@ -88,8 +87,33 @@ jobs:
with:
node-version: '22'
registry-url: 'https://registry.npmjs.org'
- name: the green CI run that built and tested this commit
id: run
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
sha="$(git rev-parse HEAD)"
run="$(gh run list --workflow ci.yml --commit "$sha" --event push --branch main --status success \
--limit 1 --json databaseId --jq '.[0].databaseId // empty')"
if [ -z "$run" ]; then
echo "::error::no green CI run of a push to main for ${sha::8}: nothing built and tested this commit, so nothing is published"
exit 1
fi
echo "id=$run" >> "$GITHUB_OUTPUT"
echo "shipping the build of CI run $run ($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$run)"
- uses: actions/download-artifact@v4
with: { pattern: engines-*, path: artifacts }
with:
pattern: engines-*
path: artifacts
run-id: ${{ steps.run.outputs.id }}
github-token: ${{ github.token }}
- uses: actions/download-artifact@v4
with:
name: code-graph-tgz
path: artifacts/tgz
run-id: ${{ steps.run.outputs.id }}
github-token: ${{ github.token }}

# A real release ships every platform the root package pins. Missing one
# means some machine installs a version whose engine does not exist.
Expand Down Expand Up @@ -126,10 +150,13 @@ jobs:
cat "packages/engine-$platform/package.json"
done

# `prepare` builds the parser and the driver, so the packed tarball holds
# parser/dist and dist exactly as a user installs them.
- name: build @axiomcode/code-graph
run: npm install --no-audit --no-fund
- name: the tested @axiomcode/code-graph tarball is this version
run: |
set -euo pipefail
tgz="$(ls artifacts/tgz/*.tgz)"
got="$(tar -xOzf "$tgz" package/package.json | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>console.log(JSON.parse(s).version))')"
[ "$got" = "$VERSION" ] || { echo "::error::the tested tarball is $got, this release is $VERSION"; exit 1; }
echo "TGZ=$tgz" >> "$GITHUB_ENV"

- name: publish (or dry-run)
env:
Expand All @@ -150,7 +177,14 @@ jobs:
&& npm publish --access public --tag "$DIST_TAG" $flag )
}
for p in packages/engine-*; do publish "$p"; done
publish .
# the tarball the e2e installed, published as it is rather than packed again
if [ -z "$flag" ] && npm view "@axiomcode/code-graph@$VERSION" version >/dev/null 2>&1; then
echo "══ @axiomcode/code-graph@$VERSION is already on the registry — skipped"
else
echo "══ @axiomcode/code-graph@$VERSION tag=$DIST_TAG $flag"
cp "$TGZ" tarballs/
npm publish "$TGZ" --access public --tag "$DIST_TAG" $flag
fi
ls -la tarballs
if [ "$DRY" = true ]; then
echo "DRY RUN — nothing was uploaded."
Expand Down
32 changes: 22 additions & 10 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,26 @@
# Every version that lands on main gets a tag and a draft release, and dev is
# brought up to date with main (the sync-dev job at the end).
#
# The version gate (ci.yml) makes a pull request that reaches users bump the
# version. When that bump merges, this workflow tags the merge commit v<version>
# and opens a DRAFT GitHub release with notes generated from the pull requests
# since the previous tag. Nothing is published here.
# A push to main whose version has no tag yet is a release. CI on that push builds
# the engines on all five platforms and runs the five-platform e2e; only when that
# run is GREEN does this workflow tag the commit it tested v<version> and open a
# DRAFT GitHub release with notes generated
# from the pull requests since the previous tag. Nothing is published here.
#
# Publishing is a person's decision: review the draft, then press Publish. That
# `release: published` event is what runs publish-npm.yml, which builds the
# engines, checks that the tag and every manifest agree, publishes to npm and
# attaches the tarballs to the release.
# `release: published` event runs publish-npm.yml, which ships the binaries and
# the tarball of that same green CI run: nothing is compiled twice (#1350).
#
# A push that does not move the version finds its tag already present and does
# nothing, so this runs on every push to main without a path filter.
# A version that is already tagged finds its tag present and does nothing.
# ─────────────────────────────────────────────────────────────────────────────
name: release

on:
push:
branches: [main] # sync-dev
workflow_run: # tag: after CI on main finished
workflows: [CI]
types: [completed]
branches: [main]
workflow_dispatch:

Expand All @@ -31,10 +34,18 @@ permissions:

jobs:
tag:
# the CI run of a PUSH to main, and only a green one: a red release build drafts nothing
if: >-
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'workflow_run' && github.event.workflow_run.event == 'push' &&
github.event.workflow_run.conclusion == 'success')
runs-on: ubuntu-24.04
env:
SHA: ${{ github.event.workflow_run.head_sha || github.sha }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ env.SHA }}
fetch-depth: 0

- uses: actions/setup-node@v4
Expand Down Expand Up @@ -67,7 +78,7 @@ jobs:
# a person to publish the draft.
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git tag -a "$tag" -m "$tag" "$GITHUB_SHA"
git tag -a "$tag" -m "$tag" "$SHA"
git push origin "refs/tags/$tag"
gh release create "$tag" --draft --verify-tag --title "$tag" \
--generate-notes $start $prerelease
Expand All @@ -81,6 +92,7 @@ jobs:
# that went straight to main and touches lines dev has since changed does not,
# and then nothing is pushed: an issue says how to resolve it by hand.
sync-dev:
if: github.event_name == 'push'
runs-on: ubuntu-24.04
permissions:
contents: write
Expand Down
Loading