Skip to content

Commit b9b9e2b

Browse files
Release: staging -> main (2026-07-27) (#7)
* clean template metadata and protections * optimize contribution guide * fix pull request existence checks * enable optional turborepo remote caching * strengthen agent instructions * standardize language formatting and linting * clean template metadata and protections * chore: refine shared template configuration * fix: install Rust CI components * fix: use workflow token for release PRs * chore: ignore generated project artifacts * chore: ignore process files * feat: add reusable setup and bootstrap tooling * docs: standardize on Bun native tests * feat: add template sync and repository doctor checks * feat: add one-command repository initializer * feat: add coverage policy and protection sync * fix: handle empty preserved checks * fix: update branch protections through full API * fix: handle unsupported protection APIs * fix: isolate Python coverage from mise environment * fix: stabilize coverage and audit exceptions * fix: enforce bun coverage in shared CI * fix: pass explicit Python coverage sources * fix: support macOS bash in CI helpers * fix: isolate Python coverage sysconfig workaround * style: format Python coverage shim * fix: ignore shared helper files in language detection * fix: recognize shared Bun coverage policy * fix: detect repository Python environments * fix: detect native Bun coverage runners * fix: provide CI-only build secret fallback * fix: remove obsolete slither protection check * feat: preserve custom workflows during sync * fix: enforce function and line coverage * fix: address template review feedback --------- Co-authored-by: NiftyAndy <andy@niftyleague.com>
1 parent a338e89 commit b9b9e2b

10 files changed

Lines changed: 262 additions & 13 deletions

File tree

‎.github/scripts/ci.sh‎

Lines changed: 84 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,21 @@ has_script() {
77
[ -f package.json ] && node -e 'const p=require("./package.json"); process.exit(p.scripts?.[process.argv[1]] ? 0 : 1)' "$1"
88
}
99

10+
has_bun_native_coverage() {
11+
[ -f package.json ] && node -e 'const p=require("./package.json"); process.exit(p.scripts?.["test:coverage"]?.includes("bun test") ? 0 : 1)'
12+
}
13+
1014
has_javascript() {
1115
git ls-files -- '*.js' '*.jsx' '*.ts' '*.tsx' | grep -q .
1216
}
1317

1418
has_python() {
1519
[ -f pyproject.toml ] || [ -f requirements.txt ] || [ -f requirements-dev.txt ] || \
16-
git ls-files -- '*.py' | grep -q .
20+
git ls-files -- '*.py' ':!.github/**' | grep -q .
21+
}
22+
23+
has_graph_project() {
24+
[ -f package.json ] && node -e 'const p=require("./package.json"); process.exit(p.devDependencies?.["@graphprotocol/graph-cli"] ? 0 : 1)'
1725
}
1826

1927
package_manager() {
@@ -44,6 +52,45 @@ run_package_tool() {
4452
esac
4553
}
4654

55+
run_bun_coverage() {
56+
local log_file coverage_line functions lines minimum
57+
log_file="$(mktemp)"
58+
if ! run_script test:coverage 2>&1 | tee "$log_file"; then
59+
rm -f "$log_file"
60+
return 1
61+
fi
62+
coverage_line="$(grep -E '^All files[[:space:]]*\|' "$log_file" | tail -n 1 || true)"
63+
if [ -z "$coverage_line" ]; then
64+
echo "Coverage summary not found in test:coverage output" >&2
65+
rm -f "$log_file"
66+
return 1
67+
fi
68+
functions="$(printf '%s\n' "$coverage_line" | awk -F'|' '{gsub(/[[:space:]]/, "", $4); print $4}')"
69+
lines="$(printf '%s\n' "$coverage_line" | awk -F'|' '{gsub(/[[:space:]]/, "", $5); print $5}')"
70+
minimum="${BUN_COVERAGE_MIN:-80}"
71+
if ! awk -v functions="$functions" -v lines="$lines" -v minimum="$minimum" \
72+
'BEGIN { exit !(functions + 0 >= minimum && lines + 0 >= minimum) }'; then
73+
echo "Coverage below ${minimum}%: functions=${functions}% lines=${lines}%" >&2
74+
rm -f "$log_file"
75+
return 1
76+
fi
77+
echo "Coverage threshold passed: functions=${functions}% lines=${lines}% (minimum ${minimum}%)"
78+
rm -f "$log_file"
79+
}
80+
81+
python_coverage_args() {
82+
if [ -f pyproject.toml ]; then
83+
python - <<'PY'
84+
import tomllib
85+
from pathlib import Path
86+
87+
config = tomllib.loads(Path("pyproject.toml").read_text())
88+
for source in config.get("tool", {}).get("coverage", {}).get("run", {}).get("source", []):
89+
print(f"--cov={source}")
90+
PY
91+
fi
92+
}
93+
4794
install() {
4895
if [ -f package.json ]; then
4996
case "$(package_manager)" in
@@ -67,6 +114,12 @@ rust_component() {
67114
fi
68115
}
69116

117+
has_rust_target() {
118+
local kind="$1"
119+
cargo metadata --no-deps --format-version 1 |
120+
jq -e --arg kind "$kind" 'any(.packages[].targets[]; (.kind | index($kind)) != null)' >/dev/null
121+
}
122+
70123
format() {
71124
if has_script format:check; then run_script format:check
72125
elif has_javascript; then run_package_tool prettier --check .
@@ -84,7 +137,9 @@ lint() {
84137
}
85138

86139
type_check() {
87-
if has_script type-check; then run_script type-check
140+
if has_graph_project; then
141+
echo "Skipping TypeScript type-check (Graph AssemblyScript project uses graph build/codegen)"
142+
elif has_script type-check; then run_script type-check
88143
elif has_script typecheck; then run_script typecheck
89144
else echo "Skipping type-check (script not defined)"; fi
90145
if [ -f Cargo.toml ]; then cargo check; fi
@@ -95,25 +150,47 @@ type_check() {
95150
fi
96151
}
97152

98-
build() { run_script build; }
153+
build() {
154+
# Some frameworks validate session secrets while statically collecting pages.
155+
# Keep CI builds deterministic without weakening runtime/deployment validation.
156+
if [ "${CI:-}" = true ] && [ -z "${NEXTAUTH_SECRET:-}" ]; then
157+
export NEXTAUTH_SECRET="ci-only-build-secret-not-for-runtime-0123456789"
158+
fi
159+
run_script build
160+
if [ -f Cargo.toml ]; then cargo build --all-targets --all-features; fi
161+
}
99162

100163
unit() {
101164
# Bun repositories should expose test scripts backed by Bun's native runner.
102165
# Specialized repositories may keep their native runner (for example Matchstick or Hardhat).
103166
if has_script test:unit; then
104167
run_script test:unit
105168
elif has_script test:coverage; then
106-
run_script test:coverage
169+
if [ "$(package_manager)" = bun ] && has_bun_native_coverage; then
170+
run_bun_coverage
171+
else
172+
run_script test:coverage
173+
fi
107174
elif has_script test && ! has_script test:integration; then
108175
run_script test
109176
else
110177
echo "Skipping JavaScript/TypeScript unit tests (script not defined)"
111178
fi
112-
if [ -f Cargo.toml ]; then cargo test --lib --all-features; fi
179+
if [ -f Cargo.toml ]; then
180+
if has_rust_target lib; then cargo test --lib --all-features
181+
elif has_rust_target bin; then cargo test --bins --all-features
182+
else echo "Skipping Rust unit tests (no library or binary target)"; fi
183+
fi
113184
if [ -d tests/unit ] && python -c 'import importlib.util; raise SystemExit(importlib.util.find_spec("pytest") is None)' 2>/dev/null; then
114-
python -m pytest -q tests/unit --cov --cov-report=term-missing
185+
coverage_args=()
186+
while IFS= read -r arg; do [ -n "$arg" ] && coverage_args+=("$arg"); done < <(python_coverage_args)
187+
[ "${#coverage_args[@]}" -gt 0 ] || coverage_args=(--cov)
188+
env -u MISE_GITHUB_TOKEN -u MISE_TRUSTED_CONFIG_PATHS -u MISE_YES -u MISE_LOG_LEVEL -u PYTHONHOME PYTHONPATH="$PWD/.github/scripts" python -m pytest -q tests/unit "${coverage_args[@]}" --cov-report=term-missing --cov-fail-under="${PYTHON_COVERAGE_MIN:-80}"
115189
elif [ -d tests ] && [ ! -d tests/integration ] && python -c 'import importlib.util; raise SystemExit(importlib.util.find_spec("pytest") is None)' 2>/dev/null; then
116-
python -m pytest -q tests --cov --cov-report=term-missing
190+
coverage_args=()
191+
while IFS= read -r arg; do [ -n "$arg" ] && coverage_args+=("$arg"); done < <(python_coverage_args)
192+
[ "${#coverage_args[@]}" -gt 0 ] || coverage_args=(--cov)
193+
env -u MISE_GITHUB_TOKEN -u MISE_TRUSTED_CONFIG_PATHS -u MISE_YES -u MISE_LOG_LEVEL -u PYTHONHOME PYTHONPATH="$PWD/.github/scripts" python -m pytest -q tests "${coverage_args[@]}" --cov-report=term-missing --cov-fail-under="${PYTHON_COVERAGE_MIN:-80}"
117194
else
118195
echo "Skipping Python unit tests (no unit suite detected)"
119196
fi

‎.github/scripts/codeql-languages.sh‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ set -euo pipefail
44
languages=()
55
if find .github/workflows -type f \( -name '*.yml' -o -name '*.yaml' \) -print -quit | grep -q .; then languages+=(actions); fi
66
if git ls-files -- '*.ts' '*.tsx' '*.js' '*.jsx' 'package.json' 'tsconfig*.json' | grep -q .; then languages+=(javascript-typescript); fi
7-
if git ls-files -- '*.py' 'pyproject.toml' 'requirements*.txt' 'setup.py' | grep -q .; then languages+=(python); fi
7+
if git ls-files -- '*.py' 'pyproject.toml' 'requirements*.txt' 'setup.py' ':!.github/**' | grep -q .; then languages+=(python); fi
88
if git ls-files -- '*.rs' 'Cargo.toml' 'Cargo.lock' | grep -q .; then languages+=(rust); fi
99

1010
json=$(printf '%s\n' "${languages[@]}" | jq -Rsc 'split("\n") | map(select(length > 0) | {language: ., name: (if . == "javascript-typescript" then "TypeScript" elif . == "actions" then "Actions" else (. | ascii_upcase[0:1] + .[1:]) end), "build-mode": "none"})')

‎.github/scripts/doctor.sh‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,15 @@ if [ -f package.json ]; then
4545
warn "JavaScript/TypeScript sources found but no test or test:unit/test:integration script is defined"
4646
fi
4747
fi
48+
if [ -f bunfig.toml ] && node -e 'const p=require("./package.json"); process.exit(p.scripts?.["test:coverage"] ? 0 : 1)' 2>/dev/null; then
49+
if ! grep -q 'coverageThreshold' bunfig.toml; then
50+
if [ -x .github/scripts/ci.sh ]; then
51+
printf '%s\n' "INFO: shared CI enforces the Bun aggregate coverage threshold"
52+
else
53+
error "Bun coverage is enabled by test:coverage but no coverage policy is configured"
54+
fi
55+
fi
56+
fi
4857
fi
4958

5059
if [ -f Cargo.toml ]; then
@@ -53,14 +62,16 @@ if [ -f Cargo.toml ]; then
5362
fi
5463

5564
if [ -f pyproject.toml ] || [ -f requirements.txt ] || [ -f requirements-dev.txt ]; then
56-
command -v python >/dev/null 2>&1 || error "Python is required for this repository"
65+
if ! command -v python >/dev/null 2>&1 && [ ! -x .venv/bin/python ]; then
66+
error "Python is required for this repository"
67+
fi
5768
fi
5869

5970
for workflow in ci.yml codeql.yml security.yml test.yml draft-pr.yml release-pr.yml release.yml; do
6071
[ -f ".github/workflows/$workflow" ] || error "missing standard workflow: $workflow"
6172
done
6273

63-
for script in ci.sh codeql-languages.sh security.sh doctor.sh bootstrap.sh sync-template.sh init-repo.sh; do
74+
for script in ci.sh codeql-languages.sh security.sh doctor.sh bootstrap.sh sync-template.sh init-repo.sh sync-protection.sh; do
6475
[ -x ".github/scripts/$script" ] || error "missing executable script: .github/scripts/$script"
6576
done
6677

‎.github/scripts/init-repo.sh‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@ set -euo pipefail
33

44
source="https://github.com/0xPlayerOne/template-repo.git"
55
ref="main"
6+
protection=false
67

78
while [ "$#" -gt 0 ]; do
89
case "$1" in
910
--source) source="${2:?missing source path or URL}"; shift 2 ;;
1011
--ref) ref="${2:?missing ref}"; shift 2 ;;
12+
--protection) protection=true; shift ;;
1113
-h|--help)
12-
printf '%s\n' 'Usage: init-repo.sh [--source PATH_OR_URL] [--ref REF]'
14+
printf '%s\n' 'Usage: init-repo.sh [--source PATH_OR_URL] [--ref REF] [--protection]'
1315
exit 0
1416
;;
1517
*)
@@ -21,3 +23,10 @@ done
2123

2224
bash .github/scripts/sync-template.sh --source "$source" --ref "$ref" --apply
2325
bash .github/scripts/bootstrap.sh
26+
27+
if [ "$protection" = true ]; then
28+
remote="$(git remote get-url origin 2>/dev/null || true)"
29+
repo="$(printf '%s\n' "$remote" | sed -E 's#.*github.com[:/]([^/]+/[^/.]+)(\.git)?$#\1#')"
30+
[ -n "$repo" ] || { echo 'Could not determine GitHub repository from origin' >&2; exit 1; }
31+
bash .github/scripts/sync-protection.sh --repo "$repo" --apply
32+
fi

‎.github/scripts/security.sh‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,15 @@ package_manager() {
1313
}
1414

1515
if [ -f package.json ]; then
16+
audit_args=(--audit-level=high)
17+
if [ -f .github/security-audit-allowlist.txt ]; then
18+
while IFS= read -r advisory; do
19+
[[ -z "$advisory" || "$advisory" == \#* ]] && continue
20+
audit_args+=(--ignore "$advisory")
21+
done < .github/security-audit-allowlist.txt
22+
fi
1623
case "$(package_manager)" in
17-
bun) audits=$((audits + 1)); bun audit --audit-level=high ;;
24+
bun) audits=$((audits + 1)); bun audit "${audit_args[@]}" ;;
1825
pnpm) audits=$((audits + 1)); corepack pnpm audit --audit-level high ;;
1926
yarn) audits=$((audits + 1)); corepack yarn npm audit --all --recursive ;;
2027
npm) audits=$((audits + 1)); npm audit --audit-level=high ;;

