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
3 changes: 3 additions & 0 deletions .standards-check-ignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Repos that never get standards-check.yml installed by
# bulk-install-standards-check.sh. One owner/repo per line; # is a comment.
nightowlstudiollc/networth-agent
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,9 @@ jobs:
uses: smartwatermelon/github-workflows/.github/workflows/standards-check.yml@standards-check-v1
```

The canonical copy is `standards/caller-stub.yml`;
`bulk-install-standards-check.sh` installs it fleet-wide.

Name the caller job `standards-check`; the required check is then
`standards-check / run-standards-check` (caller job `standards-check`, inner
job `run-standards-check`). With reusable workflows, GitHub reports the
Expand Down
227 changes: 227 additions & 0 deletions bulk-install-standards-check.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
#!/usr/bin/env bash
# Install the standards-check.yml caller stub across the fleet.
#
# Classifies every non-archived repo of --owners (plus --extra-repos) as
# MISSING / CURRENT / DIFFERS / IGNORED / ARCHIVED / ERROR and, with --apply,
# writes the canonical stub (standards/caller-stub.yml) into MISSING repos.
#
# --mode=pr branch chore/standards-check-stub + Contents-API put + PR
# --mode=push Contents-API put straight onto the default branch. This is
# the W2 Phase-2 mechanism Andrew authorized on 2026-09-08 for
# this one file only (dev-env docs/superpowers/plans/
# 2026-09-08-w2-fleet-rollout.md, "Decisions"). Never extend
# it to other content.
#
# Dry run is the default. DIFFERS repos are reported and never touched.
set -euo pipefail

here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
GH="${BULK_GH:-gh}"
STUB_FILE="${BULK_STUB_FILE:-${here}/standards/caller-stub.yml}"
IGNORE_FILE="${BULK_IGNORE_FILE:-${here}/.standards-check-ignore}"
INSTALL_PATH=".github/workflows/standards-check.yml"
BRANCH="chore/standards-check-stub"
SESSION="https://claude.ai/code/session_019HDRKLQNv82SEBd4zGpcXf"

APPLY=false
MODE="pr"
ONLY=""
OWNERS="smartwatermelon,nightowlstudiollc"
EXTRA="twistedmelonman/dotfiles,twistedmelonman/claude-config,twistedmelonman/personify,twistedmelonman/huddle-transcribe,twistedmelonman/projectinsomnia"

usage() {
cat <<EOF
Usage: ${0##*/} [--apply] [--mode=pr|push] [--only owner/repo] [--owners a,b] [--extra-repos o/r,...]
EOF
}

while [[ $# -gt 0 ]]; do
case "$1" in
--apply) APPLY=true; shift ;;
--mode=pr | --mode=push) MODE="${1#--mode=}"; shift ;;
--only) ONLY="$2"; shift 2 ;;
--owners) OWNERS="$2"; shift 2 ;;
--extra-repos) EXTRA="$2"; shift 2 ;;
-h | --help) usage; exit 0 ;;
*) echo "unknown argument: $1" >&2; usage >&2; exit 2 ;;
esac
done

[[ -f "${STUB_FILE}" ]] || {
echo "stub file not found: ${STUB_FILE}" >&2
exit 2
}
STUB_B64="$(base64 <"${STUB_FILE}" | tr -d '\n')"

is_ignored() {
[[ -f "${IGNORE_FILE}" ]] || return 1
grep -v '^[[:space:]]*#' "${IGNORE_FILE}" | grep -qx "$1"
}

# Emits "owner/repo<TAB>archived(true|false)" lines.
list_repos() {
local owner r
local -a owners extras
IFS=',' read -r -a owners <<<"${OWNERS}"
for owner in "${owners[@]}"; do
[[ -n "${owner}" ]] || continue
"${GH}" repo list "${owner}" --json name,isArchived --limit 200 |
jq -r --arg o "${owner}" '.[] | "\($o)/\(.name)\t\(.isArchived)"'
done
if [[ -n "${EXTRA}" ]]; then
IFS=',' read -r -a extras <<<"${EXTRA}"
for r in "${extras[@]}"; do
[[ -n "${r}" ]] && printf '%s\tfalse\n' "${r}"
done
fi
}

# owner/repo -> prints the file's content, base64 with no newlines.
# Returns 0 with content, 1 for a confirmed HTTP 404 (the file is absent), or
# 2 with the failure message for any other failure. A 403, a rate limit, or a
# network error must NOT read as "absent": under --apply --mode=push that
# would PUT the stub onto the default branch of a repo whose state is unknown.
# Compared byte-for-byte against STUB_B64, so a
# trailing-newline difference is a real difference and shows up as DIFFERS.
#
# The API wraps .content in 60-column lines, so it is NOT comparable to
# STUB_B64 as returned. Stripping those newlines and round-tripping through
# base64 -d | base64 renormalizes it to the same single-line encoding
# STUB_B64 uses; the decode/encode pair is a canonicalization, not a
# double-encode. Comparing raw decoded text instead would not work: $(...)
# strips trailing newlines, so a correct stub could never match.
existing_b64() {
local body rc=0
# stderr is captured, not discarded: it carries the "(HTTP <code>)" line that
# distinguishes a real 404 from every other failure. Real `gh api` prints
# "gh: Not Found (HTTP 404)" there and exits 1 (measured 2026-09-08).
body="$("${GH}" api "repos/$1/contents/${INSTALL_PATH}" 2>&1)" || rc=$?
if [[ "${rc}" -ne 0 ]]; then
if [[ "${body}" == *"(HTTP 404)"* ]]; then
return 1
fi
printf '%s' "${body}"
return 2
fi
jq -r '.content' <<<"${body}" | tr -d '\n' | base64 -d | base64 | tr -d '\n'
}

default_branch() { "${GH}" api "repos/$1" --jq .default_branch; }

put_file() { # owner/repo branch [blob-sha]
local -a sha_arg=()
# The Contents API requires the current blob sha to overwrite an existing
# file. A retry after a partial run finds the file already on the branch.
[[ -n "${3:-}" ]] && sha_arg=(-f "sha=$3")
"${GH}" api -X PUT "repos/$1/contents/${INSTALL_PATH}" \
-f message="ci: add standards-check.yml caller stub (non-required)

Installs the deterministic standards check as a non-required status.
It becomes required per repo in W3 once the repo is green.

Claude-Session: ${SESSION}" \
-f content="${STUB_B64}" \
"${sha_arg[@]}" \
-f branch="$2" >/dev/null
}

# Every step below is guarded with `|| return 1`. These functions run inside a
# command substitution in an `if`, which suspends `set -e` for everything they
# call, so an unguarded failure would fall through: an empty default branch
# would still reach the PUT, and a failed ref POST would still open a PR.
install_push() { # owner/repo
local db
db="$(default_branch "$1")" || return 1
[[ -n "${db}" ]] || return 1
put_file "$1" "${db}" || return 1
echo "pushed to ${db}"
}

# Retry-safe: a previous partial run may have left the branch, and the file on
# it, behind. Reuse both rather than failing the repo on a duplicate ref POST.
install_pr() { # owner/repo
local db sha blob=""
db="$(default_branch "$1")" || return 1
[[ -n "${db}" ]] || return 1
if "${GH}" api "repos/$1/git/ref/heads/${BRANCH}" >/dev/null 2>&1; then
# The branch already exists; the file may too, in which case the PUT needs
# its blob sha. A 404 here just means the branch is empty of the stub.
# The ref goes in the query string, not in -f: `gh api` switches the method
# to POST as soon as any -f parameter is added (`gh api --help`), which
# would turn this read into a failing write and silently drop the sha.
blob="$("${GH}" api "repos/$1/contents/${INSTALL_PATH}?ref=${BRANCH}" --jq .sha 2>/dev/null)" || blob=""
else
sha="$("${GH}" api "repos/$1/git/ref/heads/${db}" --jq .object.sha)" || return 1
[[ -n "${sha}" ]] || return 1
"${GH}" api -X POST "repos/$1/git/refs" -f ref="refs/heads/${BRANCH}" -f sha="${sha}" >/dev/null || return 1
fi
put_file "$1" "${BRANCH}" "${blob}" || return 1
"${GH}" pr create --repo "$1" --head "${BRANCH}" --base "${db}" \
--title "ci: add standards-check.yml caller stub (non-required)" \
--body "Installs \`standards-check.yml\` as a NON-required check. Branch protection is unchanged; W3 flips it to required once this repo is green.

Reusable workflow: smartwatermelon/github-workflows \`standards-check.yml@standards-check-v1\`.

${SESSION}" || return 1
}

# Dispatch table: the two install functions are reached through this map, not
# by name interpolation, so a bad --mode can never name an arbitrary function.
install_repo() { # mode owner/repo
case "$1" in
push) install_push "$2" ;;
pr) install_pr "$2" ;;
*) echo "unknown mode: $1" >&2; return 2 ;;
esac
}

# Enumerate once into a file: a process substitution here would mask
# list_repos' exit status, so a `gh repo list` failure would read as an empty
# fleet and the script would exit 0 having done nothing.
repo_list="$(mktemp)"
trap 'rm -f "${repo_list}"' EXIT
list_repos >"${repo_list}"

rc=0
while IFS=$'\t' read -r repo archived; do
[[ -n "${repo}" ]] || continue
if [[ -n "${ONLY}" && "${repo}" != "${ONLY}" ]]; then continue; fi
if [[ "${archived}" == "true" ]]; then
printf 'ARCHIVED %s\n' "${repo}"
continue
fi
if is_ignored "${repo}"; then
printf 'IGNORED %s\n' "${repo}"
continue
fi
lookup_rc=0
current="$(existing_b64 "${repo}")" || lookup_rc=$?
case "${lookup_rc}" in
0)
if [[ "${current}" == "${STUB_B64}" ]]; then
printf 'CURRENT %s\n' "${repo}"
else
printf 'DIFFERS %s (has a non-canonical stub; not touched)\n' "${repo}"
fi
continue
;;
1) : ;; # confirmed 404: the file is absent, fall through to MISSING
*)
# One line per repo is the output contract; API errors are multi-line.
printf 'ERROR %s %s\n' "${repo}" "${current//$'\n'/ }"
rc=1
continue
;;
esac
if ! ${APPLY}; then
printf 'MISSING %s (dry run; would %s)\n' "${repo}" "${MODE}"
continue
fi
if out="$(install_repo "${MODE}" "${repo}" 2>&1)"; then
printf 'MISSING %s -> %s\n' "${repo}" "${out}"
else
printf 'ERROR %s %s\n' "${repo}" "${out//$'\n'/ }"
rc=1
fi
done <"${repo_list}"
exit "${rc}"
9 changes: 9 additions & 0 deletions standards/caller-stub.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
name: Standards Check
on:
pull_request:
types: [opened, synchronize, ready_for_review, reopened]
permissions:
contents: read
jobs:
standards-check:
uses: smartwatermelon/github-workflows/.github/workflows/standards-check.yml@standards-check-v1
126 changes: 126 additions & 0 deletions tests/stub-gh/gh
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
#!/usr/bin/env bash
# Stub gh for bulk-install-standards-check.sh tests. Fixture layout:
# ${STUB_DIR}/repos/<owner>.json -> output of `gh repo list <owner> --json ...`
# ${STUB_DIR}/files/<owner>__<repo> -> existing stub content, if the repo has one
# ${STUB_DIR}/fail/<owner>__<repo> -> contents GET fails 403 instead of 404
# ${STUB_DIR}/branch-exists/<o>__<r> -> the chore/standards-check-stub ref exists
# ${STUB_DIR}/branch-files/<o>__<r> -> file already present on that branch
# ${STUB_DIR}/calls.log -> every argv, one line each
#
# Failure output copies real `gh api`: the JSON body on stdout, a
# "gh: <reason> (HTTP <code>)" line on stderr, exit 1. Measured 2026-09-08
# against `gh api repos/smartwatermelon/does-not-exist-xyz-9876`.
# Archived state comes from the isArchived field in repos/<owner>.json.
set -euo pipefail
: "${STUB_DIR:?}"
# One line per invocation: arguments carry multi-line bodies (commit message,
# PR body), so escape newlines rather than break the log's one-line contract.
printf '%s\n' "${*//$'\n'/\\n}" >>"${STUB_DIR}/calls.log"
case "$1" in
repo)
# gh repo list <owner> --json name,isArchived,defaultBranchRef --limit 200
owner="$3"
cat "${STUB_DIR}/repos/${owner}.json"
;;
api)
shift
method="GET"
path=""
filter=""
ref_param=""
while [[ $# -gt 0 ]]; do
case "$1" in
-X) method="$2"; shift 2 ;;
--jq) filter="$2"; shift 2 ;;
-f | -F) shift 2 ;;
-*) shift ;;
*) path="$1"; shift ;;
esac
done
# A ref is only honored as a query parameter. `gh api` turns any -f into a
# POST body, so a caller that passed the ref that way would not be doing a
# branch-scoped read at all; the fixture must not pretend otherwise.
if [[ "${path}" == *"?"* ]]; then
query="${path#*\?}"
path="${path%%\?*}"
[[ "${query}" == ref=* ]] && ref_param="${query#ref=}"
fi
# Real gh applies --jq to the response and prints raw strings; do the same,
# or callers reading `--jq .default_branch` get a JSON object back.
emit() {
if [[ -n "${filter}" ]]; then
jq -r "${filter}"
else
cat
fi
}
case "${method} ${path}" in
"GET repos/"*"/contents/.github/workflows/standards-check.yml")
# repos/<owner>/<repo>/contents/...
rest="${path#repos/}"; owner="${rest%%/*}"; rest="${rest#*/}"; repo="${rest%%/*}"
if [[ -f "${STUB_DIR}/fail/${owner}__${repo}" ]]; then
printf '{"message":"Forbidden","status":"403"}'
echo 'gh: Forbidden (HTTP 403)' >&2
exit 1
fi
if [[ -n "${ref_param}" && "${ref_param}" != "main" ]]; then
# A read scoped to the feature branch: a separate fixture, so a test
# can tell "already on the branch" from "on the default branch".
bf="${STUB_DIR}/branch-files/${owner}__${repo}"
if [[ -f "${bf}" ]]; then
echo '{"sha":"branchblob"}' | emit
else
printf '{"message":"Not Found","status":"404"}'
echo 'gh: Not Found (HTTP 404)' >&2
exit 1
fi
exit 0
fi
f="${STUB_DIR}/files/${owner}__${repo}"
if [[ -f "${f}" ]]; then
# The real Contents API wraps .content in 60-column lines with
# embedded "\n" escapes. Reproduce that so the caller's decode path
# is exercised as it will run in production, not a flat-base64
# simplification that would hide a newline-handling bug.
b64="$(base64 <"${f}")"
b64="$(tr -d '\n' <<<"${b64}")"
b64="$(fold -w 60 <<<"${b64}")"
b64="${b64//$'\n'/\\n}"
printf '{"sha":"deadbeef","content":"%s"}\n' "${b64}" | emit
else
printf '{"message":"Not Found","status":"404"}'
echo 'gh: Not Found (HTTP 404)' >&2
exit 1
fi ;;
"GET repos/"*"/git/ref/heads/chore/standards-check-stub")
rest="${path#repos/}"; owner="${rest%%/*}"; rest="${rest#*/}"; repo="${rest%%/*}"
if [[ -f "${STUB_DIR}/branch-exists/${owner}__${repo}" ]]; then
echo '{"object":{"sha":"branchsha"}}' | emit
else
printf '{"message":"Not Found","status":"404"}'
echo 'gh: Not Found (HTTP 404)' >&2
exit 1
fi ;;
"GET repos/"*"/git/ref/heads/"*)
echo '{"object":{"sha":"abc123"}}' | emit ;;
"GET repos/"*)
# default-branch lookup: repos/<owner>/<repo>
rest="${path#repos/}"; owner="${rest%%/*}"; rest="${rest#*/}"; repo="${rest%%/*}"
if [[ -f "${STUB_DIR}/fail-repo/${owner}__${repo}" ]]; then
printf '{"message":"Forbidden","status":"403"}'
echo 'gh: Forbidden (HTTP 403)' >&2
exit 1
fi
echo '{"default_branch":"main"}' | emit ;;
"PUT repos/"*"/contents/"*)
echo '{"content":{"path":".github/workflows/standards-check.yml"},"commit":{"sha":"c0ffee"}}' | emit ;;
"POST repos/"*"/git/refs")
echo '{"ref":"refs/heads/x"}' | emit ;;
*) echo "stub gh: unhandled ${method} ${path}" >&2; exit 99 ;;
esac
;;
pr)
echo "https://github.com/stub/pr/1"
;;
*) echo "stub gh: unhandled $*" >&2; exit 99 ;;
esac
Loading