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
27 changes: 26 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ on:
push:
branches: [main]
pull_request:
# release.yml runs this as its gate, so that a release passes the same CI as a pull request.
# release.yml runs this as its gate, so that a release passes the same CI as a pull request, unless a push or
# pull_request run of it has already passed on the same tree.
workflow_call:
# Each night on the images the tests name, without the pin below, so an engine's new release shows up here and not
# in a pull request's run.
Expand All @@ -19,6 +20,30 @@ env:
FALKORDB_TEST_IMAGE: ${{ github.event_name != 'schedule' && 'falkordb/falkordb:6.0.3' || '' }}

jobs:
# Records the git tree this run tests, as an artifact named ci-tree-<tree>, so a release of the same tree can trust
# this run and skip its gate (scripts/ci_trust.py). It matters on a pull request, which tests its merge ref, not the
# head commit the run names. It has no condition, as no job here may: release.yml trusts a run only when every job
# in it passed, and a job skipped by a condition doesn't.
tree:
name: record the tree
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Work out the tree
id: tree
run: |
tree=$(git rev-parse 'HEAD^{tree}')
echo "tree=$tree" >> "$GITHUB_OUTPUT"
echo "$tree $(git rev-parse HEAD)" > ci-tree.txt
echo "This run tests tree $tree, of commit $(git rev-parse HEAD)." >> "$GITHUB_STEP_SUMMARY"
# Overwritten when the run is re-run, as an artifact's name is unique within a run.
- uses: actions/upload-artifact@v4
with:
name: ci-tree-${{ steps.tree.outputs.tree }}
path: ci-tree.txt
if-no-files-found: error
overwrite: true

# The Cypher spec's copies in tests/cypher are the tag tests/cypher/VERSION names, unchanged, and a later tag is
# shown as a warning. The script reads the standard library alone, so nothing is installed for it.
cypher-spec:
Expand Down
55 changes: 47 additions & 8 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
# - A push to main that raises pyproject.toml's version (the merge of a "Release vX.Y.Z" PR) publishes it, and tags
# the commit vX.Y.Z. A push to main that changes pyproject.toml without raising the version publishes nothing.
# - A push of a tag vX.Y.Z, by hand, publishes too: the fallback.
# - Either way, the full CI runs, the distributions are built and checked, and those exact files are uploaded to PyPI
# with trusted publishing.
# - Either way, the full CI runs, unless CI has already passed on the same tree; the distributions are built and
# checked, and those exact files are uploaded to PyPI with trusted publishing.
# - workflow_dispatch, and a pull request that changes the release machinery, are dry runs: everything but the
# publish, with the built files left as the run's artifact to inspect.
name: Release
Expand All @@ -24,6 +24,7 @@ on:
- pyproject.toml
- scripts/smoke_wheel.py
- scripts/release_version.py
- scripts/ci_trust.py

permissions:
contents: read
Expand Down Expand Up @@ -94,16 +95,51 @@ jobs:
echo "publish=$publish" >> "$GITHUB_OUTPUT"
echo "tag=$tag" >> "$GITHUB_OUTPUT"

