code-foundry 0.1.3 → 0.2.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 (37) hide show
  1. package/.githooks/pre-commit +20 -5
  2. package/.github/CODEOWNERS +4 -2
  3. package/.github/CONTRIBUTING.md +5 -1
  4. package/.github/actions/cache/action.yml +43 -0
  5. package/.github/actions/codeql/action.yml +29 -0
  6. package/.github/actions/setup/action.yml +756 -0
  7. package/.github/scripts/bootstrap.sh +7 -0
  8. package/.github/scripts/changed-files.sh +96 -0
  9. package/.github/scripts/ci.sh +421 -28
  10. package/.github/scripts/codeql-languages.sh +71 -5
  11. package/.github/scripts/doctor.sh +1 -1
  12. package/.github/scripts/format-fast-path.sh +80 -0
  13. package/.github/scripts/init-repo.sh +39 -11
  14. package/.github/scripts/profile.sh +179 -0
  15. package/.github/scripts/security.sh +180 -20
  16. package/.github/scripts/sync-codeowners.sh +76 -0
  17. package/.github/scripts/sync-protection.sh +15 -2
  18. package/.github/scripts/sync-template.sh +222 -15
  19. package/.github/scripts/turbo-cache-probe.sh +102 -0
  20. package/.github/template.yml +14 -0
  21. package/.github/template.yml.example +10 -0
  22. package/.github/workflows/ci.yml +108 -7
  23. package/.github/workflows/codeql.yml +126 -17
  24. package/.github/workflows/draft-pr.yml +4 -7
  25. package/.github/workflows/release-pr.yml +20 -7
  26. package/.github/workflows/release.yml +99 -7
  27. package/.github/workflows/security.yml +201 -6
  28. package/.github/workflows/test.yml +100 -3
  29. package/.gitignore +4 -0
  30. package/.mise.toml +0 -2
  31. package/AGENTS.md +1 -0
  32. package/CHANGELOG.md +12 -0
  33. package/README.md +92 -10
  34. package/package.json +11 -1
  35. package/release-please-config.json +16 -0
  36. package/src/cli.mjs +20 -8
  37. package/.github/workflows/publish.yml +0 -25
@@ -91,7 +91,7 @@ for workflow in ci codeql security test draft-pr release-pr release; do
91
91
  fi
92
92
  done
93
93
 
94
- for script in ci.sh codeql-languages.sh security.sh doctor.sh bootstrap.sh sync-template.sh init-repo.sh sync-protection.sh; do
94
+ for script in ci.sh codeql-languages.sh profile.sh security.sh doctor.sh bootstrap.sh sync-template.sh init-repo.sh sync-protection.sh sync-codeowners.sh; do
95
95
  [ -x ".github/scripts/$script" ] || error "missing executable script: .github/scripts/$script"
96
96
  done
