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.
- package/.githooks/pre-commit +20 -5
- package/.github/CODEOWNERS +4 -2
- package/.github/CONTRIBUTING.md +5 -1
- package/.github/actions/cache/action.yml +43 -0
- package/.github/actions/codeql/action.yml +29 -0
- package/.github/actions/setup/action.yml +756 -0
- package/.github/scripts/bootstrap.sh +7 -0
- package/.github/scripts/changed-files.sh +96 -0
- package/.github/scripts/ci.sh +421 -28
- package/.github/scripts/codeql-languages.sh +71 -5
- package/.github/scripts/doctor.sh +1 -1
- package/.github/scripts/format-fast-path.sh +80 -0
- package/.github/scripts/init-repo.sh +39 -11
- package/.github/scripts/profile.sh +179 -0
- package/.github/scripts/security.sh +180 -20
- package/.github/scripts/sync-codeowners.sh +76 -0
- package/.github/scripts/sync-protection.sh +15 -2
- package/.github/scripts/sync-template.sh +222 -15
- package/.github/scripts/turbo-cache-probe.sh +102 -0
- package/.github/template.yml +14 -0
- package/.github/template.yml.example +10 -0
- package/.github/workflows/ci.yml +108 -7
- package/.github/workflows/codeql.yml +126 -17
- package/.github/workflows/draft-pr.yml +4 -7
- package/.github/workflows/release-pr.yml +20 -7
- package/.github/workflows/release.yml +99 -7
- package/.github/workflows/security.yml +201 -6
- package/.github/workflows/test.yml +100 -3
- package/.gitignore +4 -0
- package/.mise.toml +0 -2
- package/AGENTS.md +1 -0
- package/CHANGELOG.md +12 -0
- package/README.md +92 -10
- package/package.json +11 -1
- package/release-please-config.json +16 -0
- package/src/cli.mjs +20 -8
- 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/
|
|
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:
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
languages: $
|
|
101
|
-
features: $
|
|
102
|
-
package_manager: $
|
|
103
|
-
|
|
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
|
-
|
|
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
|
-
|
|
30
|
-
echo "Skipping JavaScript/TypeScript audit (package.json not found)"
|
|
31
|
-
fi
|
|
140
|
+
}
|
|
32
141
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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 [
|
|
42
|
-
|
|
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
|
-
|
|
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"
|