‎.github/scripts/sitecustomize.py‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
"""Keep coverage.py usable with runners that ship malformed sysconfig schemes."""
2+
3+
import sysconfig
4+
5+
_get_paths = sysconfig.get_paths
6+
7+
8+
def get_paths(scheme=None, vars=None, expand=True):
9+
try:
10+
return _get_paths(scheme, vars, expand)
11+
except ValueError as error:
12+
if "Single '}' encountered in format string" not in str(error):
13+
raise
14+
return {}
15+
16+
17+
sysconfig.get_paths = get_paths

‎.github/scripts/sync-protection.sh‎

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
branch="main"
5+
repo=""
6+
mode="check"
7+
8+
usage() {
9+
printf '%s\n' 'Usage: sync-protection.sh --repo OWNER/REPO [--branch main] [--check] [--apply]'
10+
}
11+
12+
while [ "$#" -gt 0 ]; do
13+
case "$1" in
14+
--repo) repo="${2:?missing OWNER/REPO}"; shift 2 ;;
15+
--branch) branch="${2:?missing branch}"; shift 2 ;;
16+
--check) mode="check"; shift ;;
17+
--apply) mode="apply"; shift ;;
18+
-h|--help) usage; exit 0 ;;
19+
*) usage >&2; exit 2 ;;
20+
esac
21+
done
22+
23+
[ -n "$repo" ] || { usage >&2; exit 2; }
24+
command -v gh >/dev/null 2>&1 || { echo "gh is required" >&2; exit 1; }
25+
26+
is_private="$(gh repo view "$repo" --json isPrivate --jq '.isPrivate')"
27+
contexts=(Format Lint Type-Check Build Unit Integration E2E Smoke 'Dependency Audit' Detect)
28+
29+
if [ "$is_private" != true ]; then
30+
contexts+=("Analyze (Actions)")
31+
if git ls-files -- '*.ts' '*.tsx' '*.js' '*.jsx' package.json tsconfig\*.json | grep -q .; then contexts+=("Analyze (TypeScript)"); fi
32+
if git ls-files -- '*.py' pyproject.toml requirements\*.txt setup.py ':!.github/**' | grep -q .; then contexts+=("Analyze (Python)"); fi
33+
if git ls-files -- '*.rs' Cargo.toml Cargo.lock | grep -q .; then contexts+=("Analyze (Rust)"); fi
34+
fi
35+
36+
protection="$(gh api "repos/$repo/branches/$branch/protection" 2>/dev/null || true)"
37+
if ! jq -e 'type == "object" and (.required_status_checks | type == "object")' >/dev/null 2>&1 <<< "$protection"; then
38+
if [ "$mode" = apply ]; then
39+
echo "Cannot read branch protection for $repo:$branch (repository plan or permissions may not allow it)." >&2
40+
exit 1
41+
fi
42+
existing=""
43+
else
44+
existing="$(jq -r '.required_status_checks.contexts[]?' <<< "$protection")"
45+
fi
46+
preserved=()
47+
while IFS= read -r context; do
48+
[ -n "$context" ] || continue
49+
case "$context" in
50+
'Slither / Analyze') continue ;; # removed from the standard workflow set
51+
'CI / '*|'Test / '*|'Security / '*|'CodeQL / '*) ;;
52+
*) preserved+=("$context") ;;
53+
esac
54+
done <<< "$existing"
55+
56+
payload="$(printf '%s\n' "${preserved[@]-}" "${contexts[@]}" | jq -Rsc 'split("\n") | map(select(length > 0)) | unique | {strict: true, contexts: .}')"
57+
58+
if [ "$mode" = check ]; then
59+
printf '%s\n' "$payload" | jq .
60+
exit 0
61+
fi
62+
63+
current="$protection"
64+
full_payload="$(jq --argjson checks "$payload" '
65+
{
66+
required_status_checks: $checks,
67+
enforce_admins: .enforce_admins.enabled,
68+
required_pull_request_reviews: (
69+
if .required_pull_request_reviews == null then null else {
70+
dismiss_stale_reviews: .required_pull_request_reviews.dismiss_stale_reviews,
71+
require_code_owner_reviews: .required_pull_request_reviews.require_code_owner_reviews,
72+
required_approving_review_count: .required_pull_request_reviews.required_approving_review_count,
73+
require_last_push_approval: .required_pull_request_reviews.require_last_push_approval
74+
} end
75+
),
76+
restrictions: (
77+
if .restrictions == null then null else {
78+
users: [.restrictions.users[].login],
79+
teams: [.restrictions.teams[].slug],
80+
apps: [.restrictions.apps[].slug]
81+
} end
82+
),
83+
required_linear_history: .required_linear_history.enabled,
84+
allow_force_pushes: .allow_force_pushes.enabled,
85+
allow_deletions: .allow_deletions.enabled,
86+
block_creations: .block_creations.enabled,
87+
required_conversation_resolution: .required_conversation_resolution.enabled,
88+
lock_branch: .lock_branch.enabled,
89+
allow_fork_syncing: .allow_fork_syncing.enabled
90+
}
91+
' <<< "$current")"
92+
93+
gh api --method PUT "repos/$repo/branches/$branch/protection" \
94+
--input - <<< "$full_payload" >/dev/null
95+
printf 'Updated required checks for %s:%s\n' "$repo" "$branch"

