Skip to content
CommonLayer
Documentation menu
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

WorkflowRuns onNeeds secrets
cievery pull request, pushes to main, on demandno
securityevery pull request, pushes to main, weekly, on demandno
infrapull 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
projectspull 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 projectsno
docspull requests and pushes touching Markdown, on demandno
e2epull requests touching the application, nightly when main received a commit in the last day, on demandno
opsweekly restore drill, pull requests touching scripts/ops/no
releasetag 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, infra and projects cancel a running workflow when the same branch or pull request receives another push. On main this 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 infra runs 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 when infrastructure/helm/, infrastructure/docker/, the PostgreSQL or Valkey chart, the shared chart helpers, mk/infra.mk, infra.yml or 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.
  • projects checks 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 e2e run is skipped after a day without commits. A first job looks at the age of the newest commit on main; 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 push or pull_request workflow 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, projects and docs, 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), security and e2e can 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:

WorkflowCheck
cirust (fmt, clippy, tests, migrations)
cicargo deny (licenses, bans, sources, advisories)
cigenerated files are up to date (runs make generate and make check-generated)
ciweb (lint, typecheck, unit tests, build)
ciprotobuf (buf lint, buf breaking)
securitycargo-audit
securitypnpm-audit
securitysecret scanning (gitleaks)
securityTrivy filesystem and configuration scans
securityCodeQL (TypeScript)
securitydependency-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.

NameKindUsed byValue
CL_BASE_IMAGEvariablerelease (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_RUNNERvariableprojects, releaselabel 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 (every uses: 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.

FeatureNeeded byWithout it (current state)To enable
Code scanningsecurity (Trivy configuration SARIF, CodeQL), projects (Trivy and Grype SARIF), Scorecard resultsthe 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 runpublic 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 reviewsecurity / dependency-review on pull requeststhe step is skipped with a notice; cargo-audit, pnpm-audit and the Trivy filesystem scan still check the dependencies of every pull requestpublic repository: dependency graph on (default). Private: dependency graph plus GitHub Advanced Security / Code Security
Public visibilitysecurity / OpenSSF Scorecardthe 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 repositoriesmake the repository public
Native arm64 runnersprojects, releasesee CL_ARM64_RUNNER abovesee 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 to main do 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 stack healthy, make stack-seed ("seed marker recorded"; it stops the stack's worker and scheduler while it runs), then pnpm e2e against http://localhost:3000. The stack for this job trusts the web container's X-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: read stack.log in the e2e-failure artifact.
  • 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 artifact restore-drill-report must 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

CheckLocally
Workflow syntax and embedded shellscripts/dev/install-tools.sh actionlint then actionlint (runs shellcheck on run: blocks when shellcheck is installed: scripts/dev/install-tools.sh shellcheck)
Protobufbuf lint proto and buf breaking proto --against '.git#branch=main,subdir=proto'
Generated filesmake generate && make check-generated && git status --porcelain
Feature probesGITHUB_REPOSITORY=<owner>/<repo> .github/scripts/github-feature.sh code-scanning (with gh signed in)
projects for one treemake 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