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
5 changes: 5 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,8 @@
- [ ] Root commands still work, or the PR explains the migration path.
- [ ] No root npm dependencies were added.
- [ ] Generated files are ignored or intentionally committed.

## Sign-off

- [ ] Every commit is signed off (`git commit -s`) per the
[Developer Certificate of Origin](https://github.com/ContextCake/context-cake/blob/main/DCO).
146 changes: 146 additions & 0 deletions .github/scripts/check-dco-test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
#!/usr/bin/env bash
# Proves check-dco.sh accepts signed work and rejects unsigned work, including
# the bot, merge-commit, and moved-base cases the gate has to get right.
set -euo pipefail

check="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/check-dco.sh"
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT

expect_ok() {
local label=$1 base_ref=$2 out
if ! out="$(bash "$check" "$base_ref" HEAD 2>&1)"; then
echo "DCO check test: ${label} — a valid branch was rejected" >&2
printf '%s\n' "$out" >&2
exit 1
fi
printf '%s\n' "$out"
}

expect_reject() {
local label=$1 base_ref=$2 out
if out="$(bash "$check" "$base_ref" HEAD 2>&1)"; then
echo "DCO check test: ${label} — an unsigned branch was accepted" >&2
printf '%s\n' "$out" >&2
exit 1
fi
printf '%s\n' "$out"
}

assert_contains() {
local label=$1 needle=$2 haystack=$3
if ! printf '%s\n' "$haystack" | grep -qF -e "$needle"; then
echo "DCO check test: ${label} — output missing '${needle}'" >&2
printf '%s\n' "$haystack" >&2
exit 1
fi
}

mkdir -p "$tmpdir/repo"
cd "$tmpdir/repo"
git init --quiet -b main
git config user.name "Ada Contributor"
git config user.email "ada@example.com"
git config commit.gpgsign false

echo base > file.txt
git add file.txt
git commit --quiet -s -m "base commit"
base="$(git rev-parse HEAD)"

branch() { git checkout --quiet -B "$1" "$base"; }

# A signed commit passes.
branch signed
echo signed >> file.txt
git commit --quiet -a -s -m "signed change"
out="$(expect_ok "signed commit" "$base")"
assert_contains "signed commit" "DCO OK" "$out"

# An unsigned commit is rejected, with the expected trailer in the message.
branch unsigned
echo unsigned >> file.txt
git commit --quiet -a -m "unsigned change"
out="$(expect_reject "unsigned commit" "$base")"
assert_contains "unsigned commit" \
"Signed-off-by: Ada Contributor <ada@example.com>" "$out"
assert_contains "unsigned commit" "git rebase --signoff" "$out"

# A sign-off that does not match the commit author is rejected.
branch mismatch
echo mismatch >> file.txt
git commit --quiet -a -s --author="Bo Other <bo@example.com>" -m "mismatched sign-off"
out="$(expect_reject "mismatched sign-off" "$base")"
assert_contains "mismatched sign-off" \
"Signed-off-by: Bo Other <bo@example.com>" "$out"

# Trailer matching is case-insensitive.
branch lowercase
echo lowercase >> file.txt
git commit --quiet -a -m "lowercase trailer

signed-off-by: Ada Contributor <ada@example.com>"
expect_ok "lowercase trailer" "$base" > /dev/null

# A bot cannot run `git commit -s`, so bot-authored commits are skipped in the
# unenforced local-heuristic mode (no DCO_BOT_SHAS set).
branch bot
echo bot >> file.txt
git commit --quiet -a \
--author="dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>" \
-m "bump a dependency"
out="$(expect_ok "bot author" "$base")"
assert_contains "bot author" "(bot author)" "$out"

# CI mode (DCO_BOT_SHAS set): a local author name is attacker-controlled
# before push — a human faking "[bot]" in their own commit must NOT skip the
# check just because the name heuristic would have matched it.
branch spoofed_bot
echo spoofed >> file.txt
git commit --quiet -a \
--author="attacker[bot] <attacker@example.com>" \
-m "unsigned commit pretending to be a bot"
if out=$(DCO_BOT_SHAS="" bash "$check" "$base" HEAD 2>&1); then
echo "DCO check test: spoofed bot author — an unsigned branch was accepted" >&2
printf '%s\n' "$out" >&2
exit 1
fi
assert_contains "spoofed bot author" \
"Signed-off-by: attacker[bot] <attacker@example.com>" "$out"

# CI mode: a commit whose SHA the caller verified via the GitHub API skips,
# regardless of what its local author name says.
branch verified_bot
echo verified >> file.txt
git commit --quiet -a --author="Some Human <human@example.com>" \
-m "commit the API says is bot-authored"
verified_sha="$(git rev-parse HEAD)"
if ! out=$(DCO_BOT_SHAS="$verified_sha" bash "$check" "$base" HEAD 2>&1); then
echo "DCO check test: API-verified bot sha — a verified bot commit was rejected" >&2
printf '%s\n' "$out" >&2
exit 1
fi
assert_contains "API-verified bot sha" "(bot author)" "$out"

# Merge commits are generated, not authored, so they are skipped.
branch feature
echo feature >> file.txt
git commit --quiet -a -s -m "feature work"
branch merged
echo parallel > other.txt
git add other.txt
git commit --quiet -s -m "parallel work"
git merge --quiet --no-ff --no-edit feature
expect_ok "unsigned merge commit" "$base" > /dev/null

# When the base branch has moved ahead, its own commits are not re-checked.
git checkout --quiet -B main "$base"
echo main-only > main-only.txt
git add main-only.txt
git commit --quiet -m "unsigned commit on the base branch"
branch behind-base
echo behind >> file.txt
git commit --quiet -a -s -m "signed work off an older base"
expect_ok "base branch ahead" main > /dev/null

echo "DCO check-script test passed"
109 changes: 109 additions & 0 deletions .github/scripts/check-dco.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
#!/usr/bin/env bash
# Verify that every commit in a range carries a Developer Certificate of Origin
# sign-off trailer matching its author. Plain bash + git, no third-party action:
# the contributor gate should not add a supply-chain surface the engine refuses
# to add for itself.
#
# Usage: check-dco.sh <base-ref> [head-ref]
#
# Merge commits are skipped — they are generated, not authored.
#
# Commits authored by a bot are skipped because a bot cannot execute
# `git commit -s`, matching the default behavior of GitHub's DCO app and
# keeping Dependabot PRs mergeable. Which commits count as "bot" depends on
# whether DCO_BOT_SHAS is set:
# - Set (CI): a space-separated allowlist of full SHAs the caller already
# verified via the GitHub API (commit author.type == "Bot"). Only those
# exact commits skip. A contributor's local git author name is theirs to
# set to anything before they ever push — `git config user.name
# 'x[bot]'` costs nothing — so trusting a name pattern here would let
# anyone opt their own commits out of certification.
# - Unset (local/manual runs): falls back to an "ends with [bot]" name
# heuristic, for developer convenience only. Nothing is enforced by a
# local run, so the spoofability doesn't matter there.
set -euo pipefail

base=${1:?usage: check-dco.sh <base-ref> [head-ref]}
head=${2:-HEAD}

# A PR-merge or shallow checkout may not contain the base commit yet.
if ! git cat-file -e "${base}^{commit}" 2>/dev/null; then
git fetch --no-tags --quiet origin "$base" 2>/dev/null || true
fi

if ! git cat-file -e "${base}^{commit}" 2>/dev/null; then
echo "Could not resolve base commit ${base}." >&2
exit 1
fi

# Compare against the fork point so commits already on the base branch are not
# re-checked when the base has moved ahead.
range_start="$(git merge-base "$base" "$head" 2>/dev/null || echo "$base")"

is_bot_commit() {
local sha=$1 author_name=$2

if [ -n "${DCO_BOT_SHAS+set}" ]; then
case " ${DCO_BOT_SHAS} " in
*" ${sha} "*) return 0 ;;
*) return 1 ;;
esac
fi

