On this page
Repository settings for the CI workflows
What the GitHub repository must provide for the workflows in .github/workflows/
to do their work: required status checks, branch protection, repository variables
and secrets, the GitHub features some jobs depend on, and Dependabot.
State observed on 2026-11-10: the repository is private and GitHub Advanced Security (GitHub Code Security) is not enabled — the code scanning API answers "Advanced Security must be enabled for this repository to use code scanning" and the dependency graph compare API answers 403. The workflows detect both and fall back (see Features); nothing has to be enabled for them to pass, but enabling these features restores what the fallbacks lose.
Workflows at a glance
| Workflow | Runs on | Needs secrets |
|---|---|---|
ci | every pull request, pushes to main, on demand | no |
security | every pull request, pushes to main, weekly, on demand | no |
infra | pull requests and pushes touching infrastructure, Compose, ops scripts, .github/; its kind job only when the chart, the platform Dockerfiles or the job itself changed; on demand (every job) | no |
projects | pull requests and pushes touching projects/, templates/, schemas/ or the project tooling; on demand. Exports each changed project tree to a directory outside the checkout and runs the project's own targets there; it does not release or rescan projects | no |
docs | pull requests and pushes touching Markdown, on demand | no |
e2e | pull requests touching the application, nightly when main received a commit in the last day, on demand | no |
ops | weekly restore drill, pull requests touching scripts/ops/ | no |
release | tag v<semver> | no (job token, GitHub OIDC); variable CL_BASE_IMAGE |
Releasing and rescanning the first-party projects (base image, nginx, Valkey,
PostgreSQL) is not done here: each project is a repository of its own
(commonlayer-org/<project>) with the template's workflows (validate, release on a tag
v<version>-r<revision>, daily security-scan), signed with that repository's
identity (decision 0049).
Their trees stay under projects/ until those repositories exist. The settings each
project repository needs (the variable CL_BASE_REPOSITORY until the base digest is
set, optionally COMMONLAYER_API_URL, code scanning, the CODEOWNERS team) are in
its README.
Every workflow starts from permissions: {} and grants each job only what it uses;
the only token is the automatic GITHUB_TOKEN. Because no workflow reads repository
secrets, pull requests from Dependabot (which get no secrets and a read-only token
unless a job asks for more) run the same checks as anyone else's.
Runner minutes
The workflows are written to spend runner time only on the newest commit and only on what changed:
- A newer push replaces the run in progress.
ci,security,docs,infraandprojectscancel a running workflow when the same branch or pull request receives another push. Onmainthis means a commit that was superseded before its run finished shows a cancelled run; the newest commit is always checked in full. The release workflow never cancels. - The kind job of
infraruns only for its inputs. It builds the three platform images in release mode and installs the chart on kind, about an hour. A first job (what changed,.github/scripts/changed.sh) lets it run wheninfrastructure/helm/,infrastructure/docker/, the PostgreSQL or Valkey chart, the shared chart helpers,mk/infra.mk,infra.ymlor the scripts the job uses changed, and skips it otherwise (for example for an edit to another workflow, a dashboard or an OpenTofu module). When the base commit is unknown the job runs. projectschecks only the project trees that changed, and every tree when its own tooling changed (projects.yml,project-matrix.sh,export-project.sh,check-project-trees.py,check-template-drift.sh). A change of a shared file changes every tree's copy, so it selects every tree by path; a change of the base tree selects only the base. There is no scheduled run: each project's own repository rebuilds weekly and rescans daily.- The nightly
e2erun is skipped after a day without commits. A first job looks at the age of the newest commit onmain; the stack is built and the browser suite runs only when it is younger than 26 hours. Scheduled runs are not affected by[skip ci]: a commit pushed with the marker is still tested by the next nightly run. - Work in progress is pushed without a run. GitHub starts no
pushorpull_requestworkflow when the newest commit of a push has[skip ci]in its message. Use it for the intermediate pushes of a series and leave it out of the last commit, so the series is checked once, as a whole. A commit pushed this way is unchecked until that run; do not tag a release from one. On a pull request the required checks stay pending while the newest commit skips them, so the last push before review must not carry the marker. - A run started by a push looks only at that push. The path filters of
infra,projectsanddocs, the choice of projects and the kind job compare with the commit the branch pointed to before the push. After pushes that skipped the workflows, a last push that does not touch their files leaves the earlier changes unchecked. Then start the workflow by hand (Actions → the workflow → Run workflow):ci,docs,infra(runs every job, kind included),projects(every tree, or the names given),securityande2ecan all be started this way.
Run the same checks locally before pushing (see Local equivalents).
Required status checks
Require the checks of the two workflows that run on every pull request. The names are the job names GitHub shows as check runs:
| Workflow | Check |
|---|---|
| ci | rust (fmt, clippy, tests, migrations) |
| ci | cargo deny (licenses, bans, sources, advisories) |
| ci | generated files are up to date (runs make generate and make check-generated) |
| ci | web (lint, typecheck, unit tests, build) |
| ci | protobuf (buf lint, buf breaking) |
| security | cargo-audit |
| security | pnpm-audit |
| security | secret scanning (gitleaks) |
| security | Trivy filesystem and configuration scans |
| security | CodeQL (TypeScript) |
| security | dependency-review |
dependency-review passes with a notice while dependency review is unavailable
(see below); keep it required so it starts gating as soon as the feature is enabled.
Do not make the checks of infra, projects, docs, e2e or ops required.
These workflows are filtered by paths, so a pull request that does not touch their
files never reports them, and a required check that never reports blocks the merge
forever. Their failures are still visible on the pull requests that trigger them;
reviewers should not merge such a pull request while one is red. (The projects
check names are also generated per project, version line and platform.) If they
must be enforced, the workflows have to run on every pull request and decide inside
a first job what to do, so that skipped jobs report success.
Branch protection for main
A repository ruleset (Settings → Rules → Rulesets) targeting the default branch:
- Require a pull request before merging; at least one approval; dismiss stale
approvals on new commits; require review from code owners (
.github/CODEOWNERS; its placeholder teams must first be replaced with real ones). - Require status checks to pass, with the checks listed above, and require branches to be up to date before merging.
- Require conversation resolution; block force pushes; restrict deletions.
- Require linear history (the coordinator merges with squash or rebase).
- Tag ruleset for
v*: restrict creation to maintainers — pushing such a tag publishes the platform images and a GitHub release.
Repository variables and secrets
Settings → Secrets and variables → Actions.
| Name | Kind | Used by | Value |
|---|---|---|---|
CL_BASE_IMAGE | variable | release (all platform image builds) | the released CommonLayer base image pinned by digest, registry.commonlayer.org/base:<tag>@sha256:<digest>. Required before the first v* tag; the release stops with an error while it is unset or not pinned |
CL_ARM64_RUNNER | variable | projects, release | label of a native arm64 runner. On a public repository GitHub's ubuntu-24.04-arm is used when this is unset. On a private repository set it to an arm64 runner available to the repository (for example an arm64 larger runner, or ubuntu-24.04-arm if GitHub offers it to the repository). Unset on a private repository: projects builds and tests linux/amd64 only and says so in a notice; release stops with an error, because a release must contain linux/arm64 |
No repository or environment secrets are needed. Signing and provenance use
keyless GitHub OIDC (id-token: write in the release jobs); GHCR pushes use the job
token (packages: write).
Actions settings (Settings → Actions → General):
- Workflow permissions: read repository contents (the default token stays read-only; jobs request more explicitly).
- Allow only selected actions:
actions/*,github/codeql-action/*,docker/*,pnpm/action-setup,Swatinem/rust-cache,taiki-e/install-action,ossf/scorecard-action,lycheeverse/lychee-action; and require actions to be pinned to a full-length commit SHA (everyuses:already is). - Packages: the first push of each image or chart to GHCR creates a package owned by the organization; link it to this repository (Package settings → Manage Actions access) so later workflow runs can push to it.
Features that depend on the repository
The probe-and-fallback design is recorded in ADR-0044. Until code scanning is enabled, nobody is notified of scanner results: they are only in workflow artifacts and job summaries.
| Feature | Needed by | Without it (current state) | To enable |
|---|---|---|---|
| Code scanning | security (Trivy configuration SARIF, CodeQL), projects (Trivy and Grype SARIF), Scorecard results | the job asks the API first (.github/scripts/github-feature.sh code-scanning), writes a notice, attaches the SARIF files to the run as an artifact (sarif-*, or inside the project reports) and lists the results in the job summary, with error-level results as annotations. Gates are unchanged: they are separate steps (Trivy filesystem scan, project scan gate), as with code scanning, where alerts do not fail a run | public repository: nothing. Private: GitHub Advanced Security or GitHub Code Security for the repository (Settings → Code security → Code scanning). Uploads also need actions: read and security-events: write, which the jobs request |
| Dependency review | security / dependency-review on pull requests | the step is skipped with a notice; cargo-audit, pnpm-audit and the Trivy filesystem scan still check the dependencies of every pull request | public repository: dependency graph on (default). Private: dependency graph plus GitHub Advanced Security / Code Security |
| Public visibility | security / OpenSSF Scorecard | the OpenSSF Scorecard prerequisites job logs that the repository is private and OpenSSF Scorecard is skipped. Scorecard reads the repository through GitHub APIs a private repository's workflow token cannot query (its first run failed with "Resource not accessible by integration") and publishes results only for public repositories | make the repository public |
| Native arm64 runners | projects, release | see CL_ARM64_RUNNER above | see CL_ARM64_RUNNER above |
First-run checklist
Workflows that have never completed on a runner, with what to check on their first run:
-
e2e: never run on a runner. It runs on pull requests touching the application, nightly at 02:41 UTC, or by manual dispatch (Actions → e2e → Run workflow); pushes tomaindo not start it. Unverified until then. Expect, in order: the three platform images (about 75 min on a hosted runner; job timeout 180 min),make stackhealthy,make stack-seed("seed marker recorded"; it stops the stack's worker and scheduler while it runs), thenpnpm e2eagainsthttp://localhost:3000. The stack for this job trusts the web container'sX-Forwarded-For(CL_STACK_TRUSTED_PROXIES,CL_STACK_WEB_BEHIND_TRUSTED_PROXY), so each test's own client address counts for the per-address limits; the web image includes the component gallery (CL_WEB_DEV_PAGES=1). If many tests fail with 429, the forwarded address is not reaching the API: readstack.login thee2e-failureartifact. -
infra/ kind: install,helm test, upgrade,helm test(see docs/status/WP-70g.md for the latest run). -
ops(weekly restore drill against the stack): the report artifactrestore-drill-reportmust say "Result: passed".
Dependabot
.github/dependabot.yml covers cargo, npm (apps/web), GitHub Actions, Docker
(platform and project Dockerfiles), Docker Compose and Terraform/OpenTofu providers.
Each ecosystem groups its updates (minor and patch in one pull request, majors in
another; Actions, images and providers in one), limits open pull requests to two or
three, and waits a few days after a release before proposing it. Security updates are
separate (Settings → Code security → Dependabot security updates) and are not delayed
or limited by this file. @types/node majors are ignored: raise it together with the
Node.js runtime of CI and the web image.
Dependabot pull requests run ci and security in full, plus infra, projects,
docs and e2e when they touch those paths — none of it needs secrets. Settings →
Code security → "Grouped security updates" keeps security pull requests grouped too.
Local equivalents
| Check | Locally |
|---|---|
| Workflow syntax and embedded shell | scripts/dev/install-tools.sh actionlint then actionlint (runs shellcheck on run: blocks when shellcheck is installed: scripts/dev/install-tools.sh shellcheck) |
| Protobuf | buf lint proto and buf breaking proto --against '.git#branch=main,subdir=proto' |
| Generated files | make generate && make check-generated && git status --porcelain |
| Feature probes | GITHUB_REPOSITORY=<owner>/<repo> .github/scripts/github-feature.sh code-scanning (with gh signed in) |
projects for one tree | make project-check PROJECT=<base|nginx|valkey|postgresql|template> (exports the tree outside the checkout and runs its own targets there); make projects-validate for the schema tests, the drift check, the self-contained check and every tree's make validate |