Files
gstack/.github/workflows/plain-title.yml
T
Garry Tanandcapy-ai[bot] d479c5bf1e Require the Plain title check before merging, and keep long PR bodies readable (GST-1825) (#4152)
* Keep reading an older receipt at the top of a PR body, and leave quoted receipt examples in place

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* Put a receipt posted on its own inside the PR body's Agent notes, so the new one is the one read

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* Read the PR body from the event file so a very long body cannot break the title check

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* Make the Plain title check required before gstack merges a PR

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* Keep the PR's opening paragraph when a posted receipt moves into the Agent notes

Review-Finding: v1/adversarial-native#1 reader=adversarial-native
Review-Finding: v1/outside-voice#1 reader=outside-voice
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* Say exactly when an older receipt at the top of a PR body is still read

The older receipt is read only while it still opens the body. A receipt quoted in the readable part is never read only when the body has an Agent notes section. The example policy now lists the Plain title check.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* Add the changelog entry for the required Plain title check

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
2026-11-08 03:23:53 +00:00

90 lines
3.9 KiB
YAML

name: Plain title
# Checks that a PR's title reads in everyday words (R76, docs/ship-policy.md#plain-titles).
# The title is refused in the front: tracker codes, project-only words and a
# type(scope): prefix. One trailing group of references like (ABC-123) or (#4122)
# is fine. The branch name, the PR opening and the commit subjects only warn.
# PRs created before the policy's since date are exempt.
#
# WHY pull_request_target: it runs the base repository's copy of this workflow,
# of bin/gstack-plain-title and of .gstack/plain-titles.json, so a PR cannot
# change the rule it is checked against, and a title edit reruns the check.
#
# WHY this is SAFE:
# - The job checks out the BASE repo (no ref:) and never runs PR code.
# - The token is read-only (contents: read, pull-requests: read).
# - The title, branch, body and created_at arrive only through env: and are
# written to files with printf '%s'. No PR field is inlined into run:.
# - Only the checker's match lines reach the log; PR text is never echoed.
# test/plain-title-workflow-safety.test.ts is the static tripwire for all of it.
on:
pull_request_target:
types: [opened, edited, synchronize, reopened, ready_for_review]
permissions:
contents: read
pull-requests: read
jobs:
plain-title:
name: Plain title
# No job-level if: every event, a base change included, reruns the title
# check, because a skipped run would count as passing over a refused title.
runs-on: ubuntu-24.04
timeout-minutes: 5
# A newer event for the same PR cancels the older run.
concurrency:
group: plain-title-${{ github.event.pull_request.number }}
cancel-in-progress: true
steps:
# Base repo only: trusted code. No PR-head checkout.
- name: Checkout base repo (trusted)
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 1
persist-credentials: false
- name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: 1.4.2
- name: Write the PR fields to files
env:
# Attacker-controlled on fork PRs: env only, never inlined into run:.
PR_TITLE: ${{ github.event.pull_request.title }}
GH_TOKEN: ${{ github.token }}
PR_NUM: ${{ github.event.pull_request.number }}
REPO: ${{ github.repository }}
run: |
set -euo pipefail
printf '%s' "$PR_TITLE" > "$RUNNER_TEMP/pr-title.txt"
# The body comes from the event file: a large body overflows one environment string (E2BIG).
jq -j '.pull_request.body // ""' "$GITHUB_EVENT_PATH" > "$RUNNER_TEMP/pr-body.txt"
# Commit subjects are a warning only: skip quietly when the API read fails.
if ! gh api --paginate "repos/$REPO/pulls/$PR_NUM/commits" \
--jq '.[] | select((.parents | length) < 2) | .commit.message | split("\n")[0]' \
> "$RUNNER_TEMP/pr-commits.txt" 2>/dev/null; then
: > "$RUNNER_TEMP/pr-commits.txt"
fi
- name: Check the title
env:
PR_CREATED_AT: ${{ github.event.pull_request.created_at }}
run: |
set -euo pipefail
bun --no-env-file --config=/dev/null bin/gstack-plain-title check \
--title-file "$RUNNER_TEMP/pr-title.txt" --created-at "$PR_CREATED_AT" --mode refuse
- name: Check the branch name, opening and commit subjects (warnings only)
if: ${{ !cancelled() }}
env:
PR_BRANCH: ${{ github.event.pull_request.head.ref }}
PR_CREATED_AT: ${{ github.event.pull_request.created_at }}
run: |
set -euo pipefail
bun --no-env-file --config=/dev/null bin/gstack-plain-title check \
--branch "$PR_BRANCH" --body-file "$RUNNER_TEMP/pr-body.txt" \
--commits-file "$RUNNER_TEMP/pr-commits.txt" --created-at "$PR_CREATED_AT" --mode warn