case "$author_name" in
*'[bot]') return 0 ;;
*) return 1 ;;
esac
}

failed=0
checked=0
skipped=0

while IFS= read -r sha; do
[ -n "$sha" ] || continue

author_name="$(git show -s --format='%an' "$sha")"
author_email="$(git show -s --format='%ae' "$sha")"
subject="$(git show -s --format='%s' "$sha")"
short="$(git rev-parse --short "$sha")"

if is_bot_commit "$sha" "$author_name"; then
echo "skip ${short} ${subject} (bot author)"
skipped=$((skipped + 1))
continue
fi

checked=$((checked + 1))
expected="Signed-off-by: ${author_name} <${author_email}>"

if git show -s --format='%B' "$sha" | grep -qiF -e "$expected"; then
echo "ok ${short} ${subject}"
else
echo "FAIL ${short} ${subject}"
echo " missing trailer: ${expected}"
failed=$((failed + 1))
fi
done < <(git rev-list --no-merges "${range_start}..${head}")

if [ "$failed" -gt 0 ]; then
echo >&2
echo "${failed} commit(s) are missing a sign-off matching their author." >&2
echo >&2
echo "Every commit must certify the Developer Certificate of Origin (see ./DCO)" >&2
echo "with a trailer that matches the commit author exactly:" >&2
echo >&2
echo " Signed-off-by: Your Name <your@email>" >&2
echo >&2
echo "To fix the most recent commit:" >&2
echo " git commit --amend --signoff --no-edit" >&2
echo >&2
echo "To fix every commit on this branch:" >&2
echo " git rebase --signoff ${base}" >&2
echo >&2
echo "Then update the pull request:" >&2
echo " git push --force-with-lease" >&2
exit 1
fi

