code-foundry 0.26.0 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/.githooks/pre-commit +3 -4
  2. package/.github/CONTRIBUTING.md +10 -13
  3. package/.github/actions/setup/action.yml +50 -3
  4. package/.github/code-foundry.yml +6 -1
  5. package/.github/workflows/ci.yml +20 -23
  6. package/.github/workflows/ci_self-ci.yml +1 -1
  7. package/.github/workflows/codeql.yml +23 -29
  8. package/.github/workflows/codeql_self-ci.yml +1 -1
  9. package/.github/workflows/draft-pr.yml +2 -2
  10. package/.github/workflows/release-pr.yml +4 -4
  11. package/.github/workflows/release.yml +17 -29
  12. package/.github/workflows/security.yml +24 -19
  13. package/.github/workflows/security_self-ci.yml +1 -1
  14. package/.github/workflows/test.yml +20 -20
  15. package/.github/workflows/test_self-ci.yml +1 -1
  16. package/AGENTS.md +13 -10
  17. package/CHANGELOG.md +21 -0
  18. package/README.md +16 -6
  19. package/docs/CONFIGURATION.md +10 -1
  20. package/docs/INITIALIZATION.md +6 -5
  21. package/docs/RELEASES.md +5 -0
  22. package/docs/WORKFLOWS.md +12 -5
  23. package/package.json +7 -3
  24. package/src/cli.mjs +29 -25
  25. package/src/commands/doctor.mjs +70 -0
  26. package/src/commands/sync.mjs +224 -0
  27. package/src/lib/config.mjs +38 -0
  28. package/src/lib/profile.mjs +114 -0
  29. package/src/runtime.mjs +316 -0
  30. package/.github/scripts/bootstrap.sh +0 -21
  31. package/.github/scripts/changed-files.sh +0 -96
  32. package/.github/scripts/ci.sh +0 -641
  33. package/.github/scripts/codeql-languages.sh +0 -91
  34. package/.github/scripts/doctor.sh +0 -98
  35. package/.github/scripts/format-fast-path.sh +0 -80
  36. package/.github/scripts/init-repo.sh +0 -268
  37. package/.github/scripts/pre-commit.sh +0 -67
  38. package/.github/scripts/profile.sh +0 -186
  39. package/.github/scripts/security.sh +0 -214
  40. package/.github/scripts/sitecustomize.py +0 -17
  41. package/.github/scripts/sync-codeowners.sh +0 -76
  42. package/.github/scripts/sync-protection.sh +0 -138
  43. package/.github/scripts/sync-template.sh +0 -748
  44. package/.github/scripts/turbo-cache-probe.sh +0 -102
