Skip to content

Latest commit

 

History

History
631 lines (542 loc) · 36.1 KB

ci.md

File metadata and controls

631 lines (542 loc) · 36.1 KB

CI/CD: how the workflows are organised and the rules they follow

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

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)

ci.yml: the PR gate

The jobs, grouped by what they protect:

  • Layering and packaging. base-only installs only the base package and runs tests/unit, so a framework import leaking into a lower layer fails here. It then checks the built wheel in its own environment. all-extra-resolves dry-runs pip install ".[all]" on every supported Python (#697). new-dependencies checks that any direct dependency new to the PR exists on PyPI (#936). commit-identities fails a PR whose commits carry a placeholder git identity such as x <x@x> (#1048).
  • Static checks. typecheck-and-lint runs mypy, ruff check, lint-imports, the verification-claim and doc-link checkers, the fixture scrub check and vulture.
  • Behaviour. test is the Python 3.10–3.14 matrix and includes the local_gateway suite. openai-1x runs the same suites with openai<2, the 1.x range the llm extra's floor allows (#769). adapter-contract runs one leg per ADAPTERS extra 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. benchmark enforces the < 1 ms span overhead (BG §1.6) and trends it across runs. quickstart, langgraph-demo and openai-agents-demo run the documented examples against the local simulator. docs-llms-drift regenerates llms.txt and fails on any diff. docs-site builds the site and checks its internal links and anchors (#759).
  • Supply chain. pip-audit audits a fresh resolve of the base install and of .[all] for known vulnerabilities, blocking (#757). dependency-review flags a PR that adds a vulnerable dependency or one under a GPL/AGPL licence, and runs on PRs only. workflow-lint runs actionlint and zizmor over .github/ (convention 7).
  • The gate itself. ci-ok needs every other job and fails unless each one succeeded or was skipped. It is the one required status check (convention 2).

nightly-matrix.yml: the canary

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.

lock-refresh.yml: the lock bump

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:

  1. Add a LOCK_REFRESH_TOKEN repository 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.
  2. 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.

framework-release-watch.yml: one release at a time

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.yml PR may already carry it. If not, from python/ run python scripts/compile_constraints.py, then python scripts/generate_docs.py, then python 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: read and 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. Only alert writes, 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 only http(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 its ai-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:

  1. Create an ai-review environment in Settings → Environments. Under deployment branches, allow only develop; leave required reviewers off, because the daily run is unattended.
  2. Add the environment secret ANTHROPIC_API_KEY: gh secret set ANTHROPIC_API_KEY --env ai-review prompts for the value. tests/unit/test_ci_workflows.py holds ai-review as 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.

live-contract-check.yml: the gateway contract

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):

  1. Create a live-sandbox environment in Settings → Environments. Under deployment branches, allow only develop; 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.

  2. Add three environment secrets:

    Secret Value
    DONKEY_LLM_PROXY_URL The proxy base URL, https://<gateway-host>/<base-path>. No /v1, same shape as the SDK's .env
    DONKEY_LLM_PROXY_CLIENT_ID The client_id of a consumer app with a contract on that proxy
    DONKEY_LLM_PROXY_CLIENT_SECRET That app's client_secret

    gh secret set <NAME> --env live-sandbox prompts for each value, so the value never reaches your shell history.

  3. Check the setup with a manual run, without waiting for Monday: gh workflow run live-contract-check.yml --ref develop. A healthy run passes the openai-model-routing tests 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.
  • InvalidURL in every test, before any request. DONKEY_LLM_PROXY_URL is 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-drift issue.

Scenario maps

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.

Feature or fix (normal path)

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
Loading

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.

Hotfix (into main, then back to develop)

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
Loading

Never merge main back into develop as a branch merge — only the cherry-pick PR. Never push straight to main without a PR.

Error scenarios and recovery

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
Loading

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.

Conventions

1. PR CI resolves from a lock; the nightly run resolves fresh. In place (#982)

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).

2. One required status check: ci-ok. In place (#755)

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.

3. CI runs on develop, not just on PRs. In place (#755)

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.

4. Every scheduled workflow alerts on failure. Partly in place

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.

5. One task runner mirrors CI. In place (#765)

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 main run's artifact, and workflow-lint downloads actionlint and zizmor (each verified by sha256) onto the Linux runner. Locally the session takes both linters from PATH.
  • uses: actions stay steps. pip-audit's session writes the exact pin list that the pinned pypa/gh-action-pip-audit step then audits, and run locally it installs pip-audit and audits the list itself. dependency-review is 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-site installs nothing through CI_INSTALLS: its session runs npm ci itself, under --no-install too, 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.

6. Jobs are bounded and superseded runs are cancelled. In place (#761)

  • 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-stacks and the nightly jobs).
  • Every matrix sets fail-fast: false, so each leg reports on its own.
  • ci.yml has one concurrency: 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.yml job that installs packages restores a pip cache, but only pushes to main and develop save one (#1005). A PR's own cache would be readable by that PR alone, so PRs restore what develop saved 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, or python/pyproject.toml for 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.

7. Least-privilege workflows. Partly in place (#757, #771)

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.Z comment, and every checkout sets persist-credentials: false.
  • .github/dependabot.yml sends weekly, grouped version updates for actions and for website/ npm, all targeting develop, with a 7-day cooldown on new releases. pip version updates stay off: lock-refresh.yml moves the lock (#763, ADR 0011), and the floors in pyproject.toml are never raised on a schedule (§8.4). Security updates are on for every ecosystem.
  • .github/CODEOWNERS covers .github/, python/pyproject.toml, python/scripts/, python/src/donkey_kit/core/ and python/tests/fixtures/. The owners' review is required once the branch rules turn on "Require review from Code Owners".
  • ci.yml's workflow-lint runs actionlint and zizmor over .github/.
  • tests/unit/test_workflow_supply_chain.py fails on an unpinned uses:, 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, twine and hatchling from constraints/release-py3.11.txt and build with --no-isolation (#763).
  • docs.yml always builds main, so a manual run from another branch can't publish unreleased docs (#771).
  • The model call in framework-release-watch.yml's ai-review job reads its key from the ai-review environment, which admits only develop. The job has contents: read, and the model gets read and fetch tools only; its output reaches an issue only through alert, after validation (#1067).

8. The site is built on PRs, on a supported Node. In place (#759)

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.

9. Release gates are code, not prose. Partly in place (#767)

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.

10. Workflow names follow one style. In place (#1009)

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-*.yml filenames, 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 / CI and sdk-alignment.yml / SDK alignment.
  • Job names. Renaming a workflow must not rename its jobs. Required status checks match on job names: ci-ok here, and <branch> ↔ SDK <branch> in the demos repo.

Changing a workflow

  • 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 _COMBOS entry in compile_constraints.py and a place in ci-ok's needs:. Its checks go in a python/noxfile.py session of the same name, with its install line in CI_INSTALLS, and the job runs that session (convention 5). Change a check in the session, not in the workflow. A new job in nightly-matrix.yml goes in alert's needs:.
  • 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.Z as a trailing comment, and a new checkout sets persist-credentials: false (convention 7). Run actionlint and zizmor .github/ locally before pushing a workflow change; workflow-lint runs 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).