‎.github/scripts/sync-template.sh‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,10 @@ files=(
6767
.github/scripts/codeql-languages.sh
6868
.github/scripts/doctor.sh
6969
.github/scripts/security.sh
70+
.github/scripts/sitecustomize.py
7071
.github/scripts/sync-template.sh
7172
.github/scripts/init-repo.sh
73+
.github/scripts/sync-protection.sh
7274
.github/workflows/ci.yml
7375
.github/workflows/codeql.yml
7476
.github/workflows/draft-pr.yml
@@ -78,6 +80,27 @@ files=(
7880
.github/workflows/test.yml
7981
)
8082

83+
# Workflows outside the standard baseline are repository-owned extensions.
84+
# The sync operation never deletes or replaces them; surface them explicitly
85+
# so maintainers can verify custom deployment, indexing, or security flows.
86+
standard_workflow() {
87+
case "$1" in
88+
ci.yml|codeql.yml|draft-pr.yml|release-pr.yml|release.yml|security.yml|test.yml) return 0 ;;
89+
*) return 1 ;;
90+
esac
91+
}
92+
93+
custom_workflows=()
94+
if [ -d .github/workflows ]; then
95+
while IFS= read -r workflow; do
96+
workflow="${workflow#./.github/workflows/}"
97+
standard_workflow "$workflow" || custom_workflows+=("$workflow")
98+
done < <(find .github/workflows -maxdepth 1 -type f -print | sort)
99+
fi
100+
if [ "${#custom_workflows[@]}" -gt 0 ]; then
101+
printf 'Preserving custom workflows: %s\n' "${custom_workflows[*]}"
102+
fi
103+
81104
changed=0
82105
for file in "${files[@]}"; do
83106
if [ ! -f "$template_root/$file" ]; then

‎AGENTS.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ They complement `CONTRIBUTING.md`. More specific instructions in nested `AGENTS.
1010
- Prefer the versions pinned in `.mise.toml`.
1111
- Do not commit secrets, generated credentials, local environment files, or machine-specific paths.
1212
- Add tests for behavior changes and keep coverage thresholds explicit in the project configuration.
13-
Make the smallest complete, well-tested change that solves the requested problem without disturbing unrelated work.
13+
- Make the smallest complete, well-tested change that solves the requested problem without disturbing unrelated work.
1414

1515
This repository may contain TypeScript, Rust, Python, or any combination of them. Detect the active stack from the files present; do not assume every check applies.
1616

0 commit comments

Comments
 (0)