97
97
 
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Bootstrap only the formatter for Bun projects whose repository-owned format
5
+ # script directly invokes Prettier. The workflow keeps a full-install fallback
6
+ # because formatter configs may import arbitrary repository dependencies.
7
+
8
+ set_output() {
9
+ printf '%s=%s\n' "$1" "$2" >> "${GITHUB_OUTPUT:-/dev/null}"
10
+ }
11
+
12
+ set_output hit false
13
+
14
+ [ -f package.json ] || exit 0
15
+ [ -f bun.lock ] || [ -f bun.lockb ] || exit 0
16
+
17
+ project_profile="$(node <<'NODE'
18
+ const fs = require('node:fs');
19
+ const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8'));
20
+ process.stdout.write(Array.isArray(packageJson.workspaces) ||
21
+ (packageJson.workspaces && typeof packageJson.workspaces === 'object') ? 'workspace' : 'standalone');
22
+ NODE
23
+ )"
24
+ lock_file='bun.lock'
25
+ [ -f "$lock_file" ] || lock_file='bun.lockb'
26
+ lock_bytes="$(wc -c < "$lock_file")"
27
+ # A temporary npm formatter install has fixed overhead. For small standalone
28
+ # projects, the normal Bun install is usually faster; workspace graphs and
29
+ # larger lockfiles still benefit materially from avoiding their full tree.
30
+ if [ "$project_profile" = standalone ] && [ "$lock_bytes" -le 524288 ]; then
31
+ exit 0
32
+ fi
33
+
34
+ format_script="$(node <<'NODE'
35
+ const fs = require('node:fs');
36
+ const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8'));
37
+ console.log(packageJson.scripts?.['format:check'] || packageJson.scripts?.format || '');
38
+ NODE
39
+ )"
40
+ if ! grep -Eiq '(^|[[:space:];&|])prettier([[:space:]]|$)' <<<"$format_script" ||
41
+ grep -Eiq '(^|[[:space:];&|])turbo([[:space:]]|$)' <<<"$format_script"; then
42
+ exit 0
43
+ fi
44
+
45
+ prettier_version="$(node <<'NODE'
46
+ const fs = require('node:fs');
47
+ const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8'));
48
+ for (const group of [packageJson.devDependencies, packageJson.dependencies, packageJson.optionalDependencies]) {
49
+ const value = group?.prettier;
50
+ const match = typeof value === 'string' && value.match(/(\d+\.\d+\.\d+)/);
51
+ if (match) {
52
+ console.log(match[1]);
53
+ break;
54
+ }
55
+ }
56
+ NODE
57
+ )"
58
+ [ -n "$prettier_version" ] || exit 0
59
+
60
+ tool_home="${RUNNER_TEMP:-/tmp}/repo-foundry-format"
61
+ install_log="$(mktemp)"
62
+ cleanup() { rm -f "$install_log"; }
63
+ trap cleanup EXIT
64
+
65
+ if ! npm install --prefix "$tool_home" --no-save --ignore-scripts --no-audit --no-fund \
66
+ "prettier@${prettier_version}" >"$install_log" 2>&1; then
67
+ echo 'Format Probe: formatter bootstrap unavailable; using full install'
68
+ exit 0
69
+ fi
70
+
71
+ tool_bin="$tool_home/node_modules/.bin"
72
+ if [ ! -x "$tool_bin/prettier" ]; then
73
+ echo 'Format Probe: formatter binary unavailable; using full install'
74
+ exit 0
75
+ fi
76
+
77
+ echo "$tool_bin" >> "${GITHUB_PATH:-/dev/null}"
78
+ printf 'REPO_FOUNDRY_FORMAT_FAST=true\n' >> "${GITHUB_ENV:-/dev/null}"
79
+ echo "Format Probe: using Prettier ${prettier_version} without project install"
80
+ set_output hit true
@@ -1,15 +1,18 @@
1
1
  #!/usr/bin/env bash
2
2
  set -euo pipefail
3
3
 
4
- source="https://github.com/0xPlayerOne/repo-foundry.git"
4
+ source="${REPO_FOUNDRY_SOURCE:-https://github.com/${GITHUB_REPOSITORY_OWNER:-OWNER}/code-foundry.git}"
5
5
  ref="main"
6
+ profile="${REPO_FOUNDRY_PROFILE:-auto}"
6
7
  protection=false
7
8
  dry_run=false
8
9
  prune=false
9
- languages="auto"
10
- features="all"
11
- package_manager="auto"
10
+ languages="${REPO_FOUNDRY_LANGUAGES:-auto}"
11
+ features="${REPO_FOUNDRY_FEATURES:-all}"
12
+ package_manager="${REPO_FOUNDRY_PACKAGE_MANAGER:-auto}"
12
13
  bootstrap=true
14
+ release_type="${REPO_FOUNDRY_RELEASE_TYPE:-auto}"
15
+ npm_publish="${REPO_FOUNDRY_NPM_PUBLISH:-false}"
13
16
  tool_dir=""
14
17
 
