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
15 changes: 15 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
## What this changes



## AI use

Tick exactly one. This is the signal the `scpe` check reads — an unticked box counts as
no disclosure at all. Using AI is fine here; not saying so is what this asks about.

- [ ] I used generative AI
- [ ] I did not use generative AI

<sub>Or put an `Assisted-by: <tool>` trailer on a commit instead — same effect, and it
travels with the commit rather than the pull request. Checked by
[SCPE](https://github.com/augbastos/scpe) level 1.</sub>
73 changes: 73 additions & 0 deletions .github/workflows/scpe-seal.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
name: scpe-seal

# Companion to scpe.yml — drop both in .github/workflows/. This is the TRUSTED half of
# the fork-safe split: it runs in the BASE repository with a write-scoped token, after
# the untrusted job finished, and its only job is to post what that job decided.
#
# It never checks out the contributor's code, never runs it, and installs nothing. The
# only binaries it touches are the runner's own pre-installed `python3` and `gh`.
#
# Why a separate file: a workflow cannot name itself in `workflow_run.workflows`. GitHub
# fails to register such a file — it shows the file path where the workflow name should
# be, and every run fails at the workflow level with zero jobs. So the trigger has to
# live in a second file that names the first.
#
# `workflow_run` only fires for a workflow file present on the DEFAULT branch. Merge
# both files to your default branch before expecting a seal on a pull request.
on:
workflow_run:
workflows: ["scpe"]
types: [completed]

permissions:
contents: read

jobs:
seal: # TRUSTED — write token; never runs untrusted code
if: ${{ github.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-latest
permissions:
pull-requests: write
# A job-level permissions block REPLACES the workflow default (it does not merge),
# so every unlisted scope is set to none. actions/download-artifact@v4 with run-id
# needs actions:read to reach a sibling run's artifact — without it the download
# fails and the gate never runs.
actions: read
steps:
- name: Download the verify job's results
uses: actions/download-artifact@v4
with:
name: scpe-results
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ github.token }}

- name: Post the seal comment (or the gate failure)
env:
GH_TOKEN: ${{ github.token }}
run: |
pr="$(cat pr-number.txt)"
# The require DECISION was computed upstream, in the untrusted "verify"
# job (results.json's gate_pass) — this step only ever READS it, never
# recomputes it from raw contributor input. Missing key (require=false,
# the informational path) defaults to "pass": nothing changes.
gate_pass="$(python3 -c "import json; print(str(json.load(open('results.json')).get('gate_pass', True)).lower())")"
if [ "$gate_pass" = "false" ]; then
# results.json carries its own fail_message (naming the exact §8 status
# and the reason behind it, or level 1's missing disclosure) — use it
# verbatim; fall back to the generic message only for a results.json
# written by an older tag. Either way this is a READ, never a recompute.
msg="$(python3 -c "import json; d = json.load(open('results.json')); print(d.get('fail_message') or '❌ Not verifiable — this repository requires a signed SCPE contribution (spec scpe/0.1).')")"
gh pr comment "$pr" --repo "${{ github.repository }}" --body "$msg"
exit 1
fi
# The comment is rendered upstream too, in the same untrusted job, and
# posted here byte-for-byte. That does NOT weaken the split: this job
# already posted attacker-influenced text before — every field in the seal
# comes from the contributor's PR — so moving the rendering does not
# change the trust level of the CONTENT, only where the string is built.
# What it does change is that the obligation to neutralize that content
# (fence escaping, length caps) now sits in one tested renderer instead of
# in this YAML. The trusted job stays free of checkouts, installs and
# contributor code.
msg="$(python3 -c "import json; print(json.load(open('results.json')).get('comment') or '')")"
[ -n "$msg" ] && gh pr comment "$pr" --repo "${{ github.repository }}" --body "$msg"
48 changes: 48 additions & 0 deletions .github/workflows/scpe.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: scpe

# SCPE level 1 — https://github.com/augbastos/scpe
#
# Checks that every pull request carries an AI-use disclosure: an `Assisted-by:` commit
# trailer, or a checked box in the PR template. Nothing is signed, no key is needed, and
# nothing is installed on the runner — the Action runs one stdlib-only Python file out of
# its own checkout at the tag pinned below.
#
# It is INFORMATIONAL right now: `require: "false"` posts the result as a comment and
# never fails the check. Flip it to "true" to make the disclosure a merge requirement.
#
# TWO FILES, NOT ONE. The companion scpe-seal.yml holds the write token and posts the
# comment; this file runs contributor code with no secrets. They cannot be merged: a
# workflow that names itself in `workflow_run.workflows` fails to register with GitHub.
on:
pull_request:

permissions:
contents: read

jobs:
verify: # UNTRUSTED — no secrets; safe to run contributor code
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false # never write the token into .git/config
# Level 1 reads the commit messages in the PR range, and level 2 (if this repo
# ever moves up) recomputes `git diff BASE...HEAD` to check it against the
# signed digest. Neither is possible with the default shallow checkout.
fetch-depth: 0

- id: seal
uses: augbastos/scpe@v0.2.1
with:
level: "1" # 1 = disclosure lint · 2 = signed envelope required
require: "false" # "true" makes a missing disclosure fail the check

# Hand the verdict + PR number to the trusted job. The number MUST travel in the
# artifact: github.event.workflow_run.pull_requests is empty for fork PRs.
- run: echo "${{ github.event.pull_request.number }}" > pr-number.txt
- uses: actions/upload-artifact@v4
with:
name: scpe-results
path: |
results.json
pr-number.txt
Loading