code-foundry 0.1.4
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.
- package/.editorconfig +18 -0
- package/.env.example +3 -0
- package/.gitattributes +9 -0
- package/.githooks/pre-commit +67 -0
- package/.github/CODEOWNERS +4 -0
- package/.github/CODE_OF_CONDUCT.md +41 -0
- package/.github/CONTRIBUTING.md +198 -0
- package/.github/ISSUE_TEMPLATE/bug_report.yml +56 -0
- package/.github/ISSUE_TEMPLATE/config.yml +5 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +38 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +29 -0
- package/.github/SECURITY.md +23 -0
- package/.github/actions/cache/action.yml +43 -0
- package/.github/actions/codeql/action.yml +29 -0
- package/.github/actions/setup/action.yml +776 -0
- package/.github/dependabot.yml +42 -0
- package/.github/scripts/bootstrap.sh +20 -0
- package/.github/scripts/changed-files.sh +96 -0
- package/.github/scripts/ci.sh +636 -0
- package/.github/scripts/codeql-languages.sh +86 -0
- package/.github/scripts/doctor.sh +103 -0
- package/.github/scripts/format-fast-path.sh +80 -0
- package/.github/scripts/init-repo.sh +134 -0
- package/.github/scripts/security.sh +209 -0
- package/.github/scripts/sitecustomize.py +17 -0
- package/.github/scripts/sync-codeowners.sh +76 -0
- package/.github/scripts/sync-protection.sh +137 -0
- package/.github/scripts/sync-template.sh +432 -0
- package/.github/scripts/turbo-cache-probe.sh +102 -0
- package/.github/template.yml +7 -0
- package/.github/template.yml.example +8 -0
- package/.github/workflows/ci.yml +166 -0
- package/.github/workflows/codeql.yml +160 -0
- package/.github/workflows/draft-pr.yml +59 -0
- package/.github/workflows/release-pr.yml +66 -0
- package/.github/workflows/release.yml +103 -0
- package/.github/workflows/security.yml +228 -0
- package/.github/workflows/test.yml +175 -0
- package/.gitignore +77 -0
- package/.mise.toml +6 -0
- package/.prettierrc +6 -0
- package/AGENTS.md +155 -0
- package/CHANGELOG.md +3 -0
- package/LICENSE +616 -0
- package/NOTICE +7 -0
- package/README.md +172 -0
- package/package.json +44 -0
- package/release-please-config.json +16 -0
- package/ruff.toml +6 -0
- package/src/cli.mjs +137 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
languages=()
|
|
5
|
+
changed_files=""
|
|
6
|
+
|
|
7
|
+
# Scheduled and manually dispatched scans are always full scans. For pushes
|
|
8
|
+
# and pull requests, a shallow commit diff lets the matrix skip analyzers for
|
|
9
|
+
# languages untouched by the change while keeping every matrix check visible.
|
|
10
|
+
case "${GITHUB_EVENT_NAME:-}" in
|
|
11
|
+
schedule|workflow_dispatch) ;;
|
|
12
|
+
*)
|
|
13
|
+
base_sha="${CODEQL_BASE_SHA:-}"
|
|
14
|
+
if [ -n "$base_sha" ] && [ "$base_sha" != "0000000000000000000000000000000000000000" ]; then
|
|
15
|
+
if ! git cat-file -e "$base_sha^{commit}" 2>/dev/null; then
|
|
16
|
+
git fetch --no-tags --filter=blob:none --depth=1 origin "$base_sha" >/dev/null 2>&1 || true
|
|
17
|
+
fi
|
|
18
|
+
changed_files="$(git diff --name-only "$base_sha" "${GITHUB_SHA:-HEAD}" 2>/dev/null || true)"
|
|
19
|
+
fi
|
|
20
|
+
[ -n "$changed_files" ] || changed_files="$(git diff --name-only HEAD^ HEAD 2>/dev/null || true)"
|
|
21
|
+
;;
|
|
22
|
+
esac
|
|
23
|
+
|
|
24
|
+
if find .github/workflows -type f \( -name '*.yml' -o -name '*.yaml' \) -print -quit | grep -q .; then languages+=(actions); fi
|
|
25
|
+
configured=""
|
|
26
|
+
if [ -f .github/template.yml ]; then
|
|
27
|
+
configured="$(awk -F': ' '/^languages:/ {print $2; exit}' .github/template.yml)"
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
if [ "$configured" = auto ] || [ "$configured" = all ] || [ -z "$configured" ]; then
|
|
31
|
+
tracked_files="$(git ls-files)"
|
|
32
|
+
if printf '%s\n' "$tracked_files" | grep -Eq '(^|/)([^/]+\.(ts|tsx|js|jsx)|package\.json|tsconfig[^/]*\.json)$'; then
|
|
33
|
+
languages+=(javascript-typescript)
|
|
34
|
+
fi
|
|
35
|
+
if printf '%s\n' "$tracked_files" | awk '!/^\.github\// && /(^|\/)([^\/]+\.py|pyproject\.toml|requirements[^\/]*\.txt|setup\.py)$/ { found=1; exit } END { exit !found }'; then
|
|
36
|
+
languages+=(python)
|
|
37
|
+
fi
|
|
38
|
+
if printf '%s\n' "$tracked_files" | grep -Eq '(^|/)([^/]+\.rs|Cargo\.toml|Cargo\.lock)$'; then
|
|
39
|
+
languages+=(rust)
|
|
40
|
+
fi
|
|
41
|
+
else
|
|
42
|
+
case ",$configured," in *,typescript,*) languages+=(javascript-typescript) ;; esac
|
|
43
|
+
case ",$configured," in *,python,*) languages+=(python) ;; esac
|
|
44
|
+
case ",$configured," in *,rust,*) languages+=(rust) ;; esac
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
changed_json="$(printf '%s\n' "$changed_files" | jq -Rsc 'split("\n") | map(select(length > 0))')"
|
|
48
|
+
json=$(printf '%s\n' "${languages[@]}" | jq -Rsc --argjson changed "$changed_json" '
|
|
49
|
+
split("\n")
|
|
50
|
+
| map(select(length > 0) | {
|
|
51
|
+
language: .,
|
|
52
|
+
name: (if . == "javascript-typescript" then "TypeScript" elif . == "actions" then "Actions" else (. | ascii_upcase[0:1] + .[1:]) end),
|
|
53
|
+
"build-mode": "none",
|
|
54
|
+
changed: (
|
|
55
|
+
if ($changed | length) == 0 then true
|
|
56
|
+
elif . == "actions" then any($changed[]; test("(^|/)(\\.github/workflows/|action\\.yml$)"))
|
|
57
|
+
elif . == "javascript-typescript" then any($changed[]; test("(^|/)(.*\\.(js|jsx|mjs|cjs|ts|tsx|mts|cts)|package\\.json|tsconfig[^/]*\\.json|((bun|pnpm|yarn|package-lock)\\.lock))$"))
|
|
58
|
+
elif . == "python" then any($changed[]; test("(^|/)(.*\\.py|pyproject\\.toml|requirements[^/]*\\.txt|setup\\.py|Pipfile\\.lock|poetry\\.lock)$"))
|
|
59
|
+
elif . == "rust" then any($changed[]; test("(^|/)(.*\\.rs|Cargo\\.toml|Cargo\\.lock)$"))
|
|
60
|
+
else true
|
|
61
|
+
end
|
|
62
|
+
)
|
|
63
|
+
})
|
|
64
|
+
')
|
|
65
|
+
printf 'languages=%s\n' "$json" >> "$GITHUB_OUTPUT"
|
|
66
|
+
|
|
67
|
+
# Expose stable, shell-friendly outputs for explicit analyzer jobs. Keeping
|
|
68
|
+
# these separate from the JSON matrix lets workflow-level conditions avoid
|
|
69
|
+
# allocating a runner for languages that are absent or unchanged.
|
|
70
|
+
for codeql_language in actions javascript-typescript python rust; do
|
|
71
|
+
output_prefix="$codeql_language"
|
|
72
|
+
case "$codeql_language" in
|
|
73
|
+
javascript-typescript) output_prefix="javascript" ;;
|
|
74
|
+
esac
|
|
75
|
+
|
|
76
|
+
entry="$(jq -c --arg language "$codeql_language" '.[] | select(.language == $language)' <<< "$json")"
|
|
77
|
+
if [ -n "$entry" ]; then
|
|
78
|
+
printf '%s_available=true\n' "$output_prefix" >> "$GITHUB_OUTPUT"
|
|
79
|
+
printf '%s_changed=%s\n' "$output_prefix" "$(jq -r '.changed' <<< "$entry")" >> "$GITHUB_OUTPUT"
|
|
80
|
+
printf '%s_build_mode=%s\n' "$output_prefix" "$(jq -r '."build-mode"' <<< "$entry")" >> "$GITHUB_OUTPUT"
|
|
81
|
+
else
|
|
82
|
+
printf '%s_available=false\n' "$output_prefix" >> "$GITHUB_OUTPUT"
|
|
83
|
+
printf '%s_changed=false\n' "$output_prefix" >> "$GITHUB_OUTPUT"
|
|
84
|
+
printf '%s_build_mode=none\n' "$output_prefix" >> "$GITHUB_OUTPUT"
|
|
85
|
+
fi
|
|
86
|
+
done
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
errors=0
|
|
5
|
+
|
|
6
|
+
configured_features="all"
|
|
7
|
+
if [ -f .github/template.yml ]; then
|
|
8
|
+
configured_features="$(awk -F': ' '/^features:/ {print $2; exit}' .github/template.yml)"
|
|
9
|
+
[ -n "$configured_features" ] || configured_features="all"
|
|
10
|
+
fi
|
|
11
|
+
|
|
12
|
+
feature_enabled() {
|
|
13
|
+
[ "$configured_features" = all ] && return 0
|
|
14
|
+
case " $(printf '%s' "$configured_features" | tr ',' ' ') " in
|
|
15
|
+
*" $1 "*) return 0 ;;
|
|
16
|
+
*) return 1 ;;
|
|
17
|
+
esac
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
error() {
|
|
21
|
+
printf 'ERROR: %s\n' "$1" >&2
|
|
22
|
+
errors=$((errors + 1))
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
warn() {
|
|
26
|
+
printf 'WARN: %s\n' "$1" >&2
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if [ ! -f .mise.toml ]; then
|
|
30
|
+
error ".mise.toml is missing"
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
if [ "$(git config --get core.hooksPath || true)" != ".githooks" ]; then
|
|
34
|
+
warn "Git hooks are not enabled; run bash .github/scripts/bootstrap.sh"
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
if [ -f package.json ]; then
|
|
38
|
+
node -e 'JSON.parse(require("fs").readFileSync("package.json", "utf8"))'
|
|
39
|
+
lockfiles=0
|
|
40
|
+
for lockfile in bun.lock bun.lockb pnpm-lock.yaml yarn.lock package-lock.json; do
|
|
41
|
+
if [ -f "$lockfile" ]; then lockfiles=$((lockfiles + 1)); fi
|
|
42
|
+
done
|
|
43
|
+
if [ "$lockfiles" -eq 0 ]; then
|
|
44
|
+
if 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; then
|
|
45
|
+
error "package.json exists but no supported lockfile was found"
|
|
46
|
+
else
|
|
47
|
+
printf '%s\n' "INFO: package.json has no dependencies; a lockfile is optional"
|
|
48
|
+
fi
|
|
49
|
+
elif [ "$lockfiles" -gt 1 ]; then
|
|
50
|
+
error "multiple JavaScript lockfiles found; keep one package manager"
|
|
51
|
+
fi
|
|
52
|
+
if node -e 'const p=require("./package.json"); process.exit(p.packageManager ? 0 : 1)' 2>/dev/null; then
|
|
53
|
+
declared="$(node -p 'require("./package.json").packageManager.split("@")[0]')"
|
|
54
|
+
actual=""
|
|
55
|
+
[ -f bun.lock ] || [ -f bun.lockb ] && actual="bun"
|
|
56
|
+
[ -f pnpm-lock.yaml ] && actual="pnpm"
|
|
57
|
+
[ -f yarn.lock ] && actual="yarn"
|
|
58
|
+
[ -f package-lock.json ] && actual="npm"
|
|
59
|
+
[ -n "$actual" ] && [ "$declared" != "$actual" ] && error "packageManager ($declared) does not match $actual lockfile"
|
|
60
|
+
fi
|
|
61
|
+
if git ls-files -- '*.ts' '*.tsx' '*.js' '*.jsx' | grep -q .; then
|
|
62
|
+
if ! node -e 'const p=require("./package.json"); const s=p.scripts||{}; process.exit(s.test||s["test:unit"]||s["test:integration"] ? 0 : 1)' 2>/dev/null; then
|
|
63
|
+
warn "JavaScript/TypeScript sources found but no test or test:unit/test:integration script is defined"
|
|
64
|
+
fi
|
|
65
|
+
fi
|
|
66
|
+
if [ -f bunfig.toml ] && node -e 'const p=require("./package.json"); process.exit(p.scripts?.["test:coverage"] ? 0 : 1)' 2>/dev/null; then
|
|
67
|
+
if ! grep -q 'coverageThreshold' bunfig.toml; then
|
|
68
|
+
if [ -x .github/scripts/ci.sh ]; then
|
|
69
|
+
printf '%s\n' "INFO: shared CI enforces the Bun aggregate coverage threshold"
|
|
70
|
+
else
|
|
71
|
+
error "Bun coverage is enabled by test:coverage but no coverage policy is configured"
|
|
72
|
+
fi
|
|
73
|
+
fi
|
|
74
|
+
fi
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
if [ -f Cargo.toml ]; then
|
|
78
|
+
command -v cargo >/dev/null 2>&1 || error "Cargo is required for this repository"
|
|
79
|
+
cargo metadata --no-deps --format-version 1 >/dev/null
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
if [ -f pyproject.toml ] || [ -f requirements.txt ] || [ -f requirements-dev.txt ]; then
|
|
83
|
+
if ! command -v python >/dev/null 2>&1 && [ ! -x .venv/bin/python ]; then
|
|
84
|
+
error "Python is required for this repository"
|
|
85
|
+
fi
|
|
86
|
+
fi
|
|
87
|
+
|
|
88
|
+
for workflow in ci codeql security test draft-pr release-pr release; do
|
|
89
|
+
if feature_enabled "$workflow"; then
|
|
90
|
+
[ -f ".github/workflows/$workflow.yml" ] || error "missing enabled workflow: $workflow.yml"
|
|
91
|
+
fi
|
|
92
|
+
done
|
|
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 sync-codeowners.sh; do
|
|
95
|
+
[ -x ".github/scripts/$script" ] || error "missing executable script: .github/scripts/$script"
|
|
96
|
+
done
|
|
97
|
+
|
|
98
|
+
if [ "$errors" -gt 0 ]; then
|
|
99
|
+
printf '%s\n' "Repository doctor found $errors error(s)." >&2
|
|
100
|
+
exit 1
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
printf '%s\n' "Repository doctor passed."
|
|
@@ -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
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
source="${REPO_FOUNDRY_SOURCE:-https://github.com/${GITHUB_REPOSITORY_OWNER:-OWNER}/code-foundry.git}"
|
|
5
|
+
ref="main"
|
|
6
|
+
protection=false
|
|
7
|
+
dry_run=false
|
|
8
|
+
prune=false
|
|
9
|
+
languages="auto"
|
|
10
|
+
features="all"
|
|
11
|
+
package_manager="auto"
|
|
12
|
+
bootstrap=true
|
|
13
|
+
release_type="auto"
|
|
14
|
+
npm_publish="false"
|
|
15
|
+
tool_dir=""
|
|
16
|
+
|
|
17
|
+
cleanup() {
|
|
18
|
+
if [ -n "$tool_dir" ]; then rm -rf "$tool_dir"; fi
|
|
19
|
+
}
|
|
20
|
+
trap cleanup EXIT
|
|
21
|
+
|
|
22
|
+
usage() {
|
|
23
|
+
cat <<'EOF'
|
|
24
|
+
Usage: init-repo.sh [options]
|
|
25
|
+
|
|
26
|
+
Initialize or synchronize a repository from the shared baseline.
|
|
27
|
+
|
|
28
|
+
Options:
|
|
29
|
+
--source PATH_OR_URL Template source (default: REPO_FOUNDRY_SOURCE or GitHub owner)
|
|
30
|
+
--ref REF Template branch or tag (default: main)
|
|
31
|
+
--languages LIST auto or comma-separated: typescript,rust,python,solidity
|
|
32
|
+
--features LIST all or comma-separated optional features:
|
|
33
|
+
ci,codeql,security,test,draft-pr,release-pr,release,dependabot
|
|
34
|
+
--package-manager NAME auto, bun, pnpm, yarn, or npm
|
|
35
|
+
--release-type NAME auto, node, python, rust, or simple
|
|
36
|
+
--npm-publish Enable npm publication in the release workflow
|
|
37
|
+
--dry-run Preview changes without writing files
|
|
38
|
+
--prune Remove disabled standard workflows (never custom workflows)
|
|
39
|
+
--protection Synchronize main branch required checks
|
|
40
|
+
--no-bootstrap Skip mise/hooks/doctor bootstrap after init
|
|
41
|
+
-h, --help Show this help
|
|
42
|
+
|
|
43
|
+
Examples:
|
|
44
|
+
bash .github/scripts/init-repo.sh --languages typescript,python
|
|
45
|
+
bash .github/scripts/init-repo.sh --languages rust --features ci,codeql,security,test
|
|
46
|
+
bash .github/scripts/init-repo.sh --features all --dry-run
|
|
47
|
+
EOF
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
while [ "$#" -gt 0 ]; do
|
|
51
|
+
case "$1" in
|
|
52
|
+
--source) source="${2:?missing source path or URL}"; shift 2 ;;
|
|
53
|
+
--ref) ref="${2:?missing ref}"; shift 2 ;;
|
|
54
|
+
--languages) languages="${2:?missing language list}"; shift 2 ;;
|
|
55
|
+
--features) features="${2:?missing feature list}"; shift 2 ;;
|
|
56
|
+
--package-manager) package_manager="${2:?missing package manager}"; shift 2 ;;
|
|
57
|
+
--release-type) release_type="${2:?missing release type}"; shift 2 ;;
|
|
58
|
+
--npm-publish) npm_publish=true; shift ;;
|
|
59
|
+
--dry-run) dry_run=true; shift ;;
|
|
60
|
+
--prune) prune=true; shift ;;
|
|
61
|
+
--protection) protection=true; shift ;;
|
|
62
|
+
--no-bootstrap) bootstrap=false; shift ;;
|
|
63
|
+
-h|--help) usage; exit 0 ;;
|
|
64
|
+
*)
|
|
65
|
+
printf 'Unknown option: %s\n' "$1" >&2
|
|
66
|
+
exit 2
|
|
67
|
+
;;
|
|
68
|
+
esac
|
|
69
|
+
done
|
|
70
|
+
|
|
71
|
+
case "$package_manager" in
|
|
72
|
+
auto|bun|pnpm|yarn|npm) ;;
|
|
73
|
+
*) printf 'Unsupported package manager: %s\n' "$package_manager" >&2; exit 2 ;;
|
|
74
|
+
esac
|
|
75
|
+
case "$release_type" in
|
|
76
|
+
auto|node|python|rust|simple|none) ;;
|
|
77
|
+
*) printf 'Unsupported release type: %s\n' "$release_type" >&2; exit 2 ;;
|
|
78
|
+
esac
|
|
79
|
+
|
|
80
|
+
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" 2>/dev/null && pwd || true)"
|
|
81
|
+
if [ -f "$script_dir/sync-template.sh" ]; then
|
|
82
|
+
sync_script="$script_dir/sync-template.sh"
|
|
83
|
+
elif [ -d "$source" ] && [ -f "$source/.github/scripts/sync-template.sh" ]; then
|
|
84
|
+
sync_script="$source/.github/scripts/sync-template.sh"
|
|
85
|
+
else
|
|
86
|
+
command -v git >/dev/null 2>&1 || { echo "git is required" >&2; exit 1; }
|
|
87
|
+
tool_dir="$(mktemp -d)"
|
|
88
|
+
git clone --quiet --depth 1 --branch "$ref" "$source" "$tool_dir/template-repo"
|
|
89
|
+
sync_script="$tool_dir/template-repo/.github/scripts/sync-template.sh"
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
sync_args=(
|
|
93
|
+
--source "$source"
|
|
94
|
+
--ref "$ref"
|
|
95
|
+
--languages "$languages"
|
|
96
|
+
--features "$features"
|
|
97
|
+
--package-manager "$package_manager"
|
|
98
|
+
)
|
|
99
|
+
if [ "$dry_run" = true ]; then sync_args+=(--check); else sync_args+=(--apply); fi
|
|
100
|
+
if [ "$prune" = true ]; then sync_args+=(--prune); fi
|
|
101
|
+
bash "$sync_script" "${sync_args[@]}"
|
|
102
|
+
|
|
103
|
+
if [ "$dry_run" = true ]; then
|
|
104
|
+
printf '%s\n' 'Dry run complete; no files were changed.'
|
|
105
|
+
exit 0
|
|
106
|
+
fi
|
|
107
|
+
|
|
108
|
+
mkdir -p .github
|
|
109
|
+
template_ref="$(awk -F': ' '/^template:/ {print $2; exit}' .github/template.yml 2>/dev/null || true)"
|
|
110
|
+
{
|
|
111
|
+
printf 'version: 1\n'
|
|
112
|
+
if [ -n "$template_ref" ]; then
|
|
113
|
+
printf 'template: %s\n' "$template_ref"
|
|
114
|
+
fi
|
|
115
|
+
printf 'languages: %s\n' "$languages"
|
|
116
|
+
printf 'features: %s\n' "$features"
|
|
117
|
+
printf 'package_manager: %s\n' "$package_manager"
|
|
118
|
+
printf 'release_type: %s\n' "$release_type"
|
|
119
|
+
printf 'npm_publish: %s\n' "$npm_publish"
|
|
120
|
+
} > .github/template.yml
|
|
121
|
+
|
|
122
|
+
if [ "$bootstrap" = false ]; then
|
|
123
|
+
printf '%s\n' 'Bootstrap skipped.'
|
|
124
|
+
exit 0
|
|
125
|
+
fi
|
|
126
|
+
|
|
127
|
+
bash .github/scripts/bootstrap.sh
|
|
128
|
+
|
|
129
|
+
if [ "$protection" = true ]; then
|
|
130
|
+
remote="$(git remote get-url origin 2>/dev/null || true)"
|
|
131
|
+
repo="$(printf '%s\n' "$remote" | sed -E 's#.*github.com[:/]([^/]+/[^/.]+)(\.git)?$#\1#')"
|
|
132
|
+
[ -n "$repo" ] || { echo 'Could not determine GitHub repository from origin' >&2; exit 1; }
|
|
133
|
+
bash .github/scripts/sync-protection.sh --repo "$repo" --apply
|
|
134
|
+
fi
|
|
@@ -0,0 +1,209 @@
|
|
|
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 [ -f .github/template.yml ]; then
|
|
107
|
+
configured="$(awk -F': ' '/^package_manager:/ {print $2; exit}' .github/template.yml)"
|
|
108
|
+
case "$configured" in
|
|
109
|
+
bun|pnpm|yarn|npm) echo "$configured"; return ;;
|
|
110
|
+
esac
|
|
111
|
+
fi
|
|
112
|
+
if [ -f bun.lock ] || [ -f bun.lockb ]; then echo bun
|
|
113
|
+
elif [ -f pnpm-lock.yaml ]; then echo pnpm
|
|
114
|
+
elif [ -f yarn.lock ]; then echo yarn
|
|
115
|
+
elif [ -f package-lock.json ]; then echo npm
|
|
116
|
+
else echo none
|
|
117
|
+
fi
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
audit_javascript() {
|
|
121
|
+
if ! has_javascript_dependencies; then
|
|
122
|
+
echo "Skipping JavaScript/TypeScript audit (dependency inputs not found)"
|
|
123
|
+
return
|
|
124
|
+
fi
|
|
125
|
+
audit_args=(--audit-level=high)
|
|
126
|
+
if [ -f .github/security-audit-allowlist.txt ]; then
|
|
127
|
+
while IFS= read -r advisory; do
|
|
128
|
+
[[ -z "$advisory" || "$advisory" == \#* ]] && continue
|
|
129
|
+
audit_args+=(--ignore "$advisory")
|
|
130
|
+
done < .github/security-audit-allowlist.txt
|
|
131
|
+
fi
|
|
132
|
+
case "$(package_manager)" in
|
|
133
|
+
bun) audits=$((audits + 1)); bun audit "${audit_args[@]}" ;;
|
|
134
|
+
pnpm) audits=$((audits + 1)); corepack pnpm audit --audit-level high ;;
|
|
135
|
+
yarn) audits=$((audits + 1)); corepack yarn npm audit --all --recursive ;;
|
|
136
|
+
npm) audits=$((audits + 1)); npm audit --audit-level=high ;;
|
|
137
|
+
esac
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
audit_rust() {
|
|
141
|
+
if [ -f Cargo.toml ]; then
|
|
142
|
+
audits=$((audits + 1))
|
|
143
|
+
if ! command -v cargo-audit >/dev/null 2>&1; then cargo install cargo-audit --locked --quiet; fi
|
|
144
|
+
cargo audit
|
|
145
|
+
else
|
|
146
|
+
echo "Skipping Rust audit (Cargo.toml not found)"
|
|
147
|
+
fi
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
audit_python() {
|
|
151
|
+
local pip_audit_version='2.10.1'
|
|
152
|
+
requirement_files=()
|
|
153
|
+
if [ -n "${REPO_FOUNDRY_PYTHON_REQUIREMENT:-}" ] && [ "$REPO_FOUNDRY_PYTHON_REQUIREMENT" != project ] && [ "$REPO_FOUNDRY_PYTHON_REQUIREMENT" != none ]; then
|
|
154
|
+
if ! git ls-files --error-unmatch -- "${REPO_FOUNDRY_PYTHON_REQUIREMENT}" >/dev/null 2>&1; then
|
|
155
|
+
echo "Python requirement file is not tracked: ${REPO_FOUNDRY_PYTHON_REQUIREMENT}" >&2
|
|
156
|
+
return 1
|
|
157
|
+
fi
|
|
158
|
+
requirement_files+=( "$REPO_FOUNDRY_PYTHON_REQUIREMENT" )
|
|
159
|
+
else
|
|
160
|
+
while IFS= read -r requirement_file; do
|
|
161
|
+
[ -n "$requirement_file" ] && requirement_files+=( "$requirement_file" )
|
|
162
|
+
done < <(git ls-files '*requirements*.txt')
|
|
163
|
+
fi
|
|
164
|
+
if [ "${#requirement_files[@]}" -gt 0 ] || [ -f pyproject.toml ]; then
|
|
165
|
+
audits=$((audits + 1))
|
|
166
|
+
if command -v uv >/dev/null 2>&1; then
|
|
167
|
+
local uv_audit_cache="${REPO_FOUNDRY_UV_AUDIT_CACHE:-$HOME/.cache/uv-audit}"
|
|
168
|
+
pip_audit() {
|
|
169
|
+
if [ "${REPO_FOUNDRY_UV_AUDIT_OFFLINE:-false}" = true ]; then
|
|
170
|
+
if UV_CACHE_DIR="$uv_audit_cache" uv tool run --offline --from "pip-audit==${pip_audit_version}" pip-audit "$@"; then
|
|
171
|
+
return 0
|
|
172
|
+
fi
|
|
173
|
+
echo 'Offline pip-audit cache was incomplete; retrying online.' >&2
|
|
174
|
+
fi
|
|
175
|
+
REPO_FOUNDRY_UV_AUDIT_OFFLINE=false UV_CACHE_DIR="$uv_audit_cache" uv tool run --from "pip-audit==${pip_audit_version}" pip-audit "$@"
|
|
176
|
+
}
|
|
177
|
+
else
|
|
178
|
+
python -m pip install --disable-pip-version-check --quiet "pip-audit==${pip_audit_version}"
|
|
179
|
+
pip_audit() { python -m pip_audit "$@"; }
|
|
180
|
+
fi
|
|
181
|
+
if [ "${#requirement_files[@]}" -gt 0 ]; then
|
|
182
|
+
pids=()
|
|
183
|
+
for requirement_file in "${requirement_files[@]}"; do
|
|
184
|
+
pip_audit -r "$requirement_file" &
|
|
185
|
+
pids+=( "$!" )
|
|
186
|
+
done
|
|
187
|
+
wait_for_parallel "${pids[@]}"
|
|
188
|
+
else
|
|
189
|
+
pip_audit
|
|
190
|
+
fi
|
|
191
|
+
else
|
|
192
|
+
echo "Skipping Python audit (Python dependency manifest not found)"
|
|
193
|
+
fi
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
case "$mode" in
|
|
197
|
+
audit|all)
|
|
198
|
+
run_parallel audit_javascript audit_rust audit_python
|
|
199
|
+
;;
|
|
200
|
+
javascript) audit_javascript ;;
|
|
201
|
+
rust) audit_rust ;;
|
|
202
|
+
python) audit_python ;;
|
|
203
|
+
esac
|
|
204
|
+
|
|
205
|
+
if [ "$audits" -eq 0 ] && ! has_dependency_manifest; then
|
|
206
|
+
echo "No supported dependency manifests found; nothing to audit"
|
|
207
|
+
fi
|
|
208
|
+
|
|
209
|
+
exit 0
|
|
@@ -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
|