15
18
  cleanup() {
@@ -24,12 +27,15 @@ Usage: init-repo.sh [options]
24
27
  Initialize or synchronize a repository from the shared baseline.
25
28
 
26
29
  Options:
27
- --source PATH_OR_URL Template source (default: 0xPlayerOne/template-repo)
30
+ --source PATH_OR_URL Template source (default: REPO_FOUNDRY_SOURCE or GitHub owner)
28
31
  --ref REF Template branch or tag (default: main)
32
+ --profile NAME auto, application, monorepo, or minimal
29
33
  --languages LIST auto or comma-separated: typescript,rust,python,solidity
30
34
  --features LIST all or comma-separated optional features:
31
35
  ci,codeql,security,test,draft-pr,release-pr,release,dependabot
32
36
  --package-manager NAME auto, bun, pnpm, yarn, or npm
37
+ --release-type NAME auto, node, python, rust, or simple
38
+ --npm-publish Enable npm publication in the release workflow
33
39
  --dry-run Preview changes without writing files
34
40
  --prune Remove disabled standard workflows (never custom workflows)
35
41
  --protection Synchronize main branch required checks
@@ -47,9 +53,12 @@ while [ "$#" -gt 0 ]; do
47
53
  case "$1" in
48
54
  --source) source="${2:?missing source path or URL}"; shift 2 ;;
49
55
  --ref) ref="${2:?missing ref}"; shift 2 ;;
56
+ --profile) profile="${2:?missing profile}"; shift 2 ;;
50
57
  --languages) languages="${2:?missing language list}"; shift 2 ;;
51
58
  --features) features="${2:?missing feature list}"; shift 2 ;;
52
59
  --package-manager) package_manager="${2:?missing package manager}"; shift 2 ;;
60
+ --release-type) release_type="${2:?missing release type}"; shift 2 ;;
61
+ --npm-publish) npm_publish=true; shift ;;
53
62
  --dry-run) dry_run=true; shift ;;
54
63
  --prune) prune=true; shift ;;
55
64
  --protection) protection=true; shift ;;
@@ -66,6 +75,10 @@ case "$package_manager" in
66
75
  auto|bun|pnpm|yarn|npm) ;;
67
76
  *) printf 'Unsupported package manager: %s\n' "$package_manager" >&2; exit 2 ;;
68
77
  esac
78
+ case "$release_type" in
79
+ auto|node|python|rust|simple|none) ;;
80
+ *) printf 'Unsupported release type: %s\n' "$release_type" >&2; exit 2 ;;
81
+ esac
69
82
 
70
83
  script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" 2>/dev/null && pwd || true)"
71
84
  if [ -f "$script_dir/sync-template.sh" ]; then
@@ -82,8 +95,10 @@ fi
82
95
  sync_args=(
83
96
  --source "$source"
84
97
  --ref "$ref"
98
+ --profile "$profile"
85
99
  --languages "$languages"
86
100
  --features "$features"
101
+ --package-manager "$package_manager"
87
102
  )
88
103
  if [ "$dry_run" = true ]; then sync_args+=(--check); else sync_args+=(--apply); fi
89
104
  if [ "$prune" = true ]; then sync_args+=(--prune); fi
@@ -95,12 +110,25 @@ if [ "$dry_run" = true ]; then
95
110
  fi
96
111
 
97
112
  mkdir -p .github
98
- cat > .github/template.yml <<EOF
99
- version: 1
100
- languages: $languages
101
- features: $features
102
- package_manager: $package_manager
103
- EOF
113
+ template_ref="$(awk -F': ' '/^template:/ {print $2; exit}' .github/template.yml 2>/dev/null || true)"
114
+ profile="$(awk -F': ' '/^profile:/ {print $2; exit}' .github/template.yml 2>/dev/null || true)"
115
+ languages="$(awk -F': ' '/^languages:/ {print $2; exit}' .github/template.yml 2>/dev/null || true)"
116
+ features="$(awk -F': ' '/^features:/ {print $2; exit}' .github/template.yml 2>/dev/null || true)"
117
+ package_manager="$(awk -F': ' '/^package_manager:/ {print $2; exit}' .github/template.yml 2>/dev/null || true)"
118
+ release_type="$(awk -F': ' '/^release_type:/ {print $2; exit}' .github/template.yml 2>/dev/null || true)"
119
+ npm_publish="$(awk -F': ' '/^npm_publish:/ {print $2; exit}' .github/template.yml 2>/dev/null || true)"
120
+ {
121
+ printf 'version: 1\n'
122
+ if [ -n "$template_ref" ]; then
123
+ printf 'template: %s\n' "$template_ref"
124
+ fi
125
+ printf 'profile: %s\n' "$profile"
126
+ printf 'languages: %s\n' "$languages"
127
+ printf 'features: %s\n' "$features"
128
+ printf 'package_manager: %s\n' "$package_manager"
129
+ printf 'release_type: %s\n' "$release_type"
130
+ printf 'npm_publish: %s\n' "$npm_publish"
131
+ } > .github/template.yml
104
132
 
