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,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"
|
|
@@ -0,0 +1,137 @@
|
|
|
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
|
+
if [ -f .github/template.yml ]; then
|
|
11
|
+
configured_features="$(awk -F': ' '/^features:/ {print $2; exit}' .github/template.yml)"
|
|
12
|
+
configured_languages="$(awk -F': ' '/^languages:/ {print $2; exit}' .github/template.yml)"
|
|
13
|
+
[ -n "$configured_features" ] || configured_features="all"
|
|
14
|
+
[ -n "$configured_languages" ] || configured_languages="auto"
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
feature_enabled() {
|
|
18
|
+
[ "$configured_features" = all ] && return 0
|
|
19
|
+
case " $(printf '%s' "$configured_features" | tr ',' ' ') " in
|
|
20
|
+
*" $1 "*) return 0 ;;
|
|
21
|
+
*) return 1 ;;
|
|
22
|
+
esac
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
language_enabled() {
|
|
26
|
+
[ "$configured_languages" = auto ] || [ "$configured_languages" = all ] && return 0
|
|
27
|
+
case " $(printf '%s' "$configured_languages" | tr ',' ' ') " in
|
|
28
|
+
*" $1 "*) return 0 ;;
|
|
29
|
+
*) return 1 ;;
|
|
30
|
+
esac
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
usage() {
|
|
34
|
+
printf '%s\n' 'Usage: sync-protection.sh --repo OWNER/REPO [--branch main] [--check] [--apply]'
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
while [ "$#" -gt 0 ]; do
|
|
38
|
+
case "$1" in
|
|
39
|
+
--repo) repo="${2:?missing OWNER/REPO}"; shift 2 ;;
|
|
40
|
+
--branch) branch="${2:?missing branch}"; shift 2 ;;
|
|
41
|
+
--check) mode="check"; shift ;;
|
|
42
|
+
--apply) mode="apply"; shift ;;
|
|
43
|
+
-h|--help) usage; exit 0 ;;
|
|
44
|
+
*) usage >&2; exit 2 ;;
|
|
45
|
+
esac
|
|
46
|
+
done
|
|
47
|
+
|
|
48
|
+
[ -n "$repo" ] || { usage >&2; exit 2; }
|
|
49
|
+
command -v gh >/dev/null 2>&1 || { echo "gh is required" >&2; exit 1; }
|
|
50
|
+
|
|
51
|
+
is_private="$(gh repo view "$repo" --json isPrivate --jq '.isPrivate')"
|
|
52
|
+
code_security_status="disabled"
|
|
53
|
+
if [ "$is_private" = true ]; then
|
|
54
|
+
code_security_status="$(gh api "repos/$repo" --jq '.security_and_analysis.code_security.status // "disabled"' 2>/dev/null || printf 'disabled')"
|
|
55
|
+
fi
|
|
56
|
+
contexts=()
|
|
57
|
+
if feature_enabled ci; then contexts+=(Format Lint Type-Check Build); fi
|
|
58
|
+
if feature_enabled test; then contexts+=(Unit Integration E2E Smoke); fi
|
|
59
|
+
if feature_enabled security; then
|
|
60
|
+
contexts+=(Profile 'Dependency Audit (JavaScript)' 'Dependency Audit (Rust)' 'Dependency Audit (Python)')
|
|
61
|
+
fi
|
|
62
|
+
if feature_enabled codeql; then contexts+=(Detect); fi
|
|
63
|
+
|
|
64
|
+
if { [ "$is_private" != true ] || [ "$code_security_status" = enabled ]; } && feature_enabled codeql; then
|
|
65
|
+
contexts+=("Analyze (Actions)")
|
|
66
|
+
if language_enabled typescript && git ls-files -- '*.ts' '*.tsx' '*.js' '*.jsx' package.json tsconfig\*.json | grep -q .; then contexts+=("Analyze (TypeScript)"); fi
|
|
67
|
+
if language_enabled python && git ls-files -- '*.py' pyproject.toml requirements\*.txt setup.py ':!.github/**' | grep -q .; then contexts+=("Analyze (Python)"); fi
|
|
68
|
+
if language_enabled rust && git ls-files -- '*.rs' Cargo.toml Cargo.lock | grep -q .; then contexts+=("Analyze (Rust)"); fi
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
protection="$(gh api "repos/$repo/branches/$branch/protection" 2>/dev/null || true)"
|
|
72
|
+
if ! jq -e 'type == "object" and (.required_status_checks | type == "object")' >/dev/null 2>&1 <<< "$protection"; then
|
|
73
|
+
if [ "$mode" = apply ]; then
|
|
74
|
+
echo "Cannot read branch protection for $repo:$branch (repository plan or permissions may not allow it)." >&2
|
|
75
|
+
exit 1
|
|
76
|
+
fi
|
|
77
|
+
existing=""
|
|
78
|
+
else
|
|
79
|
+
existing="$(jq -r '.required_status_checks.contexts[]?' <<< "$protection")"
|
|
80
|
+
fi
|
|
81
|
+
preserved=()
|
|
82
|
+
while IFS= read -r context; do
|
|
83
|
+
[ -n "$context" ] || continue
|
|
84
|
+
case "$context" in
|
|
85
|
+
'Slither / Analyze') continue ;; # removed from the standard workflow set
|
|
86
|
+
# Remove both the current concise job names and older workflow-prefixed
|
|
87
|
+
# names before rebuilding the standard set. This prevents stale required
|
|
88
|
+
# checks from remaining required after a repository changes visibility or
|
|
89
|
+
# template version.
|
|
90
|
+
'Dependency Audit'|'Dependency Audit ('*|'Profile'|'Test Profile'|\
|
|
91
|
+
'Format'|'Lint'|'Type-Check'|'Build'|'Unit'|'Integration'|'E2E'|'Smoke'|\
|
|
92
|
+
'Detect'|'Analyze'|'Analyze ('*|\
|
|
93
|
+
'CI / '*|'Test / '*|'Security / '*|'CodeQL / '*) ;;
|
|
94
|
+
*) preserved+=("$context") ;;
|
|
95
|
+
esac
|
|
96
|
+
done <<< "$existing"
|
|
97
|
+
|
|
98
|
+
payload="$(printf '%s\n' "${preserved[@]-}" "${contexts[@]}" | jq -Rsc 'split("\n") | map(select(length > 0)) | unique | {strict: true, contexts: .}')"
|
|
99
|
+
|
|
100
|
+
if [ "$mode" = check ]; then
|
|
101
|
+
printf '%s\n' "$payload" | jq .
|
|
102
|
+
exit 0
|
|
103
|
+
fi
|
|
104
|
+
|
|
105
|
+
current="$protection"
|
|
106
|
+
full_payload="$(jq --argjson checks "$payload" '
|
|
107
|
+
{
|
|
108
|
+
required_status_checks: $checks,
|
|
109
|
+
enforce_admins: .enforce_admins.enabled,
|
|
110
|
+
required_pull_request_reviews: (
|
|
111
|
+
if .required_pull_request_reviews == null then null else {
|
|
112
|
+
dismiss_stale_reviews: .required_pull_request_reviews.dismiss_stale_reviews,
|
|
113
|
+
require_code_owner_reviews: .required_pull_request_reviews.require_code_owner_reviews,
|
|
114
|
+
required_approving_review_count: .required_pull_request_reviews.required_approving_review_count,
|
|
115
|
+
require_last_push_approval: .required_pull_request_reviews.require_last_push_approval
|
|
116
|
+
} end
|
|
117
|
+
),
|
|
118
|
+
restrictions: (
|
|
119
|
+
if .restrictions == null then null else {
|
|
120
|
+
users: [.restrictions.users[].login],
|
|
121
|
+
teams: [.restrictions.teams[].slug],
|
|
122
|
+
apps: [.restrictions.apps[].slug]
|
|
123
|
+
} end
|
|
124
|
+
),
|
|
125
|
+
required_linear_history: .required_linear_history.enabled,
|
|
126
|
+
allow_force_pushes: .allow_force_pushes.enabled,
|
|
127
|
+
allow_deletions: .allow_deletions.enabled,
|
|
128
|
+
block_creations: .block_creations.enabled,
|
|
129
|
+
required_conversation_resolution: .required_conversation_resolution.enabled,
|
|
130
|
+
lock_branch: .lock_branch.enabled,
|
|
131
|
+
allow_fork_syncing: .allow_fork_syncing.enabled
|
|
132
|
+
}
|
|
133
|
+
' <<< "$current")"
|
|
134
|
+
|
|
135
|
+
gh api --method PUT "repos/$repo/branches/$branch/protection" \
|
|
136
|
+
--input - <<< "$full_payload" >/dev/null
|
|
137
|
+
printf 'Updated required checks for %s:%s\n' "$repo" "$branch"
|
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
usage() {
|
|
5
|
+
cat <<'EOF'
|
|
6
|
+
Usage: sync-template.sh --source PATH_OR_URL [options]
|
|
7
|
+
|
|
8
|
+
Synchronize the repository-owned baseline without replacing project-specific
|
|
9
|
+
README files, mise tool selections, or additional workflows.
|
|
10
|
+
|
|
11
|
+
Options:
|
|
12
|
+
--languages LIST auto or comma-separated: typescript,rust,python,solidity
|
|
13
|
+
--features LIST all or comma-separated standard features
|
|
14
|
+
--package-manager NAME auto, bun, pnpm, yarn, or npm
|
|
15
|
+
--check Preview changes (default)
|
|
16
|
+
--apply Apply changes
|
|
17
|
+
--prune Remove disabled standard workflows
|
|
18
|
+
EOF
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
source_ref="main"
|
|
22
|
+
mode="check"
|
|
23
|
+
source=""
|
|
24
|
+
temp_dir=""
|
|
25
|
+
languages="auto"
|
|
26
|
+
features="all"
|
|
27
|
+
prune=false
|
|
28
|
+
languages_set=false
|
|
29
|
+
features_set=false
|
|
30
|
+
package_manager=""
|
|
31
|
+
package_manager_set=false
|
|
32
|
+
template_ref=""
|
|
33
|
+
release_type="auto"
|
|
34
|
+
npm_publish="false"
|
|
35
|
+
node_version="24.18.0"
|
|
36
|
+
bun_version="1.3.14"
|
|
37
|
+
python_version="3.13"
|
|
38
|
+
rust_version="1.97.1"
|
|
39
|
+
uv_version="0.11.32"
|
|
40
|
+
ruff_version="0.16.0"
|
|
41
|
+
gitignore_backup=""
|
|
42
|
+
custom_ignores=""
|
|
43
|
+
|
|
44
|
+
valid_languages="typescript rust python solidity"
|
|
45
|
+
valid_features="ci codeql security test draft-pr release-pr release dependabot"
|
|
46
|
+
valid_release_types="auto node python rust simple none"
|
|
47
|
+
|
|
48
|
+
contains_word() {
|
|
49
|
+
case " $1 " in *" $2 "*) return 0 ;; *) return 1 ;; esac
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
normalize_csv() {
|
|
53
|
+
printf '%s' "$1" | tr ',' ' ' | awk '{$1=$1; print}'
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
validate_list() {
|
|
57
|
+
local kind="$1" value="$2" valid="$3" item
|
|
58
|
+
[ "$value" = auto ] || [ "$value" = all ] || {
|
|
59
|
+
for item in $(normalize_csv "$value"); do
|
|
60
|
+
contains_word "$valid" "$item" || { echo "Unsupported $kind: $item" >&2; exit 2; }
|
|
61
|
+
done
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
workflow_enabled() {
|
|
66
|
+
local workflow="$1" list
|
|
67
|
+
[ "$features" = all ] && return 0
|
|
68
|
+
list="$(normalize_csv "$features")"
|
|
69
|
+
contains_word "$list" "$workflow"
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
cleanup() {
|
|
73
|
+
if [ -n "$temp_dir" ]; then rm -rf "$temp_dir"; fi
|
|
74
|
+
if [ -n "$gitignore_backup" ]; then rm -f "$gitignore_backup"; fi
|
|
75
|
+
if [ -n "$custom_ignores" ]; then rm -f "$custom_ignores"; fi
|
|
76
|
+
}
|
|
77
|
+
trap cleanup EXIT
|
|
78
|
+
|
|
79
|
+
while [ "$#" -gt 0 ]; do
|
|
80
|
+
case "$1" in
|
|
81
|
+
--source) source="${2:?missing source path or URL}"; shift 2 ;;
|
|
82
|
+
--ref) source_ref="${2:?missing ref}"; shift 2 ;;
|
|
83
|
+
--languages) languages="${2:?missing language list}"; languages_set=true; shift 2 ;;
|
|
84
|
+
--features) features="${2:?missing feature list}"; features_set=true; shift 2 ;;
|
|
85
|
+
--package-manager) package_manager="${2:?missing package manager}"; package_manager_set=true; shift 2 ;;
|
|
86
|
+
--check) mode="check"; shift ;;
|
|
87
|
+
--apply) mode="apply"; shift ;;
|
|
88
|
+
--prune) prune=true; shift ;;
|
|
89
|
+
-h|--help) usage; exit 0 ;;
|
|
90
|
+
*) usage >&2; exit 2 ;;
|
|
91
|
+
esac
|
|
92
|
+
done
|
|
93
|
+
|
|
94
|
+
[ -n "$source" ] || { usage >&2; exit 2; }
|
|
95
|
+
if [ -f .github/template.yml ]; then
|
|
96
|
+
if [ "$languages_set" = false ]; then
|
|
97
|
+
configured_languages="$(awk -F': ' '/^languages:/ {print $2; exit}' .github/template.yml)"
|
|
98
|
+
[ -n "$configured_languages" ] && languages="$configured_languages"
|
|
99
|
+
fi
|
|
100
|
+
if [ "$features_set" = false ]; then
|
|
101
|
+
configured_features="$(awk -F': ' '/^features:/ {print $2; exit}' .github/template.yml)"
|
|
102
|
+
[ -n "$configured_features" ] && features="$configured_features"
|
|
103
|
+
fi
|
|
104
|
+
if [ "$package_manager_set" = false ]; then
|
|
105
|
+
package_manager="$(awk -F': ' '/^package_manager:/ {print $2; exit}' .github/template.yml)"
|
|
106
|
+
fi
|
|
107
|
+
template_ref="$(awk -F': ' '/^template:/ {print $2; exit}' .github/template.yml)"
|
|
108
|
+
configured_release_type="$(awk -F': ' '/^release_type:/ {print $2; exit}' .github/template.yml)"
|
|
109
|
+
configured_npm_publish="$(awk -F': ' '/^npm_publish:/ {print $2; exit}' .github/template.yml)"
|
|
110
|
+
[ -n "$configured_release_type" ] && release_type="$configured_release_type"
|
|
111
|
+
[ -n "$configured_npm_publish" ] && npm_publish="$configured_npm_publish"
|
|
112
|
+
fi
|
|
113
|
+
[ -n "$package_manager" ] || package_manager=auto
|
|
114
|
+
case "$package_manager" in
|
|
115
|
+
auto|bun|pnpm|yarn|npm) ;;
|
|
116
|
+
*) printf 'Unsupported package manager: %s\n' "$package_manager" >&2; exit 2 ;;
|
|
117
|
+
esac
|
|
118
|
+
validate_list language "$languages" "$valid_languages"
|
|
119
|
+
validate_list feature "$features" "$valid_features"
|
|
120
|
+
contains_word "$valid_release_types" "$release_type" || {
|
|
121
|
+
printf 'Unsupported release type: %s\n' "$release_type" >&2
|
|
122
|
+
exit 2
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if [ -d "$source" ] && [ -f "$source/.github/scripts/sync-template.sh" ]; then
|
|
126
|
+
template_root="$source"
|
|
127
|
+
else
|
|
128
|
+
command -v git >/dev/null 2>&1 || { echo "git is required" >&2; exit 1; }
|
|
129
|
+
temp_dir="$(mktemp -d)"
|
|
130
|
+
git clone --quiet --depth 1 --branch "$source_ref" "$source" "$temp_dir/template-repo"
|
|
131
|
+
template_root="$temp_dir/template-repo"
|
|
132
|
+
fi
|
|
133
|
+
|
|
134
|
+
if [ -f "$template_root/package.json" ]; then
|
|
135
|
+
template_version="$(awk -F'"' '/"version"[[:space:]]*:/ {print $4; exit}' "$template_root/package.json")"
|
|
136
|
+
[ -n "$template_version" ] && template_ref="code-foundry@$template_version"
|
|
137
|
+
fi
|
|
138
|
+
|
|
139
|
+
files=(
|
|
140
|
+
.editorconfig
|
|
141
|
+
.gitattributes
|
|
142
|
+
.gitignore
|
|
143
|
+
release-please-config.json
|
|
144
|
+
.githooks/pre-commit
|
|
145
|
+
AGENTS.md
|
|
146
|
+
LICENSE
|
|
147
|
+
NOTICE
|
|
148
|
+
ruff.toml
|
|
149
|
+
.prettierrc
|
|
150
|
+
.github/CODEOWNERS
|
|
151
|
+
.github/template.yml.example
|
|
152
|
+
.github/CODE_OF_CONDUCT.md
|
|
153
|
+
.github/CONTRIBUTING.md
|
|
154
|
+
.github/PULL_REQUEST_TEMPLATE.md
|
|
155
|
+
.github/SECURITY.md
|
|
156
|
+
.github/dependabot.yml
|
|
157
|
+
.github/ISSUE_TEMPLATE/bug_report.yml
|
|
158
|
+
.github/ISSUE_TEMPLATE/config.yml
|
|
159
|
+
.github/ISSUE_TEMPLATE/feature_request.yml
|
|
160
|
+
.github/actions/setup/action.yml
|
|
161
|
+
.github/actions/cache/action.yml
|
|
162
|
+
.github/actions/codeql/action.yml
|
|
163
|
+
.github/scripts/bootstrap.sh
|
|
164
|
+
.github/scripts/changed-files.sh
|
|
165
|
+
.github/scripts/ci.sh
|
|
166
|
+
.github/scripts/codeql-languages.sh
|
|
167
|
+
.github/scripts/doctor.sh
|
|
168
|
+
.github/scripts/security.sh
|
|
169
|
+
.github/scripts/sitecustomize.py
|
|
170
|
+
.github/scripts/sync-template.sh
|
|
171
|
+
.github/scripts/init-repo.sh
|
|
172
|
+
.github/scripts/sync-codeowners.sh
|
|
173
|
+
.github/scripts/sync-protection.sh
|
|
174
|
+
.github/workflows/ci.yml
|
|
175
|
+
.github/workflows/codeql.yml
|
|
176
|
+
.github/workflows/draft-pr.yml
|
|
177
|
+
.github/workflows/release-pr.yml
|
|
178
|
+
.github/workflows/release.yml
|
|
179
|
+
.github/workflows/security.yml
|
|
180
|
+
.github/workflows/test.yml
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
filtered_files=()
|
|
184
|
+
for file in "${files[@]}"; do
|
|
185
|
+
case "$file" in
|
|
186
|
+
.github/dependabot.yml) workflow_enabled dependabot && filtered_files+=("$file") ;;
|
|
187
|
+
.github/workflows/ci.yml) workflow_enabled ci && filtered_files+=("$file") ;;
|
|
188
|
+
.github/workflows/codeql.yml) workflow_enabled codeql && filtered_files+=("$file") ;;
|
|
189
|
+
.github/workflows/security.yml) workflow_enabled security && filtered_files+=("$file") ;;
|
|
190
|
+
.github/workflows/test.yml) workflow_enabled test && filtered_files+=("$file") ;;
|
|
191
|
+
.github/workflows/draft-pr.yml) workflow_enabled draft-pr && filtered_files+=("$file") ;;
|
|
192
|
+
.github/workflows/release-pr.yml) workflow_enabled release-pr && filtered_files+=("$file") ;;
|
|
193
|
+
.github/workflows/release.yml) workflow_enabled release && filtered_files+=("$file") ;;
|
|
194
|
+
*) filtered_files+=("$file") ;;
|
|
195
|
+
esac
|
|
196
|
+
done
|
|
197
|
+
files=("${filtered_files[@]}")
|
|
198
|
+
|
|
199
|
+
# Workflows outside the standard baseline are repository-owned extensions.
|
|
200
|
+
# The sync operation never deletes or replaces them; surface them explicitly
|
|
201
|
+
# so maintainers can verify custom deployment, indexing, or security flows.
|
|
202
|
+
standard_workflow() {
|
|
203
|
+
case "$1" in
|
|
204
|
+
ci.yml|codeql.yml|draft-pr.yml|release-pr.yml|release.yml|security.yml|test.yml) return 0 ;;
|
|
205
|
+
*) return 1 ;;
|
|
206
|
+
esac
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
custom_workflows=()
|
|
210
|
+
if [ -d .github/workflows ]; then
|
|
211
|
+
while IFS= read -r workflow; do
|
|
212
|
+
workflow="${workflow#./}"
|
|
213
|
+
workflow="${workflow#.github/workflows/}"
|
|
214
|
+
standard_workflow "$workflow" || custom_workflows+=("$workflow")
|
|
215
|
+
done < <(find .github/workflows -maxdepth 1 -type f -print | sort)
|
|
216
|
+
fi
|
|
217
|
+
if [ "${#custom_workflows[@]}" -gt 0 ]; then
|
|
218
|
+
printf 'Preserving custom workflows: %s\n' "${custom_workflows[*]}"
|
|
219
|
+
fi
|
|
220
|
+
|
|
221
|
+
changed=0
|
|
222
|
+
for file in "${files[@]}"; do
|
|
223
|
+
template_file="$template_root/$file"
|
|
224
|
+
# npm renames .gitignore to .npmignore when installing a package. Treat the
|
|
225
|
+
# renamed file as the same template asset so packaged initialization works.
|
|
226
|
+
if [ "$file" = .gitignore ] && [ ! -f "$template_file" ] && [ -f "$template_root/.npmignore" ]; then
|
|
227
|
+
template_file="$template_root/.npmignore"
|
|
228
|
+
fi
|
|
229
|
+
if [ ! -f "$template_file" ]; then
|
|
230
|
+
echo "Template file missing: $file" >&2
|
|
231
|
+
exit 1
|
|
232
|
+
fi
|
|
233
|
+
if [ "$file" = .github/CODEOWNERS ] && [ -f "$file" ]; then
|
|
234
|
+
continue
|
|
235
|
+
fi
|
|
236
|
+
if [ ! -f "$file" ] || ! cmp -s "$template_file" "$file"; then
|
|
237
|
+
changed=$((changed + 1))
|
|
238
|
+
if [ "$mode" = "check" ]; then
|
|
239
|
+
printf 'Would sync %s\n' "$file"
|
|
240
|
+
else
|
|
241
|
+
mkdir -p "$(dirname "$file")"
|
|
242
|
+
if [ "$file" = .gitignore ] && [ -f "$file" ]; then
|
|
243
|
+
# Keep repository-specific ignore rules while refreshing the shared
|
|
244
|
+
# baseline. This prevents a template sync from hiding generated files
|
|
245
|
+
# or local tooling that only one project uses.
|
|
246
|
+
gitignore_backup="$(mktemp)"
|
|
247
|
+
custom_ignores="$(mktemp)"
|
|
248
|
+
cp "$file" "$gitignore_backup"
|
|
249
|
+
cp "$template_file" "$file"
|
|
250
|
+
awk 'NR == FNR { seen[$0] = 1; next } /^# Repository-specific rules$/ { next } NF && !seen[$0] { print }' \
|
|
251
|
+
"$template_file" "$gitignore_backup" > "$custom_ignores"
|
|
252
|
+
if [ -s "$custom_ignores" ]; then
|
|
253
|
+
{
|
|
254
|
+
printf '\n# Repository-specific rules\n'
|
|
255
|
+
cat "$custom_ignores"
|
|
256
|
+
} >> "$file"
|
|
257
|
+
fi
|
|
258
|
+
rm -f "$gitignore_backup" "$custom_ignores"
|
|
259
|
+
gitignore_backup=""
|
|
260
|
+
custom_ignores=""
|
|
261
|
+
else
|
|
262
|
+
cp "$template_file" "$file"
|
|
263
|
+
fi
|
|
264
|
+
case "$file" in
|
|
265
|
+
.githooks/*|.github/scripts/*) chmod +x "$file" ;;
|
|
266
|
+
esac
|
|
267
|
+
printf 'Synced %s\n' "$file"
|
|
268
|
+
fi
|
|
269
|
+
fi
|
|
270
|
+
done
|
|
271
|
+
|
|
272
|
+
# Release Please owns changelog updates after initialization. Preserve an
|
|
273
|
+
# existing project history, but give new repositories the expected file.
|
|
274
|
+
if [ ! -f CHANGELOG.md ]; then
|
|
275
|
+
changed=$((changed + 1))
|
|
276
|
+
if [ "$mode" = "check" ]; then
|
|
277
|
+
printf '%s\n' 'Would initialize CHANGELOG.md'
|
|
278
|
+
else
|
|
279
|
+
cat > CHANGELOG.md <<'EOF'
|
|
280
|
+
# Changelog
|
|
281
|
+
|
|
282
|
+
All notable changes to this project are documented here.
|
|
283
|
+
EOF
|
|
284
|
+
printf '%s\n' 'Initialized CHANGELOG.md'
|
|
285
|
+
fi
|
|
286
|
+
fi
|
|
287
|
+
|
|
288
|
+
detect_languages() {
|
|
289
|
+
local detected=()
|
|
290
|
+
if [ -f package.json ] || git ls-files -- '*.js' '*.jsx' '*.ts' '*.tsx' | grep -q .; then
|
|
291
|
+
detected+=(typescript)
|
|
292
|
+
fi
|
|
293
|
+
if [ -f Cargo.toml ] || git ls-files -- '*.rs' | grep -q .; then
|
|
294
|
+
detected+=(rust)
|
|
295
|
+
fi
|
|
296
|
+
if [ -f pyproject.toml ] || [ -f requirements.txt ] || [ -f requirements-dev.txt ] ||
|
|
297
|
+
git ls-files -- '*.py' ':!.github/**' | grep -q .; then
|
|
298
|
+
detected+=(python)
|
|
299
|
+
fi
|
|
300
|
+
if find . -path './.git' -prune -o -type f -name '*.sol' -print | grep -q .; then
|
|
301
|
+
detected+=(solidity)
|
|
302
|
+
fi
|
|
303
|
+
printf '%s\n' "${detected[*]}"
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
initialize_mise() {
|
|
307
|
+
local selected_languages="$1"
|
|
308
|
+
local language selected_package_manager
|
|
309
|
+
if [ -f .mise.toml ]; then
|
|
310
|
+
return
|
|
311
|
+
fi
|
|
312
|
+
if [ "$selected_languages" = auto ]; then
|
|
313
|
+
selected_languages="$(detect_languages)"
|
|
314
|
+
fi
|
|
315
|
+
selected_package_manager="$package_manager"
|
|
316
|
+
if [ "$selected_package_manager" = auto ]; then
|
|
317
|
+
if [ -f bun.lock ] || [ -f bun.lockb ]; then selected_package_manager=bun
|
|
318
|
+
elif [ -f pnpm-lock.yaml ]; then selected_package_manager=pnpm
|
|
319
|
+
elif [ -f yarn.lock ]; then selected_package_manager=yarn
|
|
320
|
+
elif [ -f package-lock.json ]; then selected_package_manager=npm
|
|
321
|
+
else selected_package_manager=bun
|
|
322
|
+
fi
|
|
323
|
+
fi
|
|
324
|
+
changed=$((changed + 1))
|
|
325
|
+
if [ "$mode" = check ]; then
|
|
326
|
+
printf '%s\n' 'Would initialize .mise.toml'
|
|
327
|
+
return
|
|
328
|
+
fi
|
|
329
|
+
{
|
|
330
|
+
printf '%s\n' '[tools]'
|
|
331
|
+
for language in $(normalize_csv "$selected_languages"); do
|
|
332
|
+
case "$language" in
|
|
333
|
+
typescript|solidity)
|
|
334
|
+
printf 'node = "%s"\n' "$node_version"
|
|
335
|
+
if [ "$selected_package_manager" = bun ]; then
|
|
336
|
+
printf 'bun = "%s"\n' "$bun_version"
|
|
337
|
+
fi
|
|
338
|
+
;;
|
|
339
|
+
python)
|
|
340
|
+
printf 'python = "%s"\n' "$python_version"
|
|
341
|
+
printf 'uv = "%s"\n' "$uv_version"
|
|
342
|
+
printf 'ruff = "%s"\n' "$ruff_version"
|
|
343
|
+
;;
|
|
344
|
+
rust)
|
|
345
|
+
printf 'rust = { version = "%s", components = ["rustfmt", "clippy"] }\n' "$rust_version"
|
|
346
|
+
;;
|
|
347
|
+
esac
|
|
348
|
+
done | awk '!seen[$0]++'
|
|
349
|
+
printf '\n%s\n' '[settings]'
|
|
350
|
+
printf '%s\n' 'experimental = true'
|
|
351
|
+
} > .mise.toml
|
|
352
|
+
printf '%s\n' 'Initialized .mise.toml'
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
initialize_mise "$languages"
|
|
356
|
+
|
|
357
|
+
initialize_mise_lock() {
|
|
358
|
+
if [ ! -f .mise.toml ] || [ -f mise.lock ]; then
|
|
359
|
+
return
|
|
360
|
+
fi
|
|
361
|
+
if ! command -v mise >/dev/null 2>&1; then
|
|
362
|
+
return
|
|
363
|
+
fi
|
|
364
|
+
changed=$((changed + 1))
|
|
365
|
+
if [ "$mode" = check ]; then
|
|
366
|
+
printf '%s\n' 'Would initialize mise.lock'
|
|
367
|
+
return
|
|
368
|
+
fi
|
|
369
|
+
if MISE_TRUSTED_CONFIG_PATHS="$PWD" mise lock >/dev/null 2>&1; then
|
|
370
|
+
printf '%s\n' 'Initialized mise.lock'
|
|
371
|
+
else
|
|
372
|
+
printf '%s\n' 'Warning: mise.lock could not be generated; CI will resolve pinned tools normally.' >&2
|
|
373
|
+
fi
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
initialize_mise_lock
|
|
377
|
+
|
|
378
|
+
if [ -x .github/scripts/sync-codeowners.sh ]; then
|
|
379
|
+
if [ "$mode" = "check" ]; then
|
|
380
|
+
bash .github/scripts/sync-codeowners.sh --check
|
|
381
|
+
else
|
|
382
|
+
bash .github/scripts/sync-codeowners.sh --apply
|
|
383
|
+
fi
|
|
384
|
+
fi
|
|
385
|
+
|
|
386
|
+
# Release Please's simple strategy needs a version file. Initialize one only
|
|
387
|
+
# for Solidity-only repositories, where no package manifest owns the version.
|
|
388
|
+
if [ ! -f version.txt ] && find . -path './.git' -prune -o -type f -name '*.sol' -print | grep -q . &&
|
|
389
|
+
[ ! -f package.json ] && [ ! -f Cargo.toml ] && [ ! -f pyproject.toml ]; then
|
|
390
|
+
changed=$((changed + 1))
|
|
391
|
+
if [ "$mode" = "check" ]; then
|
|
392
|
+
printf '%s\n' 'Would initialize version.txt'
|
|
393
|
+
else
|
|
394
|
+
printf '%s\n' '0.1.0' > version.txt
|
|
395
|
+
printf '%s\n' 'Initialized version.txt'
|
|
396
|
+
fi
|
|
397
|
+
fi
|
|
398
|
+
|
|
399
|
+
if [ "$mode" = "apply" ]; then
|
|
400
|
+
git config core.hooksPath .githooks
|
|
401
|
+
mkdir -p .github
|
|
402
|
+
{
|
|
403
|
+
printf 'version: 1\n'
|
|
404
|
+
if [ -n "$template_ref" ]; then
|
|
405
|
+
printf 'template: %s\n' "$template_ref"
|
|
406
|
+
fi
|
|
407
|
+
printf 'languages: %s\n' "$languages"
|
|
408
|
+
printf 'features: %s\n' "$features"
|
|
409
|
+
if [ -n "$package_manager" ]; then
|
|
410
|
+
printf 'package_manager: %s\n' "$package_manager"
|
|
411
|
+
fi
|
|
412
|
+
printf 'release_type: %s\n' "$release_type"
|
|
413
|
+
printf 'npm_publish: %s\n' "$npm_publish"
|
|
414
|
+
} > .github/template.yml
|
|
415
|
+
fi
|
|
416
|
+
|
|
417
|
+
if [ "$prune" = true ]; then
|
|
418
|
+
for workflow in ci codeql security test draft-pr release-pr release; do
|
|
419
|
+
file=".github/workflows/$workflow.yml"
|
|
420
|
+
if [ -f "$file" ] && ! workflow_enabled "$workflow"; then
|
|
421
|
+
changed=$((changed + 1))
|
|
422
|
+
if [ "$mode" = "check" ]; then
|
|
423
|
+
printf 'Would remove disabled standard workflow %s\n' "$file"
|
|
424
|
+
else
|
|
425
|
+
rm "$file"
|
|
426
|
+
printf 'Removed disabled standard workflow %s\n' "$file"
|
|
427
|
+
fi
|
|
428
|
+
fi
|
|
429
|
+
done
|
|
430
|
+
fi
|
|
431
|
+
|
|
432
|
+
printf '%s\n' "$changed baseline file(s) differ."
|