Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .config/lefthook.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,16 @@ pre-commit:
glob: '.github/{rulesets/**,CODEOWNERS,workflows/*.yml}'
run: task check:rulesets

# Every copy of the Conventional Commits vocabulary, this file included --
# the commit-msg hook below inlines the types a fifth time. CI checks them
# too; catching a mismatch here saves a round trip, and the prefixes in
# dependabot.yml are only ever exercised by a bot nobody watches.
- name: commits
glob:
- '.github/{conventional-commits.yaml,dependabot.yml,CONTRIBUTING.md,workflows/lint-pr.yml}'
- '.config/lefthook.yml'
run: task check:commits

- name: markdown
glob: '**/*.md'
run: task check:md
Expand Down
11 changes: 10 additions & 1 deletion .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,11 @@ Types: `feat`, `fix`, `perf`, `docs`, `chore`, `refactor`, `test`, `ci`,
`build`, `style`, `revert`.

Scopes: `component`, `blueprint`, `listing`, `conformance`, `tools`, `ci`,
`devcontainer`, `docs`, `repo`, `deps`.
`devcontainer`, `docs`, `repo`, `deps`, `deps-dev`.

`deps` and `deps-dev` are Dependabot's: a dependency update arrives as
`build(deps):`, `build(deps-dev):`, or `ci(deps):`. See
[ADR 0016](../docs/adr/0016-dependency-update-policy.md).

