Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
209e63c
ci: this repo's public-repo-guard never scanned a single issue or com…
yakimoto Aug 6, 2026
8234983
fix(ci): scope body-policy allowlists and guarantee a PCRE2 ripgrep
yakimoto Aug 6, 2026
9219dd8
ci: scan review bodies, fix install list, use synthetic fixture repo …
yakimoto Aug 6, 2026
45fa1b4
fix(ci): run the tree scan on PR edits so a skipped check can't super…
yakimoto Aug 6, 2026
9bac94c
test: split private-key fixture literal so the foundation-gate secret…
yakimoto Aug 6, 2026
ee35c9e
fix: drop the stray word boundary so compound credential names match …
yakimoto Aug 6, 2026
e3a4265
docs: state precisely what per-job concurrency does and does not fix …
yakimoto Aug 6, 2026
5d9db11
fix: key the body-guard group on the comment/review id first so batch…
yakimoto Aug 6, 2026
7e086ee
fix(ci): close two body-guard pass-by-default paths
yakimoto Aug 6, 2026
f2f50d7
ci: skip tree scan for review events on closed PRs
yakimoto Aug 6, 2026
c284c89
fix(ci): skip body scan for review events on closed PRs
yakimoto Aug 6, 2026
ff2339c
Merge remote-tracking branch 'origin/main' into ci/1747-public-repo-g…
yakimoto Sep 6, 2026
d16a745
fix(ci): pin body-guard checkout to a trusted ref
yakimoto Sep 6, 2026
326844a
fix(ci): bootstrap fallback for the trusted-ref pin on body-guard
yakimoto Sep 6, 2026
4a5dc0d
Merge remote-tracking branch 'origin/main' into HEAD
yakimoto Sep 8, 2026
882e965
ci(guard): adopt the landed two-file body-scan generation, and drop t…
yakimoto Sep 8, 2026
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
122 changes: 122 additions & 0 deletions .github/workflows/public-repo-guard-body.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
name: public-repo-guard-body

# The other half of public-repo-guard.yml's coverage, deliberately in its OWN
# workflow file — see the long comment block at the top of public-repo-guard.yml
# for the incident (wave-av/cli PR #68) that caused the split and why it is a
# file-level split, not just a job-level one.
#
# `guard` (in public-repo-guard.yml) scans the published TREE and produces the
# REQUIRED check "Secrets + content policy". This job scans a PR/issue/comment
# BODY, which is just as world-readable and, until this job existed, was scanned
# by nothing server-side. That gap was real, not theoretical: a PR was blocked
# for naming a private repo in wrangler.toml while the very same name, with more
# operational detail attached, sat unchallenged in its body.
#
# This job's check-run name ("Body content policy") is NOT a required status
# context in this repo's ruleset, so it can safely trigger on every comment/review
# event without any risk of masking or wedging the required tree-scan context —
# that is the entire reason it lives in a separate file from the tree scan.
#
# Honest about what it can and cannot do. On a PR this PREVENTS the merge. On an
# issue or comment the text is already public the moment it posts, so this is
# detection — it tells us to go redact, fast. Only the client-side pre-write hook
# can stop that class before publication.
on:
# `edited` matters as much as `opened`: a body can be made to leak long after
# the PR is first raised, and until this job covered it, nothing re-scanned it.
pull_request:
types: [opened, edited, reopened, synchronize]
issues:
types: [opened, edited]
issue_comment:
types: [created, edited]
# Inline review comments on a diff are a SEPARATE event from issue_comment —
# without this trigger they are world-readable text that no job ever scans.
pull_request_review_comment:
types: [created, edited]
# A submitted review's top-level body (the free-text field above any inline
# comments) is yet another world-readable payload, separate from BOTH comment
# events — without this trigger nothing ever scans it.
pull_request_review:
types: [submitted, edited]

# `pull_request`, deliberately NOT `pull_request_target`: a fork PR must never get
# a write token or repo secrets just because a gate wanted to read its body.
permissions:
contents: read

jobs:
body-guard:
name: Body content policy
concurrency:
# Keyed on the specific comment / review / PR / issue rather than github.ref,
# because issue events all report the default branch and a ref-keyed group
# would let two comments cancel each other, leaving one unscanned. The comment
# and review ids come FIRST: those payloads also carry the PR number, and
# keying them on the PR would collapse two rapid comments into one group,
# dropping a verdict.
#
# cancel-in-progress is deliberately FALSE. Every version of a body deserves a
# verdict, the job is seconds long, and a cancelled check-run lingers on the
# commit. Since this check-run name is not required, a lingering cancelled
# run here cannot wedge a merge the way the tree scan's could — but a dropped
# verdict on a body would still be a real coverage gap, so the same "let it
# finish" policy applies.
group: public-repo-guard-body-${{ github.event.comment.id || github.event.review.id || github.event.pull_request.number || github.event.issue.number || github.ref }}
cancel-in-progress: false
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# Only the gate's own scripts are needed — no reason to pay for the whole
# tree on every comment.
sparse-checkout: scripts/public-repo-guard
sparse-checkout-cone-mode: false
# This job only reads the scripts — never leave the token sitting in
# .git/config while repo-supplied scripts execute in the workspace.
persist-credentials: false

