From 6c5f7b2941b5da5fbc78b70e883a2b3d7323fb1b Mon Sep 17 00:00:00 2001 From: Sil Date: Sat, 3 Oct 2026 00:35:22 +0200 Subject: [PATCH 1/4] Fail the check when a pull request removes the pnpm project The organization check used to pass with "Nothing to run" when package.json, pnpm-lock.yaml, or the check script was missing, so a pull request that deleted one of them silently disabled validation. The base branch now decides: when it has all three, the pull request's merge result must keep all three or the check fails. The checkout fetches depth 2 so the step can read the merge commit's first parent, which is the base branch and cannot be changed by the pull request. Repositories that are not pnpm projects with a check script on the base branch still pass. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/check.yml | 34 +++++++++++++++++++++++++++++++--- README.md | 2 +- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index a8b0927..bd8d455 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -1,7 +1,9 @@ name: Check # Required on every pull request in the organization through the "Merge policy" ruleset. -# Runs the repository's own `check` script. Repositories without one pass with a notice. +# Runs the repository's own `check` script. A pull request fails when it removes package.json, +# pnpm-lock.yaml, or the `check` script from a base branch that has all three. Repositories +# that are not pnpm projects with a check script on the base branch pass with a notice. on: pull_request: @@ -26,14 +28,40 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false + # Also fetches the parents of the pull request's merge commit, so the next step can read the base branch. + fetch-depth: 2 + # The base branch decides whether a repository must run its check, because a pull + # request cannot change it. Once the base branch has package.json, pnpm-lock.yaml, + # and a `check` script, a pull request that removes any of them fails. - name: Find the check script id: scripts run: | - if [ -f package.json ] && [ -f pnpm-lock.yaml ] && node -e "process.exit(require('./package.json').scripts?.check ? 0 : 1)"; then + # Prints what a commit lacks to be a pnpm project with a check script. + missing() { + git cat-file -e "$1:package.json" 2>/dev/null || echo "package.json" + git cat-file -e "$1:pnpm-lock.yaml" 2>/dev/null || echo "pnpm-lock.yaml" + git show "$1:package.json" 2>/dev/null | + node -e 'process.exit(JSON.parse(require("fs").readFileSync(0, "utf8")).scripts?.check ? 0 : 1)' 2>/dev/null || + echo "a check script in package.json" + } + + # A pull request is checked out as a merge commit: the base branch, then the pull request. + if ! git rev-parse --verify --quiet HEAD^2 >/dev/null; then + echo "::error::Expected a pull request merge commit with the base branch as its first parent." + exit 1 + fi + + head_missing=$(missing HEAD) + base_missing=$(missing HEAD^1) + + if [ -z "$head_missing" ]; then echo "run=true" >> "$GITHUB_OUTPUT" + elif [ -z "$base_missing" ]; then + echo "::error::$GITHUB_BASE_REF is a pnpm project with a check script, but this pull request removes: $(echo "$head_missing" | paste -sd, - | sed 's/,/, /g'). Restore it so the check can run." + exit 1 else - echo "No pnpm project with a check script. Nothing to run." + echo "No pnpm project with a check script on $GITHUB_BASE_REF or in this pull request. Nothing to run." fi - if: steps.scripts.outputs.run == 'true' diff --git a/README.md b/README.md index c2ee44f..29b1f90 100644 --- a/README.md +++ b/README.md @@ -2,4 +2,4 @@ Organization-wide GitHub configuration. -`.github/workflows/check.yml` is required on every pull request through the organization "Merge policy" ruleset. It installs with pnpm and runs the repository's `check` script. A repository without a pnpm project or a `check` script passes with a notice. Nothing needs to be added to a repository. +`.github/workflows/check.yml` is required on every pull request through the organization "Merge policy" ruleset. It installs with pnpm and runs the repository's `check` script. A pull request fails when the base branch has `package.json`, `pnpm-lock.yaml`, and a `check` script and the pull request removes any of them. A repository whose base branch is not a pnpm project with a `check` script passes with a notice. Nothing needs to be added to a repository. From c9d9e54d21d6097860e85d82f24b70af0cbee556 Mon Sep 17 00:00:00 2001 From: Sil Date: Sat, 3 Oct 2026 00:36:28 +0200 Subject: [PATCH 2/4] Temporary: run the new check logic on this pull request Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/tmp-test/fixtures.sh | 58 ++++++++++++++++++ .github/workflows/check-preview-tmp.yml | 78 +++++++++++++++++++++++++ 2 files changed, 136 insertions(+) create mode 100755 .github/tmp-test/fixtures.sh create mode 100644 .github/workflows/check-preview-tmp.yml diff --git a/.github/tmp-test/fixtures.sh b/.github/tmp-test/fixtures.sh new file mode 100755 index 0000000..7c7aff4 --- /dev/null +++ b/.github/tmp-test/fixtures.sh @@ -0,0 +1,58 @@ +#!/bin/bash +# Usage per scenario: base files spec, PR action. Runs the extracted step as GitHub would (bash -e). +set -u +SCRIPT=$PWD/find-check.sh +root=$(mktemp -d /tmp/sec-report/work/fx.XXXX) +PKG_CHECK='{"name":"x","scripts":{"check":"prettier --check ."}}' +PKG_NOCHECK='{"name":"x","scripts":{"build":"astro build"}}' +scenario() { + local name=$1 base=$2 pr=$3 expect=$4 + local d=$root/$name; mkdir -p "$d"; cd "$d" + git init -q -b main; git config user.email t@t; git config user.name t + echo readme > README.md + case $base in + pnpm) echo "$PKG_CHECK" > package.json; echo "lockfileVersion: '9.0'" > pnpm-lock.yaml;; + nocheck) echo "$PKG_NOCHECK" > package.json; echo "lockfileVersion: '9.0'" > pnpm-lock.yaml;; + nolock) echo "$PKG_CHECK" > package.json;; + none) ;; + esac + git add -A; git commit -qm base + git checkout -qb pr + case $pr in + readme) echo more >> README.md;; + rmlock) git rm -q pnpm-lock.yaml;; + rmpkg) git rm -q package.json;; + rmcheck) echo "$PKG_NOCHECK" > package.json;; + badjson) echo "{not json" > package.json;; + rmall) git rm -q package.json pnpm-lock.yaml;; + addpnpm) echo "$PKG_CHECK" > package.json; echo "lockfileVersion: '9.0'" > pnpm-lock.yaml;; + addpartial) echo "$PKG_CHECK" > package.json;; + addcheck) echo "$PKG_CHECK" > package.json;; + esac + git add -A; git commit -qm pr + # Move main forward like a real base branch, then build the merge commit GitHub would. + git checkout -q main; echo base2 > other.txt; git add -A; git commit -qm base2 + if [ "$expect" = "nomerge" ]; then git checkout -q pr; else git checkout -q --detach main; git merge -q --no-ff --no-edit pr; fi + out=$(mktemp); : > "$out" + log=$(GITHUB_OUTPUT=$out GITHUB_BASE_REF=main bash -e "$SCRIPT" 2>&1); code=$? + run=$(grep -c '^run=true$' "$out") + if [ $code -ne 0 ]; then result=fail; elif [ "$run" = 1 ]; then result=run; else result=skip; fi + [ "$expect" = nomerge ] && expect=fail + status=$([ "$result" = "$expect" ] && echo PASS || echo MISMATCH) + printf '%-9s %-28s base=%-8s pr=%-10s expect=%-4s got=%-4s exit=%s\n %s\n' "$status" "$name" "$base" "$pr" "$expect" "$result" "$code" "$log" +} +scenario pnpm-readme pnpm readme run +scenario pnpm-remove-lockfile pnpm rmlock fail +scenario pnpm-remove-package pnpm rmpkg fail +scenario pnpm-remove-check pnpm rmcheck fail +scenario pnpm-broken-json pnpm badjson fail +scenario pnpm-remove-all pnpm rmall fail +scenario exempt-none-readme none readme skip +scenario exempt-none-addpnpm none addpnpm run +scenario exempt-none-partial none addpartial skip +scenario exempt-nocheck-readme nocheck readme skip +scenario exempt-nocheck-rmlock nocheck rmlock skip +scenario exempt-nocheck-addchk nocheck addcheck run +scenario exempt-nolock-readme nolock readme skip +scenario not-a-merge-commit pnpm readme nomerge +rm -rf "$root" diff --git a/.github/workflows/check-preview-tmp.yml b/.github/workflows/check-preview-tmp.yml new file mode 100644 index 0000000..491b828 --- /dev/null +++ b/.github/workflows/check-preview-tmp.yml @@ -0,0 +1,78 @@ +--- +name: Check preview (temporary) +"on": + pull_request: +permissions: + contents: read +jobs: + check: + name: Check + runs-on: ubuntu-latest + timeout-minutes: 15 + env: + pnpm_config_ignore_scripts: 'true' + pnpm_config_ignore_pnpmfile: 'true' + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + persist-credentials: false + fetch-depth: 2 + - name: Find the check script + id: scripts + run: | + # Prints what a commit lacks to be a pnpm project with a check script. + missing() { + git cat-file -e "$1:package.json" 2>/dev/null || echo "package.json" + git cat-file -e "$1:pnpm-lock.yaml" 2>/dev/null || echo "pnpm-lock.yaml" + git show "$1:package.json" 2>/dev/null | + node -e 'process.exit(JSON.parse(require("fs").readFileSync(0, "utf8")).scripts?.check ? 0 : 1)' 2>/dev/null || + echo "a check script in package.json" + } + + # A pull request is checked out as a merge commit: the base branch, then the pull request. + if ! git rev-parse --verify --quiet HEAD^2 >/dev/null; then + echo "::error::Expected a pull request merge commit with the base branch as its first parent." + exit 1 + fi + + head_missing=$(missing HEAD) + base_missing=$(missing HEAD^1) + + if [ -z "$head_missing" ]; then + echo "run=true" >> "$GITHUB_OUTPUT" + elif [ -z "$base_missing" ]; then + echo "::error::$GITHUB_BASE_REF is a pnpm project with a check script, but this pull request removes: $(echo "$head_missing" | paste -sd, - | sed 's/,/, /g'). Restore it so the check can run." + exit 1 + else + echo "No pnpm project with a check script on $GITHUB_BASE_REF or in this pull request. Nothing to run." + fi + - if: steps.scripts.outputs.run == 'true' + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 + with: + node-version: 24 + - if: steps.scripts.outputs.run == 'true' + run: corepack enable + - if: steps.scripts.outputs.run == 'true' + id: store + run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT" + - if: steps.scripts.outputs.run == 'true' + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 + with: + path: "${{ steps.store.outputs.path }}" + key: pnpm-store-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }} + restore-keys: pnpm-store-${{ runner.os }}- + - if: steps.scripts.outputs.run == 'true' + run: pnpm install --frozen-lockfile --ignore-scripts --ignore-pnpmfile + - if: steps.scripts.outputs.run == 'true' + run: pnpm check + fixtures: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + persist-credentials: false + - run: git config --global user.email t@t && git config --global user.name t && + node --version && yq -r '.jobs.check.steps[] | select(.id == "scripts") | + .run' .github/workflows/check.yml > find-check.sh && bash .github/tmp-test/fixtures.sh + | tee out.txt && ! grep -q MISMATCH out.txt From 89309a50a2af78fdf02b280d9f3efa0db44e7434 Mon Sep 17 00:00:00 2001 From: Sil Date: Sat, 3 Oct 2026 00:37:10 +0200 Subject: [PATCH 3/4] Temporary: fix the fixture directory Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/tmp-test/fixtures.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/tmp-test/fixtures.sh b/.github/tmp-test/fixtures.sh index 7c7aff4..f17c739 100755 --- a/.github/tmp-test/fixtures.sh +++ b/.github/tmp-test/fixtures.sh @@ -2,7 +2,7 @@ # Usage per scenario: base files spec, PR action. Runs the extracted step as GitHub would (bash -e). set -u SCRIPT=$PWD/find-check.sh -root=$(mktemp -d /tmp/sec-report/work/fx.XXXX) +root=$(mktemp -d) PKG_CHECK='{"name":"x","scripts":{"check":"prettier --check ."}}' PKG_NOCHECK='{"name":"x","scripts":{"build":"astro build"}}' scenario() { From a16fb0b153b3f72cd4b9efa79f2de3e3f21fb4e1 Mon Sep 17 00:00:00 2001 From: Sil Date: Sat, 3 Oct 2026 00:37:51 +0200 Subject: [PATCH 4/4] Remove the temporary preview workflow Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/tmp-test/fixtures.sh | 58 ------------------ .github/workflows/check-preview-tmp.yml | 78 ------------------------- 2 files changed, 136 deletions(-) delete mode 100755 .github/tmp-test/fixtures.sh delete mode 100644 .github/workflows/check-preview-tmp.yml diff --git a/.github/tmp-test/fixtures.sh b/.github/tmp-test/fixtures.sh deleted file mode 100755 index f17c739..0000000 --- a/.github/tmp-test/fixtures.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/bin/bash -# Usage per scenario: base files spec, PR action. Runs the extracted step as GitHub would (bash -e). -set -u -SCRIPT=$PWD/find-check.sh -root=$(mktemp -d) -PKG_CHECK='{"name":"x","scripts":{"check":"prettier --check ."}}' -PKG_NOCHECK='{"name":"x","scripts":{"build":"astro build"}}' -scenario() { - local name=$1 base=$2 pr=$3 expect=$4 - local d=$root/$name; mkdir -p "$d"; cd "$d" - git init -q -b main; git config user.email t@t; git config user.name t - echo readme > README.md - case $base in - pnpm) echo "$PKG_CHECK" > package.json; echo "lockfileVersion: '9.0'" > pnpm-lock.yaml;; - nocheck) echo "$PKG_NOCHECK" > package.json; echo "lockfileVersion: '9.0'" > pnpm-lock.yaml;; - nolock) echo "$PKG_CHECK" > package.json;; - none) ;; - esac - git add -A; git commit -qm base - git checkout -qb pr - case $pr in - readme) echo more >> README.md;; - rmlock) git rm -q pnpm-lock.yaml;; - rmpkg) git rm -q package.json;; - rmcheck) echo "$PKG_NOCHECK" > package.json;; - badjson) echo "{not json" > package.json;; - rmall) git rm -q package.json pnpm-lock.yaml;; - addpnpm) echo "$PKG_CHECK" > package.json; echo "lockfileVersion: '9.0'" > pnpm-lock.yaml;; - addpartial) echo "$PKG_CHECK" > package.json;; - addcheck) echo "$PKG_CHECK" > package.json;; - esac - git add -A; git commit -qm pr - # Move main forward like a real base branch, then build the merge commit GitHub would. - git checkout -q main; echo base2 > other.txt; git add -A; git commit -qm base2 - if [ "$expect" = "nomerge" ]; then git checkout -q pr; else git checkout -q --detach main; git merge -q --no-ff --no-edit pr; fi - out=$(mktemp); : > "$out" - log=$(GITHUB_OUTPUT=$out GITHUB_BASE_REF=main bash -e "$SCRIPT" 2>&1); code=$? - run=$(grep -c '^run=true$' "$out") - if [ $code -ne 0 ]; then result=fail; elif [ "$run" = 1 ]; then result=run; else result=skip; fi - [ "$expect" = nomerge ] && expect=fail - status=$([ "$result" = "$expect" ] && echo PASS || echo MISMATCH) - printf '%-9s %-28s base=%-8s pr=%-10s expect=%-4s got=%-4s exit=%s\n %s\n' "$status" "$name" "$base" "$pr" "$expect" "$result" "$code" "$log" -} -scenario pnpm-readme pnpm readme run -scenario pnpm-remove-lockfile pnpm rmlock fail -scenario pnpm-remove-package pnpm rmpkg fail -scenario pnpm-remove-check pnpm rmcheck fail -scenario pnpm-broken-json pnpm badjson fail -scenario pnpm-remove-all pnpm rmall fail -scenario exempt-none-readme none readme skip -scenario exempt-none-addpnpm none addpnpm run -scenario exempt-none-partial none addpartial skip -scenario exempt-nocheck-readme nocheck readme skip -scenario exempt-nocheck-rmlock nocheck rmlock skip -scenario exempt-nocheck-addchk nocheck addcheck run -scenario exempt-nolock-readme nolock readme skip -scenario not-a-merge-commit pnpm readme nomerge -rm -rf "$root" diff --git a/.github/workflows/check-preview-tmp.yml b/.github/workflows/check-preview-tmp.yml deleted file mode 100644 index 491b828..0000000 --- a/.github/workflows/check-preview-tmp.yml +++ /dev/null @@ -1,78 +0,0 @@ ---- -name: Check preview (temporary) -"on": - pull_request: -permissions: - contents: read -jobs: - check: - name: Check - runs-on: ubuntu-latest - timeout-minutes: 15 - env: - pnpm_config_ignore_scripts: 'true' - pnpm_config_ignore_pnpmfile: 'true' - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - with: - persist-credentials: false - fetch-depth: 2 - - name: Find the check script - id: scripts - run: | - # Prints what a commit lacks to be a pnpm project with a check script. - missing() { - git cat-file -e "$1:package.json" 2>/dev/null || echo "package.json" - git cat-file -e "$1:pnpm-lock.yaml" 2>/dev/null || echo "pnpm-lock.yaml" - git show "$1:package.json" 2>/dev/null | - node -e 'process.exit(JSON.parse(require("fs").readFileSync(0, "utf8")).scripts?.check ? 0 : 1)' 2>/dev/null || - echo "a check script in package.json" - } - - # A pull request is checked out as a merge commit: the base branch, then the pull request. - if ! git rev-parse --verify --quiet HEAD^2 >/dev/null; then - echo "::error::Expected a pull request merge commit with the base branch as its first parent." - exit 1 - fi - - head_missing=$(missing HEAD) - base_missing=$(missing HEAD^1) - - if [ -z "$head_missing" ]; then - echo "run=true" >> "$GITHUB_OUTPUT" - elif [ -z "$base_missing" ]; then - echo "::error::$GITHUB_BASE_REF is a pnpm project with a check script, but this pull request removes: $(echo "$head_missing" | paste -sd, - | sed 's/,/, /g'). Restore it so the check can run." - exit 1 - else - echo "No pnpm project with a check script on $GITHUB_BASE_REF or in this pull request. Nothing to run." - fi - - if: steps.scripts.outputs.run == 'true' - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 - with: - node-version: 24 - - if: steps.scripts.outputs.run == 'true' - run: corepack enable - - if: steps.scripts.outputs.run == 'true' - id: store - run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT" - - if: steps.scripts.outputs.run == 'true' - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - with: - path: "${{ steps.store.outputs.path }}" - key: pnpm-store-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }} - restore-keys: pnpm-store-${{ runner.os }}- - - if: steps.scripts.outputs.run == 'true' - run: pnpm install --frozen-lockfile --ignore-scripts --ignore-pnpmfile - - if: steps.scripts.outputs.run == 'true' - run: pnpm check - fixtures: - runs-on: ubuntu-latest - timeout-minutes: 10 - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - with: - persist-credentials: false - - run: git config --global user.email t@t && git config --global user.name t && - node --version && yq -r '.jobs.check.steps[] | select(.id == "scripts") | - .run' .github/workflows/check.yml > find-check.sh && bash .github/tmp-test/fixtures.sh - | tee out.txt && ! grep -q MISMATCH out.txt