Releases are cut by [release-please](https://github.com/googleapis/release-please)
from these messages. A `feat(component):` commit produces a `component/v1.x.0`
Expand All @@ -155,6 +159,11 @@ Every commit must carry a `Signed-off-by` trailer:
git commit -s -m "feat(component): add restartPolicy"
```

The trailer's name and email must match the commit's author. One exception, for
a GitHub App: an app signs under its operator's address rather than the noreply
address its commits are authored from, so a bot's sign-off is matched on name
alone. See [ADR 0016](../docs/adr/0016-dependency-update-policy.md).

## Proposing a structural change

Changes to the repository architecture, the release model, or the family
Expand Down
29 changes: 25 additions & 4 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,52 @@
# - devcontainers : bumps Features in devcontainer.json AND refreshes
# .devcontainer/devcontainer-lock.json (digest pins).
# - github-actions : bumps the actions pinned in .github/workflows/.
# - bun : bumps tools/ dependencies (ajv, yaml, biome).
# - bun : bumps tools/ dependencies.
#
# Every `prefix` below is a Conventional Commits TYPE, optionally with a scope
# appended by `include: 'scope'` -- Dependabot emits the prefix as the type, so
# a value that is only a valid scope produces a message no gate will accept.
# `task check:commits` holds these against .github/conventional-commits.yaml;
# it was added because `prefix: 'devcontainer'` sat here wrong for months.
#
# `build`, `ci` and `chore` are all hidden in .github/release-please/config.json,
# so no dependency bump can cut a specification release.
#
# See docs/adr/0016-dependency-update-policy.md.
version: 2
updates:
- package-ecosystem: 'devcontainers'
directory: '/'
schedule:
interval: 'weekly'
commit-message:
prefix: 'devcontainer'
prefix: 'build'
include: 'scope' # -> build(deps): bump ...

- package-ecosystem: 'github-actions'
directory: '/'
schedule:
interval: 'weekly'
commit-message:
prefix: 'ci'
include: 'scope' # -> ci(deps): bump ...

- package-ecosystem: 'bun'
directory: '/tools'
schedule:
interval: 'weekly'
commit-message:
prefix: 'chore'
prefix-development: 'chore'
prefix: 'build'
prefix-development: 'build'
include: 'scope' # -> build(deps-dev): bump ...
groups:
tooling:
patterns:
- '*'
# wrangler is the only dependency in this repository handed a
# credential, and GOVERNANCE.md -> Tooling dependencies rests its
# guarantee on that pin moving only through a diff someone opened
# deliberately. Inside the group it would arrive folded into somebody
# else's lockfile churn, so it is kept out and reviewed on its own.
exclude-patterns:
- 'wrangler'
31 changes: 29 additions & 2 deletions .github/workflows/dco.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,21 @@ name: DCO
# Developer Certificate of Origin sign-off on every commit. Until this workflow
# existed, nothing checked it — the requirement lived entirely in prose, which
# is the same as not having it.
#
# One narrow exception, for a GitHub App. The DCO certifies provenance by a
# legal person, and for an app that person is its operator, not the app: a
# Dependabot commit is authored as
# `dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>` and
# signed off by GitHub, Inc. as `dependabot[bot] <support@github.com>`. The
# names agree; the addresses cannot, because neither field is the other's to
# set. Demanding they match rejected a sign-off that was present and valid, and
# left every dependency update mergeable only through the org-admin bypass —
# the habit docs/adr/0015-selective-code-owner-review.md was written to end.
# See docs/adr/0016-dependency-update-policy.md.
#
# This is a paper trail, not a security control, and the exception widens
# nothing: anyone who would forge a bot's sign-off can already author under a
# bot's address, or simply type the trailer themselves.

on:
pull_request:
Expand Down Expand Up @@ -41,13 +56,25 @@ jobs:

for sha in $commits; do
author="$(git show -s --format='%an <%ae>' "$sha")"
name="$(git show -s --format='%an' "$sha")"
email="$(git show -s --format='%ae' "$sha")"
subject="$(git show -s --format='%s' "$sha")"

# Trailers only — a "Signed-off-by" written into the body is not a
# sign-off, and %(trailers) is what git itself considers one.
if git show -s --format='%(trailers:key=Signed-off-by,valueonly)' "$sha" \
| grep -qxF "$author"; then
trailers="$(git show -s --format='%(trailers:key=Signed-off-by,valueonly)' "$sha")"

# A GitHub App: both halves of the identity must say so, and the
# trailer still has to name the same author. Only the address is
# allowed to differ, because only the address is out of its hands.
case "$name" in *'[bot]') bot=1 ;; *) bot=0 ;; esac
case "$email" in *'[bot]@users.noreply.github.com') ;; *) bot=0 ;; esac

if printf '%s\n' "$trailers" | grep -qxF "$author"; then
echo " ✓ ${sha:0:8} ${subject}"
elif [ "$bot" -eq 1 ] \
&& printf '%s\n' "$trailers" | sed -E 's/ *<[^>]*>$//' | grep -qxF "$name"; then
echo " ✓ ${sha:0:8} ${subject} — app sign-off by ${name}"
else
echo "::error::${sha:0:8} (${subject}) has no Signed-off-by matching its author ${author}"
failed=1
Expand Down
76 changes: 76 additions & 0 deletions .github/workflows/dependabot-title.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
name: Dependabot Title

# Dependabot capitalises the subject of the messages it writes -- `build(deps):
# Bump ...` -- which is exactly what the `subjectPattern` in lint-pr.yml
# rejects. That capital is not configurable: dependabot-core decides it from a
# heuristic over recent commit messages, and on this repository it lands on
# "capitalise" against a history that is entirely lowercase.
#
# So the choice was to exempt the bot from the title rule or to fix the title.
# Exempting it would leave `Bump` in the log on `main` forever and carve a hole
# in a rule that otherwise has none. This lowercases the first letter instead,
# and the rule stays single. See docs/adr/0016-dependency-update-policy.md.
#
# Three things about this file are load-bearing:
#
# 1. `pull_request_target`, because a `pull_request` run on a fork-context
# Dependabot pull request gets a read-only token and cannot retitle it.
# That means this workflow runs FROM THE BASE BRANCH WITH A WRITE TOKEN,
# so it must never check out, build, or execute anything from the head --
# and it does not: there is no checkout step at all.
# 2. The title reaches the shell through `env:`, never through a `${{ }}`
# expansion inside `run:`. Interpolating pull-request-controlled text into
# a privileged shell is the textbook injection, whoever opened it.
# 3. It is idempotent, which is what makes listening to `edited` safe: our own
# edit re-fires the event, the second run finds nothing to change and
# stops. There is no loop.
#
# It is a NORMALISER, not a gate -- `Conventional PR title` in lint-pr.yml is
# still the thing that judges the result. Expect one transient red on `opened`:
# that check and this job start together, this one edits the title, and the
# edit re-runs the check green.

on:
pull_request_target:
types: [opened, reopened, edited]

permissions:
pull-requests: write

# Queue rather than cancel: a cancelled retitle would leave the pull request
# with the title the check has already rejected.
concurrency:
group: dependabot-title-${{ github.event.pull_request.number }}
cancel-in-progress: false

jobs:
subject:
name: Lowercase subject
if: github.event.pull_request.user.login == 'dependabot[bot]'
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Lowercase the first letter of the subject
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_TITLE: ${{ github.event.pull_request.title }}
run: |
set -euo pipefail

# Case only, and only after a conventional-SHAPED prefix. Whether
# the type and scope in that prefix are real is the title check's
# business, not this job's: rewriting them here would paper over a
# broken .github/dependabot.yml, which is the other half of this.
if [[ ! "$PR_TITLE" =~ ^([a-z]+(\([a-z][a-z-]*\))?!?:[[:space:]])([A-Z].*)$ ]]; then
echo "Nothing to lowercase: ${PR_TITLE}"
exit 0
fi

prefix="${BASH_REMATCH[1]}"
subject="${BASH_REMATCH[3]}"
fixed="${prefix}${subject,}"

echo " ${PR_TITLE}"
echo "→ ${fixed}"
gh pr edit "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --title "$fixed"
7 changes: 6 additions & 1 deletion GOVERNANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,12 @@ lockfile rather than fetched at deploy time, so the code that receives the token
changes only through a diff someone opened deliberately — never through a
resolution that moved on its own. `tools/` is not a CODEOWNERS path, so that
diff is not gated on a review; the exact pin plus the lockfile is what carries
the guarantee. Nothing else in `tools/` holds a secret, and nothing published
the guarantee. For the same reason it is excluded from the grouped Dependabot
update in [`.github/dependabot.yml`](.github/dependabot.yml): folded into four
other packages' lockfile churn it would arrive as a diff nobody opened *for
it*, which is not the deliberate one this paragraph promises. It always comes
as its own pull request. See
[ADR 0016](docs/adr/0016-dependency-update-policy.md). Nothing else in `tools/` holds a secret, and nothing published
derives from wrangler either.

## Security
Expand Down
Loading