@@ -1,186 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
-
4
- # Resolve repository settings with this precedence:
5
- # explicit REPO_FOUNDRY_* values (CLI callers can export them), then
6
- # .github/code-foundry.yml, then detected defaults.
7
-
8
- root="${REPO_FOUNDRY_ROOT:-$PWD}"
9
- command="detect"
10
- requested_key=""
11
-
12
- usage() {
13
- cat <<'EOF'
14
- Usage: profile.sh [detect|env|get KEY] [--root PATH]
15
-
16
- Commands:
17
- detect Print the resolved profile as key=value lines (default)
18
- env Print REPO_FOUNDRY_* assignments
19
- get KEY Print one resolved profile value
20
- EOF
21
- }
22
-
23
- while [ "$#" -gt 0 ]; do
24
- case "$1" in
25
- detect|env) command="$1"; shift ;;
26
- get) command=get; requested_key="${2:?missing profile key}"; shift 2 ;;
27
- --root) root="${2:?missing root path}"; shift 2 ;;
28
- -h|--help) usage; exit 0 ;;
29
- *) printf 'Unknown option: %s\n' "$1" >&2; exit 2 ;;
30
- esac
31
- done
32
-
33
- cd "$root"
34
- template_file=.github/code-foundry.yml
35
-
36
- config_value() {
37
- local key="$1"
38
- [ -f "$template_file" ] || return 0
39
- awk -F': *' -v key="$key" '$1 == key { value=substr($0, index($0, ":") + 1); sub(/[[:space:]]+#.*/, "", value); print value; exit }' "$template_file" |
40
- sed -e 's/^ *//' -e 's/ *$//' -e 's/^['"'"'"]//' -e 's/['"'"'"]$//'
41
- }
42
-
43
- env_value() {
44
- local key="$1" env_key
45
- env_key="REPO_FOUNDRY_$(printf '%s' "$key" | tr '[:lower:]-' '[:upper:]_')"
46
- if [ -n "${!env_key+x}" ] && [ -n "${!env_key}" ]; then
47
- printf '%s\n' "${!env_key}"
48
- fi
49
- }
50
-
51
- raw_value() {
52
- local key="$1" default="${2:-}" value
53
- value="$(env_value "$key" || true)"
54
- [ -n "$value" ] || value="$(config_value "$key" || true)"
55
- [ -n "$value" ] && printf '%s\n' "$value" || printf '%s\n' "$default"
56
- }
57
-
58
- has_source() {
59
- find . -path './.git' -prune -o -type f -print |
60
- awk -v pattern="(^|/)[^/]+\\.($1)$" '$0 ~ pattern { found=1; exit } END { exit !found }'
61
- }
62
-
63
- detect_languages() {
64
- local detected=()
65
- if [ -f package.json ] || has_source 'js|jsx|mjs|cjs|ts|tsx|mts|cts' ||
66
- [ -f bun.lock ] || [ -f bun.lockb ] || [ -f pnpm-lock.yaml ] ||
67
- [ -f yarn.lock ] || [ -f package-lock.json ]; then
68
- detected+=(typescript)
69
- fi
70
- if [ -f Cargo.toml ] || has_source 'rs'; then detected+=(rust); fi
71
- if [ -f pyproject.toml ] || [ -f requirements.txt ] || [ -f requirements-dev.txt ] || has_source 'py'; then
72
- detected+=(python)
73
- fi
74
- if find . -path './.git' -prune -o -type f -name '*.sol' -print -quit | grep -q .; then
75
- detected+=(solidity)
76
- fi
77
- (IFS=,; printf '%s\n' "${detected[*]:-none}")
78
- }
79
-
80
- detect_package_manager() {
81
- if [ -f bun.lock ] || [ -f bun.lockb ]; then echo bun
82
- elif [ -f pnpm-lock.yaml ]; then echo pnpm
83
- elif [ -f yarn.lock ]; then echo yarn
84
- elif [ -f package-lock.json ]; then echo npm
85
- elif [ -f package.json ]; then echo bun
86
- else echo none
87
- fi
88
- }
89
-
90
- detect_profile() {
91
- if [ -f turbo.json ] || [ -f pnpm-workspace.yaml ] ||
92
- ( [ -f package.json ] && node -e 'const p=require("./package.json"); process.exit(Array.isArray(p.workspaces) || (p.workspaces && typeof p.workspaces === "object") ? 0 : 1)' 2>/dev/null ); then
93
- echo monorepo
94
- elif [ -f package.json ] || [ -f pyproject.toml ] || [ -f Cargo.toml ]; then
95
- echo application
96
- else
97
- echo minimal
98
- fi
99
- }
100
-
101
- resolve_languages() {
102
- local value
103
- value="$(raw_value languages auto)"
104
- [ "$value" = auto ] && value="$(detect_languages)"
105
- printf '%s\n' "$value"
106
- }
107
-
108
- resolve_profile() {
109
- local value
110
- value="$(raw_value profile auto)"
111
- [ "$value" = auto ] && value="$(detect_profile)"
112
- printf '%s\n' "$value"
113
- }
114
-
115
- resolve_package_manager() {
116
- local value
117
- value="$(raw_value package_manager auto)"
118
- [ "$value" = auto ] && value="$(detect_package_manager)"
119
- printf '%s\n' "$value"
120
- }
121
-
122
- resolve_release_type() {
123
- local value
124
- value="$(raw_value release_type auto)"
125
- if [ "$value" = auto ]; then
126
- if [ -f package.json ]; then value=node
127
- elif [ -f pyproject.toml ]; then value=python
128
- elif [ -f Cargo.toml ]; then value=rust
129
- elif [ -f version.txt ]; then value=simple
130
- else value=none
131
- fi
132
- fi
133
- printf '%s\n' "$value"
134
- }
135
-
136
- resolve_npm_publish() {
137
- local value release_type
138
- value="$(raw_value npm_publish false)"
139
- release_type="$(resolve_release_type)"
140
- [ "$release_type" = node ] || value=false
141
- printf '%s\n' "$value"
142
- }
143
-
144
- detect() {
145
- local profile languages package_manager release_type npm_publish features runner unit_runner
146
- profile="$(resolve_profile)"
147
- languages="$(resolve_languages)"
148
- package_manager="$(resolve_package_manager)"
149
- release_type="$(resolve_release_type)"
150
- npm_publish="$(resolve_npm_publish)"
151
- features="$(raw_value features all)"
152
- runner="$(raw_value runner ubuntu-latest)"
153
- unit_runner="$(raw_value unit_runner ubuntu-slim)"
154
- printf 'profile=%s\n' "$profile"
155
- printf 'languages=%s\n' "$languages"
156
- printf 'features=%s\n' "$features"
157
- printf 'package_manager=%s\n' "$package_manager"
158
- printf 'release_type=%s\n' "$release_type"
159
- printf 'npm_publish=%s\n' "$npm_publish"
160
- printf 'runner=%s\n' "$runner"
161
- printf 'unit_runner=%s\n' "$unit_runner"
162
- printf 'ci_runner=%s\n' "$(raw_value ci_runner "$runner")"
163
- printf 'test_runner=%s\n' "$(raw_value test_runner "$runner")"
164
- printf 'security_runner=%s\n' "$(raw_value security_runner ubuntu-slim)"
165
- printf 'codeql_runner=%s\n' "$(raw_value codeql_runner "$runner")"
166
- printf 'pr_runner=%s\n' "$(raw_value pr_runner ubuntu-slim)"
167
- printf 'release_runner=%s\n' "$(raw_value release_runner ubuntu-slim)"
168
- printf 'cache_packages=%s\n' "$(raw_value cache_packages auto)"
169
- printf 'cache_build=%s\n' "$(raw_value cache_build auto)"
170
- printf 'coverage_minimum=%s\n' "$(raw_value coverage_minimum 80)"
171
- printf 'turbo_remote=%s\n' "$(raw_value turbo_remote auto)"
172
- printf 'prune_standard=%s\n' "$(raw_value prune_standard false)"
173
- }
174
-
175
- case "$command" in
176
- detect) detect ;;
177
- get)
178
- detect | awk -F= -v key="$requested_key" '$1 == key { print substr($0, index($0, "=") + 1); found=1 } END { exit !found }'
179
- ;;
180
- env)
181
- detect | while IFS='=' read -r key value; do
182
- env_key="REPO_FOUNDRY_$(printf '%s' "$key" | tr '[:lower:]-' '[:upper:]_')"
183
- printf '%s=%q\n' "$env_key" "$value"
184
- done
185
- ;;
186
- esac
@@ -1,214 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
-
4
- source .github/scripts/changed-files.sh
5
-
6
- has_javascript_dependencies() {
7
- [ -f bun.lock ] || [ -f bun.lockb ] || [ -f pnpm-lock.yaml ] ||
8
- [ -f yarn.lock ] || [ -f package-lock.json ] ||
9
- { [ -f package.json ] && node -e 'const p=require("./package.json"); const groups=[p.dependencies,p.devDependencies,p.optionalDependencies,p.peerDependencies]; process.exit(groups.some((g)=>g && Object.keys(g).length) ? 0 : 1)' 2>/dev/null; }
10
- }
11
-
12
- has_python_manifest() {
13
- [ -f pyproject.toml ] || [ -f uv.lock ] ||
14
- git ls-files | grep -Eq '(^|/)requirements[^/]*\.txt$'
15
- }
16
-
17
- has_dependency_manifest() {
18
- has_javascript_dependencies || [ -f Cargo.toml ] || has_python_manifest
19
- }
20
-
21
- should_run() {
22
- if repo_foundry_governance_only; then
23
- printf '%s\n' 'applicable=false'
24
- return 0
25
- fi
26
- if repo_foundry_pr_dependencies_unchanged "${1:-all}"; then
27
- return 1
28
- fi
29
- case "${1:-all}" in
30
- javascript) has_javascript_dependencies || return 1 ;;
31
- rust) [ -f Cargo.toml ] || return 1 ;;
32
- python) has_python_manifest || return 1 ;;
33
- all) has_dependency_manifest || return 1 ;;
34
- *) echo "unknown ecosystem: ${1:-}" >&2; return 2 ;;
35
- esac
36
- printf '%s\n' 'applicable=true'
37
- }
38
-
39
- if [ "${1:-audit}" = should_run ]; then
40
- if should_run "${2:-all}"; then
41
- exit 0
42
- else
43
- status=$?
44
- [ "$status" -eq 1 ] && printf '%s\n' 'applicable=false' && exit 0
45
- exit "$status"
46
- fi
47
- fi
48
-
49
- if [ "${1:-}" = profile ]; then
50
- python_requirements='["none"]'
51
- if has_python_manifest && ! repo_foundry_governance_only && ! repo_foundry_pr_dependencies_unchanged python; then
52
- python_requirements="$(
53
- git ls-files -z '*requirements*.txt' |
54
- node -e 'let data=""; process.stdin.on("data", (chunk) => { data += chunk; }).on("end", () => process.stdout.write(JSON.stringify(data.split("\0").filter(Boolean))));'
55
- )"
56
- [ "$python_requirements" = "[]" ] && python_requirements='["project"]'
57
- fi
58
- for ecosystem in javascript rust python; do
59
- if should_run "$ecosystem"; then
60
- printf '%s=true\n' "$ecosystem"
61
- else
62
- status=$?
63
- [ "$status" -eq 1 ] && printf '%s=false\n' "$ecosystem" && continue
64
- exit "$status"
65
- fi
66
- done
67
- printf 'python_requirements=%s\n' "$python_requirements"
68
- exit 0
69
- fi
70
-
71
- mode="${1:-audit}"
72
- if [ "$mode" = audit ] && [ -n "${2:-}" ]; then mode="$2"; fi
73
- case "$mode" in
74
- audit|all|javascript|rust|python) ;;
75
- *)
76
- echo "usage: $0 [audit|should_run] [javascript|rust|python|all]" >&2
77
- exit 2
78
- ;;
79
- esac
80
-
81
- audits=0
82
-
83
- wait_for_parallel() {
84
- local status=0 pid
85
- for pid in "$@"; do
86
- if ! wait "$pid"; then status=1; fi
87
- done
88
- return "$status"
89
- }
90
-
91
- run_parallel() {
92
- local status=0 pid task
93
- local -a pids=()
94
- for task in "$@"; do
95
- "$task" &
96
- pids+=("$!")
97
- done
98
- for pid in "${pids[@]}"; do
99
- if ! wait "$pid"; then status=1; fi
100
- done
101
- return "$status"
102
- }
103
-
104
- package_manager() {
105
- local configured=""
106
- if [ -x .github/scripts/profile.sh ]; then
107
- configured="$(bash .github/scripts/profile.sh get package_manager 2>/dev/null || true)"
108
- else
109
- config_file=.github/code-foundry.yml
110
- if [ -f "$config_file" ]; then
111
- configured="$(awk -F': ' '/^package_manager:/ {print $2; exit}' "$config_file")"
112
- fi
113
- fi
114
- case "$configured" in
115
- bun|pnpm|yarn|npm) echo "$configured"; return ;;
116
- esac
117
- if [ -f bun.lock ] || [ -f bun.lockb ]; then echo bun
118
- elif [ -f pnpm-lock.yaml ]; then echo pnpm
119
- elif [ -f yarn.lock ]; then echo yarn
120
- elif [ -f package-lock.json ]; then echo npm
121
- else echo none
122
- fi
123
- }
124
-
125
- audit_javascript() {
126
- if ! has_javascript_dependencies; then
127
- echo "Skipping JavaScript/TypeScript audit (dependency inputs not found)"
128
- return
129
- fi
130
- audit_args=(--audit-level=high)
131
- if [ -f .github/security-audit-allowlist.txt ]; then
132
- while IFS= read -r advisory; do
133
- [[ -z "$advisory" || "$advisory" == \#* ]] && continue
134
- audit_args+=(--ignore "$advisory")
135
- done < .github/security-audit-allowlist.txt
136
- fi
137
- case "$(package_manager)" in
138
- bun) audits=$((audits + 1)); bun audit "${audit_args[@]}" ;;
139
- pnpm) audits=$((audits + 1)); corepack pnpm audit --audit-level high ;;
140
- yarn) audits=$((audits + 1)); corepack yarn npm audit --all --recursive ;;
141
- npm) audits=$((audits + 1)); npm audit --audit-level=high ;;
142
- esac
143
- }
144
-
145
- audit_rust() {
146
- if [ -f Cargo.toml ]; then
147
- audits=$((audits + 1))
148
- if ! command -v cargo-audit >/dev/null 2>&1; then cargo install cargo-audit --locked --quiet; fi
149
- cargo audit
150
- else
151
- echo "Skipping Rust audit (Cargo.toml not found)"
152
- fi
153
- }
154
-
155
- audit_python() {
156
- local pip_audit_version='2.10.1'
157
- requirement_files=()
158
- if [ -n "${REPO_FOUNDRY_PYTHON_REQUIREMENT:-}" ] && [ "$REPO_FOUNDRY_PYTHON_REQUIREMENT" != project ] && [ "$REPO_FOUNDRY_PYTHON_REQUIREMENT" != none ]; then
159
- if ! git ls-files --error-unmatch -- "${REPO_FOUNDRY_PYTHON_REQUIREMENT}" >/dev/null 2>&1; then
160
- echo "Python requirement file is not tracked: ${REPO_FOUNDRY_PYTHON_REQUIREMENT}" >&2
161
- return 1
162
- fi
163
- requirement_files+=( "$REPO_FOUNDRY_PYTHON_REQUIREMENT" )
164
- else
165
- while IFS= read -r requirement_file; do
166
- [ -n "$requirement_file" ] && requirement_files+=( "$requirement_file" )
167
- done < <(git ls-files '*requirements*.txt')
168
- fi
169
- if [ "${#requirement_files[@]}" -gt 0 ] || [ -f pyproject.toml ]; then
170
- audits=$((audits + 1))
171
- if command -v uv >/dev/null 2>&1; then
172
- local uv_audit_cache="${REPO_FOUNDRY_UV_AUDIT_CACHE:-$HOME/.cache/uv-audit}"
173
- pip_audit() {
174
- if [ "${REPO_FOUNDRY_UV_AUDIT_OFFLINE:-false}" = true ]; then
175
- if UV_CACHE_DIR="$uv_audit_cache" uv tool run --offline --from "pip-audit==${pip_audit_version}" pip-audit "$@"; then
176
- return 0
177
- fi
178
- echo 'Offline pip-audit cache was incomplete; retrying online.' >&2
179
- fi
180
- REPO_FOUNDRY_UV_AUDIT_OFFLINE=false UV_CACHE_DIR="$uv_audit_cache" uv tool run --from "pip-audit==${pip_audit_version}" pip-audit "$@"
181
- }
182
- else
183
- python -m pip install --disable-pip-version-check --quiet "pip-audit==${pip_audit_version}"
184
- pip_audit() { python -m pip_audit "$@"; }
185
- fi
186
- if [ "${#requirement_files[@]}" -gt 0 ]; then
187
- pids=()
188
- for requirement_file in "${requirement_files[@]}"; do
189
- pip_audit -r "$requirement_file" &
190
- pids+=( "$!" )
191
- done
192
- wait_for_parallel "${pids[@]}"
193
- else
194
- pip_audit
195
- fi
196
- else
197
- echo "Skipping Python audit (Python dependency manifest not found)"
198
- fi
199
- }
200
-
201
- case "$mode" in
202
- audit|all)
203
- run_parallel audit_javascript audit_rust audit_python
204
- ;;
205
- javascript) audit_javascript ;;
206
- rust) audit_rust ;;
207
- python) audit_python ;;
208
- esac
209
-
210
- if [ "$audits" -eq 0 ] && ! has_dependency_manifest; then
211
- echo "No supported dependency manifests found; nothing to audit"
212
- fi
213
-
214
- exit 0
@@ -1,17 +0,0 @@
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
@@ -1,76 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
-
4
- mode=check
5
- while [ "$#" -gt 0 ]; do
6
- case "$1" in
7
- --check) mode=check; shift ;;
8
- --apply) mode=apply; shift ;;
9
- -h|--help)
10
- printf '%s\n' 'Usage: sync-codeowners.sh [--check|--apply]'
11
- exit 0
12
- ;;
13
- *) printf 'Unknown option: %s\n' "$1" >&2; exit 2 ;;
14
- esac
15
- done
16
-
17
- codeowners_file=.github/CODEOWNERS
18
- mkdir -p .github
19
- remote="$(git remote get-url origin 2>/dev/null || true)"
20
- repo="$(printf '%s\n' "$remote" | sed -E 's#.*github.com[:/]([^/]+/[^/.]+)(\.git)?$#\1#')"
21
- owners=()
22
-
23
- if [ -n "$repo" ] && command -v gh >/dev/null 2>&1; then
24
- owner_json="$(gh repo view "$repo" --json owner 2>/dev/null || true)"
25
- owner_login="$(jq -r '.owner.login // empty' <<< "$owner_json" 2>/dev/null || true)"
26
- owner_type="$(jq -r '.owner.type // empty' <<< "$owner_json" 2>/dev/null || true)"
27
- if [ "$owner_type" = User ] && [ -n "$owner_login" ]; then
28
- owners+=("@$owner_login")
29
- fi
30
- admins="$(gh api --paginate "repos/$repo/collaborators?per_page=100" \
31
- --jq '.[] | select(.permissions.admin == true) | .login' 2>/dev/null || true)"
32
- while IFS= read -r admin; do
33
- [ -n "$admin" ] && owners+=("@$admin")
34
- done <<< "$admins"
35
- fi
36
-
37
- if [ "${#owners[@]}" -gt 0 ]; then
38
- owner_line="$(printf '%s\n' "${owners[@]}" | awk '!seen[$0]++' | paste -sd ' ' -)"
39
- else
40
- owner_line='@OWNER'
41
- printf '%s\n' 'GitHub owner/admin discovery unavailable; retaining @OWNER placeholder.' >&2
42
- fi
43
-
44
- existing_file=""
45
- if [ -f "$codeowners_file" ]; then
46
- existing_file="$(mktemp)"
47
- cp "$codeowners_file" "$existing_file"
48
- fi
49
-
50
- if [ "$mode" = check ]; then
51
- if [ -f "$codeowners_file" ] && grep -q "^\* $owner_line$" "$codeowners_file"; then
52
- printf '%s\n' 'CODEOWNERS is current.'
53
- else
54
- printf 'Would update %s with %s\n' "$codeowners_file" "$owner_line"
55
- fi
56
- [ -n "$existing_file" ] && unlink "$existing_file"
57
- exit 0
58
- fi
59
-
60
- tmp_file="$(mktemp)"
61
- {
62
- printf '%s\n' '# Managed by code-foundry. Repository owners/admins are discovered during initialization.'
63
- printf '* %s\n' "$owner_line"
64
- if [ -n "$existing_file" ]; then
65
- awk '
66
- /^# Managed by code-foundry\./ { managed=1; next }
67
- managed && /^\* / { managed=0; next }
68
- managed { next }
69
- /^\* @(OWNER|[^[:space:]]+)/ { next }
70
- { print }
71
- ' "$existing_file"
72
- fi
73
- } > "$tmp_file"
74
- mv "$tmp_file" "$codeowners_file"
75
- [ -n "$existing_file" ] && unlink "$existing_file"
76
- printf 'Updated %s with %s\n' "$codeowners_file" "$owner_line"
@@ -1,138 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
-
4
- branch="main"
5
- repo=""
6
- mode="check"
7
- configured_features="all"
8
- configured_languages="auto"
9
-
10
- config_file=.github/code-foundry.yml
11
- if [ -f "$config_file" ]; then
12
- configured_features="$(awk -F': ' '/^features:/ {print $2; exit}' "$config_file")"
13
- configured_languages="$(awk -F': ' '/^languages:/ {print $2; exit}' "$config_file")"
14
- [ -n "$configured_features" ] || configured_features="all"
15
- [ -n "$configured_languages" ] || configured_languages="auto"
16
- fi
17
-
18
- feature_enabled() {
19
- [ "$configured_features" = all ] && return 0
20
- case " $(printf '%s' "$configured_features" | tr ',' ' ') " in
21
- *" $1 "*) return 0 ;;
22
- *) return 1 ;;
23
- esac
24
- }
25
-
26
- language_enabled() {
27
- [ "$configured_languages" = auto ] || [ "$configured_languages" = all ] && return 0
28
- case " $(printf '%s' "$configured_languages" | tr ',' ' ') " in
29
- *" $1 "*) return 0 ;;
30
- *) return 1 ;;
31
- esac
32
- }
33
-
34
- usage() {
35
- printf '%s\n' 'Usage: sync-protection.sh --repo OWNER/REPO [--branch main] [--check] [--apply]'
36
- }
37
-
38
- while [ "$#" -gt 0 ]; do
39
- case "$1" in
40
- --repo) repo="${2:?missing OWNER/REPO}"; shift 2 ;;
41
- --branch) branch="${2:?missing branch}"; shift 2 ;;
42
- --check) mode="check"; shift ;;
43
- --apply) mode="apply"; shift ;;
44
- -h|--help) usage; exit 0 ;;
45
- *) usage >&2; exit 2 ;;
46
- esac
47
- done
48
-
49
- [ -n "$repo" ] || { usage >&2; exit 2; }
50
- command -v gh >/dev/null 2>&1 || { echo "gh is required" >&2; exit 1; }
51
-
52
- is_private="$(gh repo view "$repo" --json isPrivate --jq '.isPrivate')"
53
- code_security_status="disabled"
54
- if [ "$is_private" = true ]; then
55
- code_security_status="$(gh api "repos/$repo" --jq '.security_and_analysis.code_security.status // "disabled"' 2>/dev/null || printf 'disabled')"
56
- fi
57
- contexts=()
58
- if feature_enabled ci; then contexts+=(Format Lint Type-Check Build); fi
59
- if feature_enabled test; then contexts+=(Unit Integration E2E Smoke); fi
60
- if feature_enabled security; then
61
- contexts+=(Profile 'Dependency Audit (JavaScript)' 'Dependency Audit (Rust)' 'Dependency Audit (Python)')
62
- fi
63
- if feature_enabled codeql; then contexts+=(Detect); fi
64
-
65
- if { [ "$is_private" != true ] || [ "$code_security_status" = enabled ]; } && feature_enabled codeql; then
66
- contexts+=("Analyze (Actions)")
67
- if language_enabled typescript && git ls-files -- '*.ts' '*.tsx' '*.js' '*.jsx' package.json tsconfig\*.json | grep -q .; then contexts+=("Analyze (TypeScript)"); fi
68
- if language_enabled python && git ls-files -- '*.py' pyproject.toml requirements\*.txt setup.py ':!.github/**' | grep -q .; then contexts+=("Analyze (Python)"); fi
69
- if language_enabled rust && git ls-files -- '*.rs' Cargo.toml Cargo.lock | grep -q .; then contexts+=("Analyze (Rust)"); fi
70
- fi
71
-
72
- protection="$(gh api "repos/$repo/branches/$branch/protection" 2>/dev/null || true)"
73
- if ! jq -e 'type == "object" and (.required_status_checks | type == "object")' >/dev/null 2>&1 <<< "$protection"; then
74
- if [ "$mode" = apply ]; then
75
- echo "Cannot read branch protection for $repo:$branch (repository plan or permissions may not allow it)." >&2
76
- exit 1
77
- fi
78
- existing=""
79
- else
80
- existing="$(jq -r '.required_status_checks.contexts[]?' <<< "$protection")"
81
- fi
82
- preserved=()
83
- while IFS= read -r context; do
84
- [ -n "$context" ] || continue
85
- case "$context" in
86
- 'Slither / Analyze') continue ;; # removed from the standard workflow set
87
- # Remove both the current concise job names and older workflow-prefixed
88
- # names before rebuilding the standard set. This prevents stale required
89
- # checks from remaining required after a repository changes visibility or
90
- # template version.
91
- 'Dependency Audit'|'Dependency Audit ('*|'Profile'|'Test Profile'|\
92
- 'Format'|'Lint'|'Type-Check'|'Build'|'Unit'|'Integration'|'E2E'|'Smoke'|\
93
- 'Detect'|'Analyze'|'Analyze ('*|\
94
- 'CI / '*|'Test / '*|'Security / '*|'CodeQL / '*) ;;
95
- *) preserved+=("$context") ;;
96
- esac
97
- done <<< "$existing"
98
-
99
- payload="$(printf '%s\n' "${preserved[@]-}" "${contexts[@]}" | jq -Rsc 'split("\n") | map(select(length > 0)) | unique | {strict: true, contexts: .}')"
100
-
101
- if [ "$mode" = check ]; then
102
- printf '%s\n' "$payload" | jq .
103
- exit 0
104
- fi
105
-
106
- current="$protection"
107
- full_payload="$(jq --argjson checks "$payload" '
108
- {
109
- required_status_checks: $checks,
110
- enforce_admins: .enforce_admins.enabled,
111
- required_pull_request_reviews: (
112
- if .required_pull_request_reviews == null then null else {
113
- dismiss_stale_reviews: .required_pull_request_reviews.dismiss_stale_reviews,
114
- require_code_owner_reviews: .required_pull_request_reviews.require_code_owner_reviews,
115
- required_approving_review_count: .required_pull_request_reviews.required_approving_review_count,
116
- require_last_push_approval: .required_pull_request_reviews.require_last_push_approval
117
- } end
118
- ),
119
- restrictions: (
120
- if .restrictions == null then null else {
121
- users: [.restrictions.users[].login],
122
- teams: [.restrictions.teams[].slug],
123
- apps: [.restrictions.apps[].slug]
124
- } end
125
- ),
126
- required_linear_history: .required_linear_history.enabled,
127
- allow_force_pushes: .allow_force_pushes.enabled,
128
- allow_deletions: .allow_deletions.enabled,
129
- block_creations: .block_creations.enabled,
130
- required_conversation_resolution: .required_conversation_resolution.enabled,
131
- lock_branch: .lock_branch.enabled,
132
- allow_fork_syncing: .allow_fork_syncing.enabled
133
- }
134
- ' <<< "$current")"
135
-
136
- gh api --method PUT "repos/$repo/branches/$branch/protection" \
137
- --input - <<< "$full_payload" >/dev/null
138
- printf 'Updated required checks for %s:%s\n' "$repo" "$branch"