105
133
  if [ "$bootstrap" = false ]; then
106
134
  printf '%s\n' 'Bootstrap skipped.'
@@ -0,0 +1,179 @@
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/template.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/template.yml
35
+
36
+ config_value() {
37
+ local key="$1"
38
+ [ -f "$template_file" ] || return 0
39
+ awk -F': *' -v key="$key" '$1 == key { print substr($0, index($0, ":") + 1); 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 'cache_packages=%s\n' "$(raw_value cache_packages auto)"
163
+ printf 'cache_build=%s\n' "$(raw_value cache_build auto)"
164
+ printf 'coverage_minimum=%s\n' "$(raw_value coverage_minimum 80)"
165
+ printf 'turbo_remote=%s\n' "$(raw_value turbo_remote auto)"
166
+ }
167
+
168
+ case "$command" in
169
+ detect) detect ;;
170
+ get)
171
+ detect | awk -F= -v key="$requested_key" '$1 == key { print substr($0, index($0, "=") + 1); found=1 } END { exit !found }'
172
+ ;;
173
+ env)
174
+ detect | while IFS='=' read -r key value; do
175
+ env_key="REPO_FOUNDRY_$(printf '%s' "$key" | tr '[:lower:]-' '[:upper:]_')"
176
+ printf '%s=%q\n' "$env_key" "$value"
177
+ done
178
+ ;;
179
+ esac
@@ -1,9 +1,116 @@
1
1
  #!/usr/bin/env bash
2
2
  set -euo pipefail
3
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
+
4
81
  audits=0
5
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
+
6
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
+ elif [ -f .github/template.yml ]; then
109
+ configured="$(awk -F': ' '/^package_manager:/ {print $2; exit}' .github/template.yml)"
110
+ fi
111
+ case "$configured" in
112
+ bun|pnpm|yarn|npm) echo "$configured"; return ;;
113
+ esac
7
114
  if [ -f bun.lock ] || [ -f bun.lockb ]; then echo bun
8
115
  elif [ -f pnpm-lock.yaml ]; then echo pnpm
9
116
  elif [ -f yarn.lock ]; then echo yarn
@@ -12,7 +119,11 @@ package_manager() {
12
119
  fi
13
120
  }
14
121
 
15
- if [ -f package.json ]; then
122
+ audit_javascript() {
123
+ if ! has_javascript_dependencies; then
124
+ echo "Skipping JavaScript/TypeScript audit (dependency inputs not found)"
125
+ return
126
+ fi
16
127
  audit_args=(--audit-level=high)
17
128
  if [ -f .github/security-audit-allowlist.txt ]; then
18
129
  while IFS= read -r advisory; do
@@ -26,26 +137,75 @@ if [ -f package.json ]; then
26
137
  yarn) audits=$((audits + 1)); corepack yarn npm audit --all --recursive ;;
27
138
  npm) audits=$((audits + 1)); npm audit --audit-level=high ;;
28
139
  esac
29
- else
30
- echo "Skipping JavaScript/TypeScript audit (package.json not found)"
31
- fi
140
+ }
32
141
 
