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
64 changes: 64 additions & 0 deletions .github/tests/validator-gate-harness.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,32 @@ def workflow_permissions(path):
return result


def job_if(path, job_name):
"""Return a job's top-level `if:` expression (unquoted), or None when absent.

A line-based read of the 2-space-indented `<job_name>:` block under `jobs:`, then
the first 4-space-indented `if:` inside it — the harness's no-PyYAML technique.
"""
if not os.path.exists(path):
raise HarnessError("workflow not found: " + path)
with open(path, "r", encoding="utf-8") as fh:
lines = fh.read().splitlines()
in_job = False
for line in lines:
if not line.strip() or line.lstrip().startswith("#"):
continue
indent = len(line) - len(line.lstrip(" "))
stripped = line.strip()
if indent == 2 and stripped == job_name + ":":
in_job = True
continue
if in_job and indent == 2 and stripped.endswith(":"):
break # the next job begins
if in_job and indent >= 4 and stripped.startswith("if:"):
return unquote(stripped.partition(":")[2].strip())
return None


def indent(text, pad):
return NL.join(pad + line for line in text.splitlines())

Expand Down Expand Up @@ -1444,6 +1470,44 @@ def note(ok, message):
"functional: the org-wide caller org-wide-pr-validator-required.yml ALSO grants "
"`issues` (a called workflow holds only what its caller grants; asserting only the "
"reusable would pass while the caller nullified it — .github#173)")
# SAME-REPO vs EXTERNAL-FORK (the dsh-github#2 block, 2026-10-07). The validate job must
# gate on `head.repo.full_name == github.repository` — same-repo PRs RUN even when the
# repo is a first-party FORK of an upstream, and only a genuine external contribution
# (a DIFFERENT head repo) skips. The retired `head.repo.fork == false` test is TRUE for
# every PR in a fork repo, so it SKIPPED the org's own PRs and the ruleset's REQUIRED
# `validate / validate` was never produced (measured: `dsh-github` main carries the
# ruleset; `gh pr merge` reports `Required status check "validate / validate" is
# expected`). Assert BOTH the predicate's presence and its evaluated truth table, so a
# silent revert to the fork-repo form — or the deletion of the `if:` — FAILS here.
caller_if = job_if(CALLER_WORKFLOW_PATH, "validate")
note(caller_if is not None,
"structural: the org-wide caller's validate job carries an `if:` (a job with no "
"`if:` would also run, but the predicate is the point — assert it is present)")
if caller_if is not None:
def caller_gate(head_fork, same_repo):
# A null head repo (a deleted fork) evaluates every `full_name` lookup to the
# empty string, which is != github.repository, so the job SKIPS — the safe
# default. Model `head_fork` explicitly so a revert to the fork-repo test is
# caught even where same_repo is true.
head_full = "o/r" if same_repo else "someoneelse/r"
ns = Ctx({
"github": Ctx({"repository": "o/r",
"event": Ctx({"pull_request": Ctx({"head": Ctx({"repo": Ctx({
"full_name": head_full, "fork": head_fork})})})})}),
})
return eval_gh(caller_if, ns)
note(caller_gate(head_fork=True, same_repo=True) is True,
"functional: a SAME-REPO PR in a first-party FORK repo (head.fork=true, "
"head.full_name==github.repository) RUNS the validator (the exact dsh-github#2 "
"shape the fork-repo test wrongly skipped)")
note(caller_gate(head_fork=False, same_repo=True) is True,
"functional: a SAME-REPO PR in a non-fork repo RUNS the validator")
note(caller_gate(head_fork=True, same_repo=False) is False,
"functional: an EXTERNAL-FORK PR (head.full_name!=github.repository) SKIPS "
"(untrusted contribution + no fork secrets — the security intent preserved)")
note("head.repo.fork == false" not in caller_if,
"forbid: the retired fork-repo predicate `head.repo.fork == false` is GONE "
"(it is true for every PR in a fork repo, so it dead-locked first-party forks)")
# FUNCTIONAL coverage (the review's R10 finding: the env entry and the engine-defective
# classification shipped with NO assertion that fails without them).
review_env = find_step(steps, "id", "review")["env"]
Expand Down
16 changes: 15 additions & 1 deletion .github/workflows/org-wide-pr-validator-required.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,21 @@ permissions:
jobs:
validate:
name: validate
if: github.event.pull_request.head.repo.fork == false
# SAME-REPO vs EXTERNAL-FORK — measured 2026-10-07 (opencharly/dsh-github#2). The gate
# tests whether the head repo is a DIFFERENT repository than the base
# (`head.repo.full_name == github.repository`), NOT whether the head repo is itself a
# fork (`head.repo.fork == false`). The old fork-repo test is TRUE for every PR in a
# first-party FORK repo — opencharly vendors several upstreams as forks (dsh-github,
# crabbox, the pi-* family) — so it SKIPPED the org's OWN same-repo PRs: the ruleset's
# REQUIRED `validate / validate` was then never produced and the PR could never merge
# (measured: `dsh-github` main carries the ruleset while every pre-existing fork does
# not; `gh pr merge` reports `Required status check "validate / validate" is expected`).
# The new form skips ONLY a genuine external contribution — a PR whose head repo is a
# DIFFERENT repo than the base, whose fork secrets are unavailable and whose code is
# untrusted — while running for every SAME-REPO PR regardless of whether the repo is a
# fork of an upstream. A null head repo (a deleted fork) yields an absent `full_name`
# and skips, the safe default.
if: github.event.pull_request.head.repo.full_name == github.repository
uses: opencharly/.github/.github/workflows/pr-validator.yml@main
with:
pr-number: ${{ github.event.pull_request.number }}
Expand Down
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,37 @@ $ scripts/org-ruleset.sh apply # then enable the org rulese
$ scripts/org-ruleset.sh verify # assert the whole end state
```

### The gate runs on SAME-REPO PRs, whatever the repo is

The required workflow's `validate` job gates on
**`github.event.pull_request.head.repo.full_name == github.repository`** — the head
repo is a *different* repository than the base. It runs for **every same-repo PR**,
including PRs in a first-party **fork** repo (the org vendors several upstreams as
forks: `dsh-github`, `crabbox`, the `pi-*` family), and skips only a genuine
**external contribution** (a head repo that is a *different* repo — whose fork
secrets are unavailable and whose code is untrusted). A null head repo (a deleted
fork) resolves to the empty string, which is `!= github.repository`, so the job
skips — the safe default.

**This predicate is load-bearing.** The retired `head.repo.fork == false` test asked
"is the head repo itself a fork?", which is true for **every** PR in a fork repo, so
it skipped the org's own PRs. Combined with the ruleset's REQUIRED
`validate / validate` check, that made such a PR un-mergeable — the required check was
never produced (`measured 2026-10-07, opencharly/dsh-github#2`: `dsh-github` main
carries the ruleset while every pre-existing fork does not, and `gh pr merge` reports
`Required status check "validate / validate" is expected`). The predicate and its
truth table are asserted by `.github/tests/validator-gate-harness.py` (which runs on
every PR to this repo) and by a `same_repo_gate` row in `scripts/governance-claims.tsv`.

