Skip to content
Open
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
1 change: 1 addition & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
Closes #

## Checklist
- [ ] Every commit includes a DCO `Signed-off-by` trailer (`git commit -s`; see `docs/dco.md`)
- [ ] My code follows the rule template in CONTRIBUTING.md
- [ ] I added or updated the matching CLI playbook
- [ ] I added or updated all four compliance framework mappings
Expand Down
24 changes: 24 additions & 0 deletions .github/workflows/dco.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: Developer Certificate of Origin

on:
pull_request:
branches: [dev, main]

permissions:
contents: read

jobs:
signoff:
name: DCO sign-off
runs-on: ubuntu-latest
steps:
- name: Checkout pull request history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 0

- name: Verify every pull request commit
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: python scripts/check_dco.py "$BASE_SHA" "$HEAD_SHA"
37 changes: 37 additions & 0 deletions docs/access-continuity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Project Access Continuity

OpenShield's public component ownership is recorded in `.github/CODEOWNERS`.
OpenSSF continuity requires more than public names: organization owners must
verify that the project can continue if any one person becomes unavailable.

## Required capability matrix

At least two currently available people must independently be able to perform
each critical capability, or use a tested organization-controlled recovery
process:

| Capability | Primary confirmed | Backup confirmed | Last tested |
|---|---|---|---|
| Triage and close issues | Owner record | Owner record | Date required |
| Review and merge approved changes | Owner record | Owner record | Date required |
| Publish and verify a release | Owner record | Owner record | Date required |
| Recover GitHub organization access | Owner record | Owner record | Date required |
| Manage production deployment access | Owner record | Owner record | Date required |
| Manage domain/DNS access, if applicable | Owner record | Owner record | Date required |
| Rotate security-reporting access | Owner record | Owner record | Date required |

Names and recovery details may remain in a private owner-controlled record when
publishing them would increase risk. The public OpenSSF justification should
state the date of verification and that two independent holders were confirmed,
without exposing secrets.

## Review process

- Review the matrix at least every six months and before each major release.
- Remove access promptly when a role ends and confirm the backup remains valid.
- Test recovery without sharing credentials between individuals.
- Store recovery material outside any single maintainer's personal account.
- Record the review in issue #205 or another auditable owner-approved record.

The continuity and bus-factor criteria must remain pending until an organization
owner completes and records this verification. Documentation alone is not proof.
33 changes: 33 additions & 0 deletions docs/dco.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Developer Certificate of Origin

OpenShield uses the [Developer Certificate of Origin 1.1](https://developercertificate.org/)
as its contribution authorization mechanism. A `Signed-off-by` trailer states
that the contributor is legally entitled to submit the work under the project's
license and agrees to the DCO certification.

Add the trailer automatically when committing:

```bash
git commit -s -m "feat: describe the change"
```

The name and email in the trailer should identify the contributor and should
match the commit author unless a documented contribution workflow requires a
different authorized signer. Every non-merge commit introduced by a pull
request is checked; a sign-off only in the pull request description is
insufficient.

Merge commits (e.g. from running `git merge origin/dev` to bring your branch
up to date) are exempt — they carry Git's own default message, not your
authorship, so there is nothing for you to sign off on. Only commits you
authored yourself need the trailer.

To repair the latest local commit before review:

```bash
git commit --amend --signoff --no-edit
git push --force-with-lease
```

For multiple commits, use an interactive rebase and add a sign-off to each
commit. Do not add another person's sign-off without their authorization.
68 changes: 68 additions & 0 deletions scripts/check_dco.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"""Require a Developer Certificate of Origin sign-off on each PR commit."""

import re
import subprocess
import sys
from typing import Iterable


SIGNOFF = re.compile(r"^Signed-off-by:\s+.+\s+<[^<>@\s]+@[^<>\s]+>$", re.IGNORECASE | re.MULTILINE)


def has_signoff(message: str) -> bool:
"""Return whether a commit message contains a well-formed DCO trailer."""
return SIGNOFF.search(message) is not None


def commits_between(base: str, head: str) -> list[str]:
"""Return commits introduced between the pull request base and head.

Excludes merge commits: a `git merge origin/dev` inside a long-running PR
branch produces a commit with Git's default merge message and no
Signed-off-by trailer, through no fault of the author's own commits.
GitHub's own DCO app skips merge commits for the same reason.
"""
output = subprocess.check_output(
["git", "rev-list", "--reverse", "--no-merges", f"{base}..{head}"],
text=True,
)
return [item for item in output.splitlines() if item]


def commit_message(commit: str) -> str:
"""Read one commit message without interpreting its contents as a command."""
return subprocess.check_output(
["git", "show", "--no-patch", "--format=%B", commit],
text=True,
)


def unsigned_commits(commits: Iterable[str]) -> list[str]:
"""Return commit IDs that lack a valid Signed-off-by trailer."""
return [commit for commit in commits if not has_signoff(commit_message(commit))]


def main() -> int:
if len(sys.argv) != 3:
print("Usage: check_dco.py <base-sha> <head-sha>", file=sys.stderr)
return 2

commits = commits_between(sys.argv[1], sys.argv[2])
if not commits:
print("No pull request commits found.", file=sys.stderr)
return 1

missing = unsigned_commits(commits)
if missing:
print("The following commits lack a valid DCO Signed-off-by trailer:", file=sys.stderr)
for commit in missing:
print(f" {commit}", file=sys.stderr)
print("Recreate or amend them with: git commit -s", file=sys.stderr)
return 1

print(f"DCO sign-off verified for {len(commits)} commit(s).")
return 0


if __name__ == "__main__":
raise SystemExit(main())
36 changes: 36 additions & 0 deletions tests/test_dco_check.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""Tests for the dependency-free DCO enforcement helper."""

from unittest.mock import patch

from scripts.check_dco import commits_between, has_signoff, unsigned_commits


def test_has_signoff_accepts_standard_dco_trailer():
assert has_signoff("feat: change\n\nSigned-off-by: Tanvir Farhad <tanvir@example.com>\n")


def test_has_signoff_rejects_missing_or_malformed_trailer():
assert not has_signoff("feat: unsigned change")
assert not has_signoff("Signed-off-by: anonymous")
assert not has_signoff("Signed-off-by: Name <not-an-email>")


def test_unsigned_commits_checks_each_commit():
messages = {
"a": "fix: one\n\nSigned-off-by: A User <a@example.com>",
"b": "fix: two",
}
with patch("scripts.check_dco.commit_message", side_effect=messages.get):
assert unsigned_commits(["a", "b"]) == ["b"]


def test_commits_between_excludes_merge_commits():
"""A `git merge origin/dev` inside a PR branch has no Signed-off-by
trailer and isn't the author's own commit — it must never be checked,
or a legitimate PR gets blocked for merging the base branch in."""
with patch("scripts.check_dco.subprocess.check_output", return_value="abc123\ndef456\n") as mock_run:
result = commits_between("base-sha", "head-sha")

assert result == ["abc123", "def456"]
called_args = mock_run.call_args.args[0]
assert "--no-merges" in called_args
Loading