echo "DCO OK — ${checked} commit(s) signed off, ${skipped} skipped."
55 changes: 55 additions & 0 deletions .github/workflows/dco.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: DCO

on:
pull_request:
branches:
- main

permissions:
contents: read
pull-requests: read

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
sign-off:
name: sign-off
runs-on: ubuntu-latest
timeout-minutes: 5

steps:
- name: Checkout
uses: actions/checkout@v7
with:
# Full history: the check compares against the fork point with the base
# branch, so both parents of the pull request merge ref must be
# reachable.
fetch-depth: 0

- name: Test the check script
run: bash .github/scripts/check-dco-test.sh

- name: Identify GitHub-verified bot commits
id: bots
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
# A commit's local git author name is whatever the contributor set
# before pushing — never trust it for a security decision. The API's
# per-commit `author` object instead reflects GitHub's own account
# lookup, so `author.type == "Bot"` cannot be spoofed the same way.
shas="$(gh api "repos/${REPO}/pulls/${PR_NUMBER}/commits" --paginate \
--jq '.[] | select(.author != null and .author.type == "Bot") | .sha' \
| tr '\n' ' ')"
echo "shas=${shas}" >> "$GITHUB_OUTPUT"

- name: Verify sign-off on every commit
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
DCO_BOT_SHAS: ${{ steps.bots.outputs.shas }}
run: bash .github/scripts/check-dco.sh "$BASE_SHA" "$HEAD_SHA"
44 changes: 43 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,53 @@ npm --prefix apps/site run build
manifest.
- Keep generated files out of Git unless a spec says otherwise.

## Sign Your Work

ContextCake uses the [Developer Certificate of Origin](DCO) (DCO). Signing off
is how you certify that you wrote the contribution, or otherwise have the right
to submit it under this project's MIT license. There is no CLA to sign and no
copyright assignment.

Add the trailer by committing with `-s`:

```bash
git commit -s -m "fix(core): date sections from git history"
```

That appends a line matching your Git identity:

```text
Signed-off-by: Your Name <your@email>
```

Every commit in a pull request needs one, and it must match that commit's
author. Use your real name and an address you can be reached at; the sign-off
becomes a permanent part of the public history.

To sign off work you already committed:

```bash
git commit --amend --signoff --no-edit # the most recent commit
git rebase --signoff main # every commit on the branch
git push --force-with-lease
```

CI enforces this on every pull request. To check a branch before pushing:

```bash
bash .github/scripts/check-dco.sh main
```

Merge commits are skipped, as are commits authored by bots — Dependabot cannot
run `git commit -s`.

## Validation

Run the smallest relevant checks while developing, then run the broader gates
before opening a PR. `npm test` starts a local playground server, so it needs a
local environment where binding to `127.0.0.1` is allowed.

For AI contributors: state which commands you ran, include failures honestly,
and avoid broad rewrites unrelated to the issue.
and avoid broad rewrites unrelated to the issue. Agent-assisted commits are
signed off by the person submitting them — the DCO is a human certification,
so use the submitter's identity, not the agent's.
Loading