# Same rationale as the tree job: body-policy.sh needs a PCRE2-enabled rg,
# and Ubuntu's apt package has none.
- name: Install ripgrep (pinned + checksum-verified, PCRE2 build)
env:
RIPGREP_VERSION: "14.1.1"
RIPGREP_SHA256: "4cf9f2741e6c465ffdb7c26f38056a59e2a2544b51f7cc128ef28337eeae4d8e"
run: |
if command -v rg >/dev/null && rg --pcre2-version >/dev/null 2>&1; then
echo "using preinstalled $(rg --version | head -n1) with PCRE2"; exit 0
fi
curl -fsSL --proto '=https' --tlsv1.2 -o ripgrep.tar.gz \
"https://github.com/BurntSushi/ripgrep/releases/download/${RIPGREP_VERSION}/ripgrep-${RIPGREP_VERSION}-x86_64-unknown-linux-musl.tar.gz"
echo "${RIPGREP_SHA256} ripgrep.tar.gz" | sha256sum -c -
tar -xzf ripgrep.tar.gz --strip-components=1 "ripgrep-${RIPGREP_VERSION}-x86_64-unknown-linux-musl/rg"
sudo install -m 0755 rg /usr/local/bin/rg
rm -f rg ripgrep.tar.gz
rg --pcre2-version

# The body is read straight out of the event payload FILE and written to
# another file. It is never interpolated into a run: block and never placed
# in an environment variable, so shell metacharacters in a hostile PR body
# have nothing to act on. jq is preinstalled on the GitHub-hosted images.
- name: Materialize the untrusted title/body to a file
run: |
set -euo pipefail
mkdir -p "$RUNNER_TEMP/bodyscan"
# An UNRECOGNIZED payload shape must fail, never quietly scan nothing and
# report a pass. If the event schema ever moves, this job must go red
# rather than become a green rubber stamp over an unscanned body.
if [ "$(jq -r 'has("pull_request") or has("issue") or has("comment") or has("review")' "$GITHUB_EVENT_PATH")" != "true" ]; then
echo "::error title=public-repo-guard-body::Event payload contains no pull_request/issue/comment/review object — refusing to report a pass on an unscanned body."
exit 1
fi
jq -r '[.pull_request.title, .pull_request.body,
.issue.title, .issue.body,
.comment.body, .review.body]
| map(select(. != null)) | join("\n")' \
"$GITHUB_EVENT_PATH" > "$RUNNER_TEMP/bodyscan/body.txt"
echo "scanning $(wc -l < "$RUNNER_TEMP/bodyscan/body.txt") line(s) of body text"

- name: body policy (PR / issue / comment text)
env:
GUARD_PRIVATE_REPOS: ${{ vars.GUARD_PRIVATE_REPOS }}
run: bash scripts/public-repo-guard/body-policy.sh "$RUNNER_TEMP/bodyscan/body.txt"
93 changes: 84 additions & 9 deletions .github/workflows/public-repo-guard.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
name: public-repo-guard

# Pre-publication content gate for WAVE public repos. Two complementary checks:
# Pre-publication content gate for WAVE public repos. Two complementary checks,
# split across TWO workflow files (this one, plus public-repo-guard-body.yml):
# 1. gitleaks — formatted secrets (API keys, tokens, private keys) in the tree.
# 2. content-policy.sh — WAVE-specific leaks gitleaks misses: live Stripe account
# IDs, hardcoded Cloudflare account_ids, developer absolute paths, references
Expand All @@ -13,36 +14,84 @@ name: public-repo-guard
# wave-av/.github must not be able to alter another repo's secret scanner). The
# gitleaks binary is version-pinned AND SHA-256-verified before it runs.
#
# To install on a new repo, copy all three files together:
# To install on a new repo, copy all six files together (the guard job runs the
# fixture tests, so a repo missing the tests file fails on every run):
# .github/workflows/public-repo-guard.yml
# .github/workflows/public-repo-guard-body.yml
# .gitleaks.toml
# scripts/public-repo-guard/content-policy.sh
# scripts/public-repo-guard/body-policy.sh
# scripts/public-repo-guard/tests/body-policy.test.sh
#
# Scan scope: the published working TREE (gitleaks --no-git), NOT git history. The
# goal is "what is public right now is clean", so a shallow checkout is sufficient.
#
# Allowlisting: annotate a verified-safe line with `# guard:allow <reason>`, add a
# path glob to a repo-root `.guardignore`, or extend the repo-local `.gitleaks.toml`.
#
# WHY THIS IS A SEPARATE WORKFLOW FROM public-repo-guard-body.yml (this used to be
# one file with two jobs sharing one `on:` block):
#
# The `guard` job below produces the check-run named "Secrets + content policy",
# which is the REQUIRED status context in this repo's branch-protection ruleset
# (public-repo-guard-required). Before this split, that job's shared `on:` block
# had to include pull_request_review / pull_request_review_comment (needed only by
# the sibling body scan), and the job used a job-level `if:` to skip those events
# for the tree scan (a title/comment/review event cannot change the tree). GitHub
# still publishes a check-run named "Secrets + content policy" with conclusion
# `skipped` for every skipped event, on the same head SHA. Branch-protection/
# ruleset required-status-check evaluation treats `skipped` as passing and reads
# only the NEWEST check-run of a given name — so a review comment (or any other
# skipped event) could flip an already-failed, or never-yet-completed, required
# tree scan to green with nothing re-examining the tree. Observed and confirmed on
# a sibling public repo before this shape was adopted here: wave-av/mcp-server
# PR 87 (merged) — the tree job's only non-skipped run on that head SHA was
# `cancelled`, followed by a dozen `skipped` runs, and the required check's final
# state read `skipped` (passing) despite no completed real verdict ever having
# been produced for that SHA. This file is the same fix, ported.
#
# Splitting into two workflow FILES — not just two jobs — removes the shared
# trigger set entirely: this file's `on:` block now lists ONLY events that can
# change the published tree (pull_request open/reopen/sync, push, workflow_dispatch,
# merge_group). A review comment or a title/body edit never matches this
# workflow's trigger at all, so GitHub never runs it and never publishes ANY
# check-run — skipped, cancelled, or otherwise — under the required name for that
# event. There is nothing left to mask. Coverage is unchanged: every event that
# could previously produce a real (non-skipped) tree-scan run still produces that
# same real run after the split.

