This page is the map of the repository's GitHub Actions setup: what each workflow is for, which signal it is meant to give, and the conventions every workflow change should follow. It is the design reference for the CI/CD epic (#711). Each convention below is marked in place or target, and a target links the issue that delivers it. When that issue lands, update the convention's status here in the same PR.
For the commands to run before opening a PR, see the
pre-PR gate in CONTRIBUTING.md. For how
a release reaches PyPI, see releasing.md. For the dependency
policy behind the locks and the nightly run, see
ADR 0007.
Contents
- The signals
- Scenario maps
- Conventions
- 1. PR CI resolves from a lock; the nightly run resolves fresh. In place (#982)
- 2. One required status check: ci-ok. In place (#755)
- 3. CI runs on develop, not just on PRs. In place (#755)
- 4. Every scheduled workflow alerts on failure. Partly in place
- 5. One task runner mirrors CI. In place (#765)
- 6. Jobs are bounded and superseded runs are cancelled. In place (#761)
- 7. Least-privilege workflows. Partly in place (#757, #771)
- 8. The site is built on PRs, on a supported Node. In place (#759)
- 9. Release gates are code, not prose. Target (#767)
- 10. Workflow names follow one style. In place (#1009)
- Changing a workflow
Each workflow answers exactly one question. Mixing two signals in one job makes a red run ambiguous, so a workflow that starts answering a second question should be split.
| Question | Answered by | When it runs |
|---|---|---|
| Did this change break something? | ci.yml (PR-gating jobs, resolving from the committed lock) |
Every PR; pushes to main and develop |
| Did an upstream release break us? | nightly-matrix.yml, plus the fresh-resolve jobs in ci.yml (all-extra-resolves, anthropic-stacks, adk-stacks) |
Daily 06:00 UTC; every PR for the ci.yml jobs |
| Can the lock move to the newest releases? | lock-refresh.yml (opens one bump PR; its CI answers) |
After each green nightly on develop; manual |
| Which new framework release breaks which adapter? | framework-release-watch.yml (one framework-release issue per release ahead of the lock) |
Daily 05:30 UTC; manual, optionally for one package or a replay of one past release; a dry run on PRs that change it |
| Did the live gateway's contract drift? | live-contract-check.yml (the openai-model-routing proxy only) |
Mondays 07:00 UTC |
| Did a secret get committed? | secret-scan.yml (gitleaks over the full history) |
Every PR; pushes to main and develop |
| Is the published site current? | docs.yml (build and deploy to GitHub Pages) |
Pushes to main under website/**; every 6 hours; manual |
| Is the artifact fit to publish? | publish-testpypi.yml, publish-pypi.yml |
Manual dispatch on develop / a published GitHub Release |
| Does the PR carry its issue's labels? | pr-labels.yml |
PR events (pull_request_target) |
The jobs, grouped by what they protect:
- Layering and packaging.
base-onlyinstalls only the base package and runstests/unit, so a framework import leaking into a lower layer fails here. It then checks the built wheel in its own environment.all-extra-resolvesdry-runspip install ".[all]"on every supported Python (#697).new-dependencieschecks that any direct dependency new to the PR exists on PyPI (#936).commit-identitiesfails a PR whose commits carry a placeholder git identity such asx <x@x>(#1048). - Static checks.
typecheck-and-lintrunsmypy,ruff check,lint-imports, the verification-claim and doc-link checkers, the fixture scrub check andvulture. - Behaviour.
testis the Python 3.10–3.14 matrix and includes thelocal_gatewaysuite.openai-1xruns the same suites withopenai<2, the 1.x range thellmextra's floor allows (#769).adapter-contractruns one leg perADAPTERSextra with its real framework installed (#742). The framework-specific jobs (anthropic-stacks,adk-stacks,agent-framework-middleware,llamaindex-transport,agents-strands-last-call) cover tests that only mean something with that framework present. - Promises made in the docs.
benchmarkenforces the < 1 ms span overhead (BG §1.6) and trends it across runs.quickstart,langgraph-demoandopenai-agents-demorun the documented examples against the local simulator.docs-llms-driftregeneratesllms.txtand fails on any diff.docs-sitebuilds the site and checks its internal links and anchors (#759). - Supply chain.
pip-auditaudits a fresh resolve of the base install and of.[all]for known vulnerabilities, blocking (#757).dependency-reviewflags a PR that adds a vulnerable dependency or one under a GPL/AGPL licence, and runs on PRs only.workflow-lintruns actionlint and zizmor over.github/(convention 7). - The gate itself.
ci-okneeds every other job and fails unless each one succeeded or was skipped. It is the one required status check (convention 2).
The nightly run resolves fresh against the newest upstream releases, with no
constraints (ADR 0007 rule 4). matrix re-verifies the conformance-tested
adapters (LangGraph, the OpenAI Agents SDK) against §8 signatures, and framework-legs runs every
ADAPTERS entry (#748). lowest-direct installs base, the framework-free
extras and each framework extra at their declared floors (uv pip install --resolution lowest-direct, Python 3.10), runs their suites, and fails a leg
whose installed versions are not its declared floors
(scripts/check_floors.py, ADR 0007 rule 1). co-installability re-resolves
every framework extra pair and fails when docs/co-installability.md no longer
matches (#769). pip-audit repeats the PR job's vulnerability audit, so an
advisory published between PRs still reaches a human (#757). A live proxy
round-trip is available only through
workflow_dispatch with live: true. A red nightly is an upstream signal: fix
it by adapting the code or by moving the lock, never by adding a ceiling
(§8.4). Any failed run, scheduled or manual, opens one issue labelled
nightly-failure, or comments on it if it is already open (#755). Close that
issue once the nightly is green again.
After every green nightly on develop, and on manual dispatch, this workflow
reruns python scripts/compile_constraints.py on develop (#763,
ADR 0011). The script also moves the
ruff-pre-commit rev: and the lint-imports hook's import-linter pin in
.pre-commit-config.yaml to the new typecheck-and-lint lock, so a ruff release
doesn't fail tests/unit/test_noxfile.py on the bump PR (#1206). When any file under
python/constraints/ changed, it regenerates the docs that restate the lock
(python scripts/generate_docs.py, which writes the
framework version pages, #1068) and the site's llms
outputs, force-pushes the result to bot/lock-refresh and opens one PR to
develop, or updates the body of the one already open. Merge it once its CI is green. A red bump PR is an upstream
change the lock can't take yet: fix the code, or leave the PR open until the
next refresh replaces it. Never add a ceiling (§8.4). The branch belongs to
the workflow, so don't push to it by hand. A failed run opens or updates one
issue labelled lock-refresh-failure.
One-time setup. A PR opened with the default github.token doesn't start
pull_request workflows, so its CI never runs on its own. A maintainer with
admin rights on the repository picks one:
- Add a
LOCK_REFRESH_TOKENrepository secret: a fine-grained token, or a GitHub App installation token, with Contents and Pull requests write access to this repository only. The workflow uses it when it exists, and CI runs on the bump PR like on any other. - Or, with no secret, allow the default token to open PRs: Settings → Actions → General → Workflow permissions → Allow GitHub Actions to create and approve pull requests. Then close and reopen each bump PR to start its CI.
Without either, the PR step fails and the run files the
lock-refresh-failure issue. Check the setup with
gh workflow run lock-refresh.yml --ref develop.
The nightly resolves everything fresh at once, so a red nightly doesn't say
which upstream release broke which adapter. This workflow tests each new
release on its own (#1066). detect runs
python scripts/check_framework_releases.py, which lists every direct
requirement of each framework extra whose newest stable PyPI release is ahead
of its pin in python/constraints/adapter-contract-<extra>-py3.12.txt.
Pre-releases and yanked releases don't count. evaluate runs one leg per
release. It installs the adapter-contract job's line with that one pin moved.
If that doesn't resolve, it installs the release unconstrained and marks it
"needs co-bump". Then it runs the adapter-contract checks:
verify_frameworks.py --require-installed, the whole suite with the skip guard
armed, and the example smoke. A red check is a result, not a red run.
alert keeps one open issue per release, titled
framework-release: <package> <locked> → <latest> and labelled
framework-release and dependencies. The issue lists the versions, the bump
kind, the release date and notes, the result of each check, the run link and
the next action. A rerun refreshes the body and comments only when the results
changed. A newer release of the same package closes the older issue as
superseded. Once the lock reaches the release, the next run closes the issue
with a link to the lock commit.
What to do with an issue:
- All green: safe to bump. The next
lock-refresh.ymlPR may already carry it. If not, frompython/runpython scripts/compile_constraints.py, thenpython scripts/generate_docs.py, thenpython scripts/verify_frameworks.py --only <framework> --emit-verified, and then the pre-PR gate. - Anything red. Adapt the adapter to the release, or record the
incompatibility in
verified-apis.md§8.1. Never add a ceiling (§8.4).
Impact review (AI, advisory). The checks say whether a release breaks the
adapter; they don't say what in the changelog did. Beside evaluate, the
ai-review job runs one leg per release (#1067). It gives
anthropics/claude-code-action (pinned by SHA, model claude-sonnet-5-5) a
prompt that python scripts/review_framework_release.py --prompt builds: the
package and both versions, the release notes and the GitHub compare link, the
adapter module, its FRAMEWORKS rows in verify_frameworks.py, and its
verified-apis.md §8 rows with all of §8.1 and §8.2. The
model answers in a fixed schema: Risk (none, low or high), the affected
symbols, each with an upstream citation, the recommended action (bump,
adapt adapter, record §8.1, or watch) and its confidence. alert puts the
answer in the issue under "Impact review (AI, advisory)".
- It is advisory. It is never verification (§0.3), and it never changes a §8 row or the next action the checks give. Confirm a finding against the cited source before acting on it.
- It is read-only. The job has
contents: readand no write token. The model can read files (never/proc, where its key lives) and fetch pages from github.com, raw.githubusercontent.com and pypi.org, nothing else: no shell, no edits, no subagents and no GitHub tools, and in-repo Claude settings are ignored. The prompt says fetched pages are data, not instructions. Onlyalertwrites, and the answer is untrusted: the script keeps only the schema's fields, escapes Markdown, strips HTML and comment markers, neutralises@mentions and#references, keeps onlyhttp(s)citations, and drops an answer that holds anything shaped like a credential. - It never fails the run. No key, a failed action or an answer outside the schema leaves "Impact review unavailable" in the issue and a notice in the run. A rerun without a review keeps the review an earlier run wrote.
- It runs once per release. The review is cached by extra, package and
version (
actions/cache), so the daily rerun of an open issue costs nothing. To review a release again, delete itsai-review-v1-…cache under Actions → Caches.
One-time setup for the review. The key is an environment secret, never a
repository secret, and the environment admits only develop:
- Create an
ai-reviewenvironment in Settings → Environments. Under deployment branches, allow onlydevelop; leave required reviewers off, because the daily run is unattended. - Add the environment secret
ANTHROPIC_API_KEY:gh secret set ANTHROPIC_API_KEY --env ai-reviewprompts for the value.tests/unit/test_ci_workflows.pyholdsai-reviewas the only job on that environment, and keeps it off pull requests.
Run it for one package with
gh workflow run framework-release-watch.yml --ref develop -f package=crewai.
To check the review against a release with a known outcome, replay that
release as a dry run:
gh workflow run framework-release-watch.yml --ref develop -f package=anthropic -f replay=0.116.0..1.0.0
evaluates and reviews anthropic 1.0.0 as if the lock still pinned 0.116.0,
and puts the planned issue in the job summary without writing it. A PR that
changes the workflow or either script gets a dry run too: the planned issue
bodies go to the job summary, nothing is written, and the review doesn't run. A
failed run opens or updates one issue labelled framework-release-watch-failure.
Every Monday this workflow runs the sandbox suite (tests/sandbox/) against
one real proxy, openai-model-routing. From the repository secrets it writes a
tests/sandbox/proxies.toml with that one entry, so tests for every other proxy
skip. The drift guard is test_openai_routing_response_shape_matches_fixture.
Any failure opens one issue labelled live-contract-drift, or updates it if it
is already open (#753).
One-time setup. The workflow names the environment; repository settings supply its contents. A maintainer with admin rights on the repository sets them up (#999):
-
Create a
live-sandboxenvironment in Settings → Environments. Under deployment branches, allow onlydevelop; scheduled runs use the default branch. Leave required reviewers off, because the Monday run is unattended. A reviewer would hold every run until someone approved it. -
Add three environment secrets:
Secret Value DONKEY_LLM_PROXY_URLThe proxy base URL, https://<gateway-host>/<base-path>. No/v1, same shape as the SDK's.envDONKEY_LLM_PROXY_CLIENT_IDThe client_idof a consumer app with a contract on that proxyDONKEY_LLM_PROXY_CLIENT_SECRETThat app's client_secretgh secret set <NAME> --env live-sandboxprompts for each value, so the value never reaches your shell history. -
Check the setup with a manual run, without waiting for Monday:
gh workflow run live-contract-check.yml --ref develop. A healthy run passes theopenai-model-routingtests and skips the rest.
Reading a red run.
- Missing secret(s). The first step fails the run when a secret is empty. Without it, every sandbox test would skip and the run would go green having checked nothing.
InvalidURLin every test, before any request.DONKEY_LLM_PROXY_URLis not a plain URL; it may be a truncated or placeholder value. Set it again.- A 401 or 403 from the proxy. The credentials are wrong, or the consumer app's contract was revoked.
- A fixture or shape mismatch. The gateway's contract really drifted. Track
it on the
live-contract-driftissue.
Which workflows fire for a change, and what to do when a run goes red. Merge
mechanics (squash vs rebase vs no-ff) live in
CONTRIBUTING.md §1;
these diagrams only show the Actions side.
Branch from develop, PR into develop, later promote when a milestone is
ready. docs.yml and the publish workflows are optional legs on that path.
Grey boxes are human / git steps. Coloured boxes are workflows: blue =
PR/push gates (ci.yml, secret-scan.yml), violet = PR hygiene
(pr-labels.yml), green = site deploy (docs.yml), amber = publish.
flowchart TD
issue[Issue filed] --> branch["Branch from develop<br/>feat|fix|docs|chore/#-slug"]
branch --> push[Push commits]
push --> pr[Open PR → develop]
pr --> prci["ci.yml + secret-scan.yml"]
pr --> labels["pr-labels.yml"]
prci -->|ci-ok green| review[Review + squash merge]
review --> developPush["Push to develop"]
developPush --> developCi["ci.yml + secret-scan.yml<br/>again on develop"]
developCi --> wait[More issues / milestone → 0 open]
wait --> promo["Promotion PR: develop → main<br/>merge commit, no fast-forward"]
promo --> mainPush["Push to main"]
mainPush --> mainCi["ci.yml + secret-scan.yml"]
mainPush -->|website/** or docs.yml changed| docs["docs.yml<br/>build + deploy Pages"]
mainPush --> tag["Tag main + GitHub Release<br/>see releasing.md"]
tag -->|final, non-prerelease| pypi["publish-pypi.yml"]
developPush -.->|manual workflow_dispatch| testpypi["publish-testpypi.yml<br/>.devN dry-run"]
classDef gate fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
classDef hygiene fill:#ede9fe,stroke:#7c3aed,color:#4c1d95
classDef deploy fill:#dcfce7,stroke:#16a34a,color:#14532d
classDef publish fill:#fef3c7,stroke:#d97706,color:#78350f
class prci,developCi,mainCi gate
class labels hygiene
class docs deploy
class pypi,testpypi publish
Scheduled workflows (nightly-matrix.yml, live-contract-check.yml, the
docs.yml cron) are independent of this path; they keep running on their own
cadence.
The exception when main must move without waiting for a full promotion —
usually a broken live docs site (website/** / docs.yml), sometimes a
package patch. Branch from main, rebase-merge one commit, then cherry-pick
onto develop via a second PR. A docs hotfix never bumps the version; a
package hotfix does (see releasing.md → Hotfix releases).
Same colour key as the feature path: blue gates, green deploy.
flowchart TD
break["Live main broken<br/>or urgent package patch"] --> issue[Issue filed]
issue --> branch["Branch from main<br/>hotfix/#-slug"]
branch --> prMain["PR → main"]
prMain --> prci["ci.yml + secret-scan.yml<br/>required on the PR"]
prci -->|ci-ok green| rebase["Rebase-merge<br/>one commit on main"]
rebase --> mainCi["ci.yml + secret-scan.yml<br/>on the push"]
rebase -->|website/** or docs.yml| docs["docs.yml<br/>rebuilds live site"]
rebase --> cherry["Cherry-pick SHA onto<br/>hotfix/#-cherry-pick-develop"]
cherry --> prDev["PR → develop"]
prDev --> prDevCi["ci.yml + secret-scan.yml"]
prDevCi -->|ci-ok green| squash["Squash-merge into develop"]
squash --> developCi["ci.yml + secret-scan.yml<br/>on develop"]
classDef gate fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
classDef deploy fill:#dcfce7,stroke:#16a34a,color:#14532d
class prci,mainCi,prDevCi,developCi gate
class docs deploy
Never merge main back into develop as a branch merge — only the
cherry-pick PR. Never push straight to main without a PR.
Red / orange / pink boxes are the failing workflow; grey boxes are the recovery steps.
flowchart TD
red{Which signal is red?}
red -->|PR: ci-ok or secret-scan| prFail["ci.yml / secret-scan.yml"]
prFail --> prFix["Fix on the branch and push.<br/>Concurrency cancels the previous PR run."]
prFix --> prGate["Re-run is the same gates.<br/>Do not open a second PR."]
red -->|Push to develop after merge| developFail["ci.yml on develop"]
developFail --> developRed["Two green PRs can still break develop.<br/>That is why convention 3 exists."]
developRed --> developRecover["Fix-forward PR, or<br/>git revert the squash SHA on develop."]
red -->|Push / PR to main hotfix| mainFail["ci.yml on main / hotfix PR"]
mainFail --> mainRed["main ruleset requires ci-ok.<br/>Fix the hotfix branch; do not force-push past a red gate."]
mainRed --> mainRecover["If already merged: git revert the hotfix SHA<br/>and revert the cherry-pick on develop too."]
red -->|nightly-matrix.yml| nightly["nightly-matrix.yml<br/>opens or updates nightly-failure"]
nightly --> nightlyFix["Adapt the code or refresh the lock.<br/>Never add an upper ceiling ADR 0007 / §8.4."]
nightlyFix --> nightlyClose["Close the issue when the next nightly is green."]
red -->|live-contract-check.yml| live["live-contract-check.yml<br/>opens or updates live-contract-drift"]
live --> liveRead["Read Reading a red run above:<br/>empty secret, bad URL, 401/403, or real drift."]
liveRead --> liveClose["Fix env/creds or the fixture;<br/>close the issue when Monday or a manual run is green."]
red -->|docs.yml deploy| docsRed["docs.yml"]
docsRed --> docsPath["If visitors are broken and promotion cannot wait:<br/>use the hotfix path above.<br/>Else fix on develop and ride the next promotion."]
classDef failGate fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
classDef failNightly fill:#ffedd5,stroke:#ea580c,color:#7c2d12
classDef failLive fill:#fce7f3,stroke:#db2777,color:#831843
classDef failDeploy fill:#fecaca,stroke:#b91c1c,color:#7f1d1d
class prFail,developFail,mainFail failGate
class nightly failNightly
class live failLive
class docsRed failDeploy
A red PR is expected during development; a red develop after merge, a red
nightly, or a red live-contract run is a maintainer signal — track it on the
labelled issue those workflows open, and close that issue only when the signal
is green again.
PR-gating jobs install with
-c python/constraints/<job>-py3.NN.txt. There is one file per job's exact
extras, dev group and Python combination, never merged across combinations
(the reason is in python/scripts/compile_constraints.py). Jobs whose purpose
is a fresh resolve keep resolving fresh. To refresh the lock, run
python scripts/compile_constraints.py, and keep its _COMBOS in sync with the
install lines in ci.yml.
The lock also covers tools (#763). The release dependency group (build,
twine, hatchling) and the lock group (uv) compile to
constraints/release-py3.11.txt and constraints/lock-py3.11.txt. Every
python -m build step installs from the release file and builds with
--no-isolation, so a published dist is built by the locked hatchling.
lock-refresh.yml opens the PR that moves the lock after each green nightly
(ADR 0011; Dependabot would raise the
floors and bump pins one at a time). mypy never follows an optional extra
(follow_imports = "skip"), so its result doesn't depend on which extras are
installed. The declared floors are not locked: the nightly lowest-direct job
installs them fresh (#769). One tool stays unlocked: NOX_VERSION pins nox
itself, but each job's pipx run resolves nox's own dependencies fresh
(convention 5).
An aggregate ci-ok job needs: every other job, runs with if: always(), and
fails if any dependency did not succeed (skipped counts as a pass, because
new-dependencies and commit-identities run on PRs only). The develop and main rulesets require
only ci-ok, so renaming a job or changing a matrix never silently drops a
required check. A new job is added to ci-ok's needs: in the same PR;
tests/unit/test_ci_workflows.py fails when one is missing.
ci.yml also runs on pushes to develop, so two individually green PRs that
combine into a broken develop are caught at merge time instead of at
promotion. secret-scan.yml covers develop too. There is no merge queue, so
ci.yml has no merge_group: trigger; add one if a queue is adopted.
Each cron workflow ends with an if: failure() job or step that opens or
updates one labelled tracking issue (never one issue per run), using
issues: write on that job only. live-contract-check.yml does this with the
live-contract-drift label (#753), nightly-matrix.yml with the
nightly-failure label (#755), lock-refresh.yml, which follows the
nightly, with the lock-refresh-failure label (#763), and
framework-release-watch.yml with the framework-release-watch-failure label
(#1066). docs.yml's cron still needs the same step.
python/noxfile.py has one session per ci.yml job, named after the job
(ci-ok aside), and the CONTRIBUTING gate documents sessions instead of raw
commands, so nox from python/ reproduces every blocking job. Each job keeps
its install and cache steps (its lock lives there, convention 1) and then runs
its session in place: pipx run --spec "nox==$NOX_VERSION" nox --no-venv --no-install -s <job>. Locally, the session installs the same line into its own
virtualenv. tests/unit/test_noxfile.py checks that every job has a session,
calls it, installs what the session's CI_INSTALLS entry says, and runs no
check inline (a check tool, a repo script, npm, node or timeout). The
exceptions are named in that test and in the noxfile's docstring:
- CI-only steps. The benchmark's trend alert reads the previous
mainrun's artifact, andworkflow-lintdownloads actionlint and zizmor (each verified by sha256) onto the Linux runner. Locally the session takes both linters fromPATH. uses:actions stay steps.pip-audit's session writes the exact pin list that the pinnedpypa/gh-action-pip-auditstep then audits, and run locally it installs pip-audit and audits the list itself.dependency-reviewis a single action with no checkout, and the dependency graph diff it reads exists only on GitHub, so its job calls no session. The session of the same name only skips, saying so.- The site's
npm ci.docs-siteinstalls nothing throughCI_INSTALLS: its session runsnpm ciitself, under--no-installtoo, so the job keeps only its Node setup.
.pre-commit-config.yaml
runs gitleaks, ruff, ruff format, lint-imports and the llms.txt regeneration
at commit time, with the byte-exact fixtures excluded from the formatter. The
nightly matrix and the publish workflows are not sessions.
- Every job in every workflow sets
timeout-minutes: 10–15 for most jobs, 30 for jobs that resolve dependencies fresh (all-extra-resolves,anthropic-stacks,adk-stacksand the nightly jobs). - Every matrix sets
fail-fast: false, so each leg reports on its own. ci.ymlhas oneconcurrency:group per PR (per ref for pushes). A new push to a PR cancels the PR's previous run. Branch pushes are never cancelled.- Each
ci.ymljob that installs packages restores a pip cache, but only pushes tomainanddevelopsave one (#1005). A PR's own cache would be readable by that PR alone, so PRs restore whatdevelopsaved and add nothing to the repository's 10 GB cache quota. The key is the Python minor (not the patch, #1004), the job, and the job's constraints file, orpython/pyproject.tomlfor the fresh-resolve jobs. The cache stores downloaded wheels only, so a fresh-resolve job still resolves against the index. On #1002, a warm cache did not shorten the run (5m06s warm, 4m39s cold): the point is staying under the quota, not speed.
tests/unit/test_workflow_bounds.py fails on a job without a timeout of 30
minutes or less, a matrix without fail-fast: false, a ci.yml without the
concurrency group, or a ci.yml cache that a PR run can save. docs.yml and live-contract-check.yml deliberately
serialise their runs and never cancel one mid-flight.
Every workflow declares permissions: at the top, and elevated permissions go
only on the job that needs them. The publish workflows grant id-token: write
to the publish job alone, and docs.yml grants pages: write and
id-token: write to its deploy job alone; its build job, which runs
third-party npm code, has contents: read only (#757). Values from ${{ }}
are bound to env: and used as quoted shell variables, never interpolated
into a run: body. In place:
- Every
uses:is pinned to a full commit SHA with a# vX.Y.Zcomment, and every checkout setspersist-credentials: false. .github/dependabot.ymlsends weekly, grouped version updates for actions and forwebsite/npm, all targetingdevelop, with a 7-day cooldown on new releases. pip version updates stay off:lock-refresh.ymlmoves the lock (#763, ADR 0011), and the floors inpyproject.tomlare never raised on a schedule (§8.4). Security updates are on for every ecosystem..github/CODEOWNERScovers.github/,python/pyproject.toml,python/scripts/,python/src/donkey_kit/core/andpython/tests/fixtures/. The owners' review is required once the branch rules turn on "Require review from Code Owners".ci.yml'sworkflow-lintruns actionlint and zizmor over.github/.tests/unit/test_workflow_supply_chain.pyfails on an unpinneduses:, a checkout that keeps the token, an OIDC or Pages grant outside the publish and deploy jobs, or a missing Dependabot, CODEOWNERS or binary-image rule.- The publish workflows install
build,twineandhatchlingfromconstraints/release-py3.11.txtand build with--no-isolation(#763). docs.ymlalways buildsmain, so a manual run from another branch can't publish unreleased docs (#771).- The model call in
framework-release-watch.yml'sai-reviewjob reads its key from theai-reviewenvironment, which admits onlydevelop. The job hascontents: read, and the model gets read and fetch tools only; its output reaches an issue only throughalert, after validation (#1067).
The docs-site job in ci.yml builds the site on every PR that touches it,
so broken MDX, a bad import or a dead internal link fails there instead of in
the release deploy. Its docs-site nox session (convention 5) runs npm ci,
npm run typecheck, npm run lint, a production build with
DOCS_BASE_PATH=/donkey-development-kit (the project prefix the Pages deploy
uses), and npm run check:links over out/. Only the build sees
GITHUB_TOKEN, for the roadmap fetch. The
check is a job-level path test on website/** and the two docs workflows, not
a workflow-level paths: filter: a skipped workflow would never report
ci-ok, the required check (convention 2). The job always starts and its steps
no-op when the PR leaves the site alone. Pushes to main and develop always
run it. ci.yml and docs.yml both use Node 24, since Node 20 reached end of
life on 2026-04-30.
The version is declared once, as __version__ in
python/src/donkey_kit/__init__.py; pyproject.toml reads it from there.
Before either publish workflow uploads anything, its test job runs the suite
at that commit, and its build job checks that every built dist carries the
declared version (and, on publish-pypi.yml, that the tag equals
v<version> and a final Release carries a final X.Y.Z), then installs the
built wheel into a clean virtualenv and imports it. The upload generates PEP
740 attestations. The acceptance run against the TestPyPI build is a listed
gate in releasing.md: after the TestPyPI
upload, publish-testpypi.yml's acceptance job runs its offline groups
against the published version and reports them as the acceptance (published)
commit status, which the promotion PR shows (#1082). Still by hand: required
reviewers on the pypi environment are a repository setting, so no workflow
can enforce them, and the live acceptance groups are run and recorded on the
promotion PR by the maintainer.
The same rules apply in every Donkey-Development-Kit repo:
- Display name. The top-level
name:is what the Actions tab and the PR checks show. Write it in sentence case, keep acronyms upper case (CI, PR, SDK, UI, PyPI), and use at most four words. No kebab case, no parentheses, and no hosting detail such as "GitHub Pages". Start with a verb when the workflow does something (Deploy docs,Publish to PyPI). Use a noun phrase when it checks something (Secret scan,Nightly matrix). - Filename. Kebab case, and don't rename an existing one. PyPI Trusted
Publishing pins the
publish-*.ymlfilenames, docs and scripts refer to workflows by filename, and a rename splits the Actions run history. - Shared workflows. A workflow that exists in several repos has the same
filename and display name in each, such as
ci.yml/CIandsdk-alignment.yml/SDK alignment. - Job names. Renaming a workflow must not rename its jobs. Required status
checks match on job names:
ci-okhere, and<branch> ↔ SDK <branch>in the demos repo.
- Keep each job's header comment accurate. It is the job's spec. Say which issue added it and what it protects, and correct it in the same PR when the behaviour changes (#755 found comments promising behaviour that did not exist).
- A new PR-gating job installs from its own constraints file, gets a
_COMBOSentry incompile_constraints.pyand a place inci-ok'sneeds:. Its checks go in apython/noxfile.pysession of the same name, with its install line inCI_INSTALLS, and the job runs that session (convention 5). Change a check in the session, not in the workflow. A new job innightly-matrix.ymlgoes inalert'sneeds:. - A new scheduled workflow ships with its failure-alert step (convention 4).
- A new
uses:is pinned to the full commit SHA of a release, with the release's# vX.Y.Zas a trailing comment, and a new checkout setspersist-credentials: false(convention 7). Runactionlintandzizmor .github/locally before pushing a workflow change;workflow-lintruns both on the PR. - A change to which tests run where updates the test-surface table in
CONTRIBUTING.md§2. - A change to the dependency policy itself needs an ADR (see
adr/README.md).