**Consequence for a new fork repo.** Forks are **excluded from the ruleset by
design** (`discover_excludes` filters on `isFork == true`), so they carry no required
check and their same-repo PRs merge without an org verdict. The hazard is a fork
created *after* the last `scripts/org-ruleset.sh apply`: it is absent from the stale
exclude list, so the ruleset's `~ALL`-minus-exclude scope still lists it and its
same-repo PRs get the REQUIRED check. With the retired predicate that check was never
produced (deadlock); with this predicate it runs and the PR merges on PASS. The next
`apply` re-derives the exclude set and drops the fork back out of the ruleset target.

## New repos: bootstrap the initial `main`

The org ruleset carries the `creation` rule on `refs/heads/main`, so a **brand-new
Expand Down
13 changes: 13 additions & 0 deletions scripts/governance-claims.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,16 @@ ci_gate_mechanical validator workflow dotgithub/.github/workflows/pr-validator.y
ci_gate_mechanical validator rulebook dotgithub/charly.yml require BRANCH PROTECTION DOES NOT ENFORCE IT
ci_gate_mechanical validator rulebook dotgithub/charly.yml require enforced by that first gate, NOT by branch
ci_gate_mechanical validator rulebook dotgithub/charly.yml forbid enforced by branch protection
# SAME-REPO vs EXTERNAL-FORK (opencharly/.github dsh-github#2, 2026-10-07). The org-required
# caller must gate the validate job on `head.repo.full_name == github.repository` — the
# head repo is a DIFFERENT repository than the base — NOT on `head.repo.fork == false`,
# which is TRUE for every PR in a first-party FORK repo (opencharly vendors several upstreams
# as forks: dsh-github, crabbox, the pi-* family). The retired predicate skipped the org's OWN
# same-repo PRs, so the ruleset's REQUIRED `validate / validate` was never produced and the PR
# could never merge. The new form skips ONLY a genuine external contribution.
same_repo_gate org-wide required workflow dotgithub/.github/workflows/org-wide-pr-validator-required.yml require head.repo.full_name == github.repository
# The forbid pattern targets the ACTIVE form — an `if:` line using the retired predicate —
# not the prose that explains why it is retired (the same technique as the
# `check-runs...filter=latest` row above: the file names the retired form in a comment, so a
# bare-substring forbid would fail on its own explanation).
same_repo_gate org-wide required workflow dotgithub/.github/workflows/org-wide-pr-validator-required.yml forbid if: .*head\.repo\.fork == false
Loading