33
- if [ -f Cargo.toml ]; then
34
- audits=$((audits + 1))
35
- if ! command -v cargo-audit >/dev/null 2>&1; then cargo install cargo-audit --locked --quiet; fi
36
- cargo audit
37
- else
38
- echo "Skipping Rust audit (Cargo.toml not found)"
39
- fi
142
+ audit_rust() {
143
+ if [ -f Cargo.toml ]; then
144
+ audits=$((audits + 1))
145
+ if ! command -v cargo-audit >/dev/null 2>&1; then cargo install cargo-audit --locked --quiet; fi
146
+ cargo audit
147
+ else
148
+ echo "Skipping Rust audit (Cargo.toml not found)"
149
+ fi
150
+ }
151
+
152
+ audit_python() {
153
+ local pip_audit_version='2.10.1'
154
+ requirement_files=()
155
+ if [ -n "${REPO_FOUNDRY_PYTHON_REQUIREMENT:-}" ] && [ "$REPO_FOUNDRY_PYTHON_REQUIREMENT" != project ] && [ "$REPO_FOUNDRY_PYTHON_REQUIREMENT" != none ]; then
156
+ if ! git ls-files --error-unmatch -- "${REPO_FOUNDRY_PYTHON_REQUIREMENT}" >/dev/null 2>&1; then
157
+ echo "Python requirement file is not tracked: ${REPO_FOUNDRY_PYTHON_REQUIREMENT}" >&2
158
+ return 1
159
+ fi
160
+ requirement_files+=( "$REPO_FOUNDRY_PYTHON_REQUIREMENT" )
161
+ else
162
+ while IFS= read -r requirement_file; do
163
+ [ -n "$requirement_file" ] && requirement_files+=( "$requirement_file" )
164
+ done < <(git ls-files '*requirements*.txt')
165
+ fi
166
+ if [ "${#requirement_files[@]}" -gt 0 ] || [ -f pyproject.toml ]; then
167
+ audits=$((audits + 1))
168
+ if command -v uv >/dev/null 2>&1; then
169
+ local uv_audit_cache="${REPO_FOUNDRY_UV_AUDIT_CACHE:-$HOME/.cache/uv-audit}"
170
+ pip_audit() {
171
+ if [ "${REPO_FOUNDRY_UV_AUDIT_OFFLINE:-false}" = true ]; then
172
+ if UV_CACHE_DIR="$uv_audit_cache" uv tool run --offline --from "pip-audit==${pip_audit_version}" pip-audit "$@"; then
173
+ return 0
174
+ fi
175
+ echo 'Offline pip-audit cache was incomplete; retrying online.' >&2
176
+ fi
177
+ REPO_FOUNDRY_UV_AUDIT_OFFLINE=false UV_CACHE_DIR="$uv_audit_cache" uv tool run --from "pip-audit==${pip_audit_version}" pip-audit "$@"
178
+ }
179
+ else
180
+ python -m pip install --disable-pip-version-check --quiet "pip-audit==${pip_audit_version}"
181
+ pip_audit() { python -m pip_audit "$@"; }
182
+ fi
183
+ if [ "${#requirement_files[@]}" -gt 0 ]; then
184
+ pids=()
185
+ for requirement_file in "${requirement_files[@]}"; do
186
+ pip_audit -r "$requirement_file" &
187
+ pids+=( "$!" )
188
+ done
189
+ wait_for_parallel "${pids[@]}"
190
+ else
191
+ pip_audit
192
+ fi
193
+ else
194
+ echo "Skipping Python audit (Python dependency manifest not found)"
195
+ fi
196
+ }
197
+
198
+ case "$mode" in
199
+ audit|all)
200
+ run_parallel audit_javascript audit_rust audit_python
201
+ ;;
202
+ javascript) audit_javascript ;;
203
+ rust) audit_rust ;;
204
+ python) audit_python ;;
205
+ esac
40
206
 
41
- if [ -f requirements.txt ] || [ -f requirements-dev.txt ] || [ -f pyproject.toml ]; then
42
- audits=$((audits + 1))
43
- python -m pip install --disable-pip-version-check --quiet pip-audit
44
- if [ -f requirements.txt ]; then python -m pip_audit -r requirements.txt; fi
45
- if [ -f requirements-dev.txt ]; then python -m pip_audit -r requirements-dev.txt; fi
46
- if [ -f pyproject.toml ] && [ ! -f requirements.txt ] && [ ! -f requirements-dev.txt ]; then python -m pip_audit; fi
47
- else
48
- echo "Skipping Python audit (Python dependency manifest not found)"
207
+ if [ "$audits" -eq 0 ] && ! has_dependency_manifest; then
208
+ echo "No supported dependency manifests found; nothing to audit"
49
209
  fi
50
210
 
51
- if [ "$audits" -eq 0 ]; then echo "No supported dependency manifests found; nothing to audit"; fi
211
+ exit 0
@@ -0,0 +1,76 @@
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"