# The full CI, as ci.yml runs it on a push or a pull request. A push that releases nothing stops at check: ci.yml
# runs on the same push to main by itself.
gate:
# Whether ci.yml has already passed on the git tree this run builds, from a push or a pull request, so the gate
# needn't run it again: the release PR's run, when it was up to date with main and squash merged, or the push run on
# the merge commit, which starts beside this one and is waited for. A run on this tree that fails fails the release.
# scripts/ci_trust.py says how a run's tree is known, and why a pull request's run can be believed; the step summary
# links the run trusted, or says why none was. Skipped, as the gate is, on a push that releases nothing.
trust:
needs: check
if: needs.check.outputs.publish == 'true' || github.event_name != 'push'
runs-on: ubuntu-latest
timeout-minutes: 70
permissions:
# The checkout's, as for the workflow.
contents: read
# Reading ci.yml's runs, their jobs and their artifacts.
actions: read
outputs:
trusted: ${{ steps.trust.outputs.trusted }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
# On a pull request the checkout is its merge ref, and ci.yml's runs for it name its head commit. The script
# reads the standard library alone, so the runner's python3 runs it, as check's does.
- name: Look for a run of CI on this tree
id: trust
env:
GH_TOKEN: ${{ github.token }}
PR_HEAD: ${{ github.event.pull_request.head.sha }}
run: |
tree=$(git rev-parse 'HEAD^{tree}')
python3 scripts/ci_trust.py "$GITHUB_REPOSITORY" "$tree" "$(git rev-parse HEAD)" "$PR_HEAD"

# The full CI, as ci.yml runs it on a push or a pull request, unless trust found it has already passed on this tree.
# A push that releases nothing stops at check: ci.yml runs on the same push to main by itself.
gate:
needs: [check, trust]
if: needs.trust.outputs.trusted != 'true'
uses: ./.github/workflows/ci.yml

# A push that releases nothing skips gate, and so this and the jobs after it.
# Runs when the gate passed, or was skipped because trust found CI had passed on this tree. A failed or cancelled
# check, trust or gate stops it, and so does a gate skipped for any other reason (a push that releases nothing).
build:
needs: [check, gate]
needs: [check, trust, gate]
if: >-
${{ !cancelled() && needs.check.result == 'success' && needs.trust.result == 'success' &&
(needs.gate.result == 'success' ||
(needs.gate.result == 'skipped' && needs.trust.outputs.trusted == 'true')) }}
runs-on: ubuntu-latest
env:
VERSION: ${{ needs.check.outputs.version }}
Expand Down Expand Up @@ -157,9 +193,11 @@ jobs:
path: dist/
if-no-files-found: error

# A skipped gate is still upstream of this job, so its condition checks build's result itself rather than leave it
# to the default, which would skip it.
publish:
needs: [check, build]
if: needs.check.outputs.publish == 'true'
if: ${{ !cancelled() && needs.build.result == 'success' && needs.check.outputs.publish == 'true' }}
runs-on: ubuntu-latest
environment:
name: release
Expand All @@ -179,6 +217,7 @@ jobs:

github-release:
needs: [check, publish]
if: ${{ !cancelled() && needs.publish.result == 'success' }}
runs-on: ubuntu-latest
permissions:
contents: write
Expand Down
29 changes: 24 additions & 5 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ three repositories at once.
lists the commits since the last tag.
2. **Review and merge the release PR.** It passes CI like any other PR. Merging it is the release point.
3. **CI publishes.** The merge, a push to the default branch that changes the version file, runs
`.github/workflows/release.yml`. It runs the full gate, builds, checks, publishes, and creates the GitHub release
with its tag `vX.Y.Z` on the merge commit. Nothing is built from a laptop.
`.github/workflows/release.yml`. It runs the full gate, unless CI has already passed on the same tree, builds,
checks, publishes, and creates the GitHub release with its tag `vX.Y.Z` on the merge commit. Nothing is built from
a laptop.
4. **Credentials.** Repository secrets or trusted publishing (OIDC), held in a GitHub environment named `release`.
Give that environment required reviewers: now that merging publishes, their approval of the run is the human
checkpoint before anything goes out.
Expand Down Expand Up @@ -43,8 +44,24 @@ git push origin vX.Y.Z
bump, for example.
- On a tag push it fails, saying why, unless the tag is `vX.Y.Z` (or `vX.Y.Z-suffix`), its version equals the build
file's, and the tagged commit is on the default branch.
- **`gate`** runs the repository's full CI, `ci.yml`, unchanged. A push to the default branch that releases nothing
skips it, as `ci.yml` runs on that push anyway.
- **`trust`** looks for a `ci.yml` run that has already passed on the git tree being released
(`git rev-parse HEAD^{tree}` of the commit the run checked out), so that `gate` needn't run CI again. Trees are
compared, not commits: a squash merge of a release PR that was up to date with the default branch has the tree its
pull request run tested. `scripts/ci_trust.py` does it, reading the GitHub API with `actions: read`.
- A run counts when it is from a push or a pull request (the nightly, which runs without the pins, doesn't), its
head commit has this tree, and every job of its latest attempt concluded success. A pull request run tests the
pull request's merge ref, not its head, so `ci.yml`'s first job records the tree it checked out as an artifact,
`ci-tree-<tree>`, and a pull request run counts only when it recorded this tree as well. A run of a fork's
pull request is believed for the same reason: its head has this tree, so the `ci.yml` it ran is the one being
released.
- When one has passed, `gate` is skipped. When one is still going, as the push run on the merge commit is when
the release starts beside it, `trust` waits for it, up to an hour: if it passes `gate` is skipped, and if it fails
or is cancelled the release fails, linking it. Re-running the release's failed jobs then runs `gate`, unless that
CI run has been re-run too, which `trust` then waits for or trusts. When none counts, or the wait runs out, or
the API can't be asked, `gate` runs as before.
- The run's summary links the run trusted, or says of each run it looked at why it didn't count.
- **`gate`** runs the repository's full CI, `ci.yml`, unchanged, unless `trust` found it has already passed on this
tree. A push to the default branch that releases nothing skips both, as `ci.yml` runs on that push anyway.
- **`build`** builds the release artifacts from the checked-out ref, checks them, runs a smoke test, and uploads them
as the run's artifact.
- **`publish`** runs on a release alone, in the `release` environment. It publishes the exact files `build` made.
Expand All @@ -59,7 +76,9 @@ failed jobs.

A **dry run** is everything but `publish` and `github-release`. Run it from the Actions tab (Release, Run workflow,
on any branch or tag), and it runs by itself on a pull request that changes the release machinery, a release PR
included. Only a merged version bump or a pushed tag publishes.
included. Only a merged version bump or a pushed tag publishes. `trust` skips its `gate` as it does a release's: a
manual run on the default branch trusts the push run on its commit, and a pull request's dry run the pull request's
own run, when the pull request is up to date with its base.

## drivine-python: PyPI

Expand Down
Loading
Loading