on:
pull_request:
types: [opened, reopened, synchronize]
push:
branches: [main, master]
workflow_dispatch:
# Required by the merge queue: a `merge_group` build never runs the `pull_request`
# trigger above, so without this the required "Secrets + content policy" check
# never reports on the queue's temporary ref and every queued PR waits forever.
merge_group:

# `pull_request`, deliberately NOT `pull_request_target`: a fork PR must never get
# a write token or repo secrets just because a gate wanted to read its body.
permissions:
contents: read

concurrency:
group: public-repo-guard-${{ github.ref }}
cancel-in-progress: true

jobs:
guard:
name: Secrets + content policy
# No job-level `if:` needed: the `on:` block above already scopes this job to
# exactly the tree-changing events, so every triggering event is a real run —
# never skipped, never a candidate for the masking bug described above.
concurrency:
group: public-repo-guard-tree-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Comment thread
yakimoto marked this conversation as resolved.
with:
# Nothing in this job pushes or calls the API, so the token has no
# business lingering in .git/config while repo-checked-out scripts run.
persist-credentials: false

# gitleaks' GitHub Action requires a paid license for organizations; the CLI
# itself is MIT-licensed and free. Pin the version AND verify the release
Expand All @@ -64,10 +113,36 @@ jobs:
- name: gitleaks (secret scan — published tree)
run: gitleaks detect --no-git --source . --config .gitleaks.toml --redact --no-banner --exit-code 1

- name: Install ripgrep
run: command -v rg >/dev/null || (sudo apt-get update -qq && sudo apt-get install -y -qq ripgrep)
# Both policy scripts are rg -P (PCRE2), and Ubuntu's apt ripgrep is built
# WITHOUT it — with that build every rule exits 2 and this required check
# goes permanently red. Install the upstream binary the same way as gitleaks
# above: version-pinned AND SHA-256-verified before it runs. Skipped when the
# runner image already carries a PCRE2-capable rg (probed, not assumed).
- name: Install ripgrep (PCRE2 build, pinned + checksum-verified)
env:
RIPGREP_VERSION: "14.1.1"
RIPGREP_SHA256: "4cf9f2741e6c465ffdb7c26f38056a59e2a2544b51f7cc128ef28337eeae4d8e"
run: |
set -euo pipefail
if command -v rg >/dev/null && rg --pcre2-version >/dev/null 2>&1; then
echo "using preinstalled $(rg --version | head -n1) with PCRE2"; exit 0
fi
curl -fsSL --proto '=https' --tlsv1.2 -o ripgrep.tar.gz \
"https://github.com/BurntSushi/ripgrep/releases/download/${RIPGREP_VERSION}/ripgrep-${RIPGREP_VERSION}-x86_64-unknown-linux-musl.tar.gz"
echo "${RIPGREP_SHA256} ripgrep.tar.gz" | sha256sum -c -
tar -xzf ripgrep.tar.gz --strip-components=1 "ripgrep-${RIPGREP_VERSION}-x86_64-unknown-linux-musl/rg"
sudo install -m 0755 rg /usr/local/bin/rg
rm -f rg ripgrep.tar.gz
rg --pcre2-version

- name: content policy (WAVE trade-secret / internal-leak gate)
env:
GUARD_PRIVATE_REPOS: ${{ vars.GUARD_PRIVATE_REPOS }}
run: bash scripts/public-repo-guard/content-policy.sh .

# The body gate's own fixtures. Its negatives are the load-bearing half — a
# leak gate that blocks legitimate cross-repo references gets switched off,
# and then it protects nothing. Runs here so a regression is caught by CI
# rather than by a leak.
- name: body policy self-test (fixtures)
run: bash scripts/public-repo-guard/tests/body-policy.test.sh
Loading
Loading