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
26 changes: 24 additions & 2 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ This repository follows the Bugs5382 standard workflow.
1. Open an issue from a template (free-form issues are disabled). For multi-step work,
use a parent issue with ordered sub-issues, and put it on the active milestone.
2. Branch from `main` as `<type>/<issue#>-<slug>` (for example `feat/12-add-listener`).
3. Commit using Conventional Commits (`type(scope): description`). No attribution
trailers, no emoji in source or commit messages (emoji are fine in Markdown).
3. Commit using Conventional Commits (`type(scope): description`) and sign off every
commit with `git commit -s` (see [Developer Certificate of Origin](#developer-certificate-of-origin)).
The sign-off is the only trailer: no attribution trailers, and no emoji in source or
commit messages (emoji are fine in Markdown).
4. Open a PR with a Conventional Commit title. The autolabeler sets the category label
from the title; fill the PR template, reference the issue (`Closes #N`), and add a
closing summary before merge.
Expand All @@ -19,6 +21,26 @@ Keep one concern per PR, even small ones. When editing GitHub Actions workflows,
plain identifier (a letter or `_`, then alphanumerics/`-`/`_`); put emoji and display text in the
job's `name:`. The Actionlint check enforces this.

## Developer Certificate of Origin

Contributions are accepted under the [Developer Certificate of Origin](https://developercertificate.org/)
(DCO) 1.1. Signing off a commit certifies that you wrote the change, or otherwise have the
right to submit it under this repository's licence (Apache License 2.0), and that the
contribution and your sign-off are public and kept permanently. The organization's
[contributing guide](https://github.com/CryptOS-PKI/.github/blob/main/CONTRIBUTING.md#developer-certificate-of-origin)
quotes the full certificate.

Sign off with `git commit -s`. Git builds the trailer from your `user.name` and
`user.email`, and it has to match the commit's author:

```text
Signed-off-by: Jane Doe <jane@example.com>
```

The DCO check fails a pull request when any commit, other than a merge commit, has no
matching sign-off. To sign off commits you have already pushed, run
`git rebase --signoff origin/main` and force-push the branch.

## Local setup

Install the governance hooks once per clone: `bash .claude/hooks/install.sh`. They
Expand Down
67 changes: 67 additions & 0 deletions .github/workflows/job-dco.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
name: DCO
# Developer Certificate of Origin: every commit in a pull request must carry a
# `Signed-off-by:` trailer (`git commit -s`) naming the commit's author, with
# the same name and email (compared case-insensitively). Merge commits are
# skipped, since they add no authored change of their own.
#
# The commit list comes from the pull request API, so the job needs no
# checkout, no Node action and no third-party DCO app: `gh` and `jq` ship on
# the hosted runner. The API returns at most 250 commits for a pull request;
# a longer one fails with a message rather than passing unchecked.
#
# Pull requests only, and not while the PR is a draft. Title and body edits
# don't change the commits, so they skip; a base change (retarget) reruns.
on:
pull_request:
branches: [main]
types: [opened, synchronize, reopened, ready_for_review, edited]
# One run per PR per workflow: a new push cancels the older PR run. A title or
# body edit gets its own group, so its skipped run never cancels a real one.
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}${{ github.event.action == 'edited' && github.event.changes.base == null && format('-edit-{0}', github.run_id) || '' }}
cancel-in-progress: true
permissions:
contents: read
pull-requests: read
jobs:
dco:
name: ✍️ DCO sign-off${{ github.event.action == 'edited' && github.event.changes.base == null && ' (edit, not run)' || '' }}
if: github.event.pull_request.draft == false && (github.event.action != 'edited' || github.event.changes.base != null)
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: ✍️ Check every commit is signed off by its author
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
PR: ${{ github.event.pull_request.number }}
EXPECTED: ${{ github.event.pull_request.commits }}
run: |
set -euo pipefail
commits="$(gh api --paginate "repos/$REPO/pulls/$PR/commits?per_page=100" --jq '.[]' | jq -s '.')"
total="$(jq 'length' <<<"$commits")"
if [ "$total" -lt "$EXPECTED" ]; then
echo "::error::The API listed $total of $EXPECTED commits (it stops at 250). Split the pull request so every commit can be checked."
exit 1
fi
missing="$(jq -r '
.[]
| select((.parents | length) < 2)
| . as $k
| ($k.commit.author.name | ascii_downcase) as $name
| ($k.commit.author.email | ascii_downcase) as $email
| select([$k.commit.message
| scan("(?m)^Signed-off-by:[ \t]*([^<\n]*?)[ \t]*<([^>\n]*)>[ \t]*$")
| select((.[0] | ascii_downcase) == $name and (.[1] | ascii_downcase) == $email)
] | length == 0)
| "\($k.sha[0:12]) \($k.commit.author.name) <\($k.commit.author.email)>"
' <<<"$commits")"
if [ -n "$missing" ]; then
while IFS= read -r line; do
echo "::error::Commit $line has no Signed-off-by line matching its author."
done <<<"$missing"
echo "Sign off each commit with 'git commit -s'. To fix commits already pushed,"
echo "run 'git rebase --signoff origin/main' and force-push the branch."
exit 1
fi
echo "All $total commits carry a sign-off matching their author."