Files
BrunoGaoandBrunoGaoSZ e29e2eed81 feat(governance): finalize lifecycle controls and compliance baseline (#22)
* fix(ci): sanitize branch docker tags for ghcr builds

* feat(governance): finalize lifecycle controls and compliance baseline

* fix(governance): unblock p0 policy and gate checks

* fix(governance): add drift soft-fail and compile compatibility layer

* fix(app): scan legacy mapper package for mybatis

* fix(coverage): add jacoco and bootstrap soft-fail gate

* fix(gate): stabilize spec-quality and analysis lint checks

* fix(governance): restore legacy app imports for coverage build

* fix(ci): include master branch in ci triggers

* fix(ci): stabilize backend and frontend required checks

* fix(ci): allow frontend install without frozen lockfile

* fix(ci): soft-fail frontend lint in required check

---------

Co-authored-by: BrunoGaoSZ <[email protected]>
2026-03-03 22:40:05 +08:00

390 lines
9.9 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
DRY_RUN=true
PHASE_RANGE=""
GATE_RULESET=""
REPORT_PATH="docs/reports/"
ARCHITECT_TEAM="@org/architects"
SECURITY_TEAM="@org/security"
DBA_TEAM="@org/dba"
PRODUCT_TEAM="@org/product"
OFFICIAL_ONLY="true"
INIT_MILESTONES="false"
usage() {
cat <<USAGE
Usage: scripts/setup-github.sh [options]
Options:
--dry-run Print commands only (default)
--apply Execute commands
--phase-range <start-end> Example: 20-27
--gate-ruleset <value> Example: R01-R09
--report-path <path> Default: docs/reports/
--architect-team <team> Default: @org/architects
--security-team <team> Default: @org/security
--dba-team <team> Default: @org/dba
--product-team <team> Default: @org/product
--official-only <true|false> Default: true
--init-milestones Optional module C (default: off)
-h, --help Show help
USAGE
}
print_cmd() {
printf '[dry-run] '
printf '%q ' "$@"
echo
}
require_gh() {
if ! command -v gh >/dev/null 2>&1; then
echo "gh CLI not found. Install: https://cli.github.com/" >&2
exit 1
fi
}
detect_phase_range() {
local max_phase=""
if [[ -d spec/phase ]]; then
max_phase="$(find spec/phase -maxdepth 1 -type f -name 'phase-*.md' -printf '%f\n' 2>/dev/null \
| sed -E 's/^phase-([0-9]{2})\.md$/\1/' \
| sort -n \
| tail -n1 || true)"
fi
if [[ -n "$max_phase" ]]; then
PHASE_RANGE="00-$((10#$max_phase))"
else
PHASE_RANGE="00-53"
fi
}
detect_gate_ruleset() {
if [[ -f scripts/gates/gate-rules.sh ]]; then
if grep -q 'R01' scripts/gates/gate-rules.sh && grep -q 'R09' scripts/gates/gate-rules.sh; then
GATE_RULESET="R01-R09"
return
fi
fi
GATE_RULESET="R01-R09"
}
detect_repo() {
local origin
origin="$(git config --get remote.origin.url 2>/dev/null || true)"
case "$origin" in
[email protected]:*.git)
REPO="${origin#[email protected]:}"
REPO="${REPO%.git}"
;;
https://github.com/*/*.git)
REPO="${origin#https://github.com/}"
REPO="${REPO%.git}"
;;
https://github.com/*/*)
REPO="${origin#https://github.com/}"
;;
*)
REPO=""
;;
esac
}
while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run)
DRY_RUN=true
shift
;;
--apply)
DRY_RUN=false
shift
;;
--phase-range)
PHASE_RANGE="${2:?missing value for --phase-range}"
shift 2
;;
--gate-ruleset)
GATE_RULESET="${2:?missing value for --gate-ruleset}"
shift 2
;;
--report-path)
REPORT_PATH="${2:?missing value for --report-path}"
shift 2
;;
--architect-team)
ARCHITECT_TEAM="${2:?missing value for --architect-team}"
shift 2
;;
--security-team)
SECURITY_TEAM="${2:?missing value for --security-team}"
shift 2
;;
--dba-team)
DBA_TEAM="${2:?missing value for --dba-team}"
shift 2
;;
--product-team)
PRODUCT_TEAM="${2:?missing value for --product-team}"
shift 2
;;
--official-only)
OFFICIAL_ONLY="${2:?missing value for --official-only}"
shift 2
;;
--init-milestones)
INIT_MILESTONES="true"
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown option: $1" >&2
usage
exit 1
;;
esac
done
require_gh
if [[ -z "$PHASE_RANGE" ]]; then
detect_phase_range
fi
if [[ -z "$GATE_RULESET" ]]; then
detect_gate_ruleset
fi
if ! [[ "$PHASE_RANGE" =~ ^([0-9]{1,2})-([0-9]{1,2})$ ]]; then
echo "Invalid PHASE_RANGE: $PHASE_RANGE (expected start-end, e.g. 20-27)" >&2
exit 1
fi
START_PHASE="${BASH_REMATCH[1]}"
END_PHASE="${BASH_REMATCH[2]}"
START_PHASE=$((10#$START_PHASE))
END_PHASE=$((10#$END_PHASE))
if (( END_PHASE < START_PHASE )); then
echo "Invalid PHASE_RANGE: end < start" >&2
exit 1
fi
if [[ "$OFFICIAL_ONLY" != "true" && "$OFFICIAL_ONLY" != "false" ]]; then
echo "OFFICIAL_ONLY must be true or false" >&2
exit 1
fi
REPO="${GITHUB_REPOSITORY:-}"
if [[ -z "$REPO" ]]; then
detect_repo
fi
if [[ -z "$REPO" ]]; then
REPO="$(gh repo view --json nameWithOwner -q .nameWithOwner 2>/dev/null || true)"
fi
if [[ -z "$REPO" ]]; then
echo "Unable to detect repository name. Set GITHUB_REPOSITORY=owner/repo." >&2
exit 1
fi
declare -A EXISTING_LABELS
gather_existing_labels() {
local label
while IFS= read -r label; do
[[ -n "$label" ]] && EXISTING_LABELS["$label"]=1
done < <(gh label list --repo "$REPO" --limit 1000 --json name --jq '.[].name')
}
label_exists() {
local name="$1"
[[ -n "${EXISTING_LABELS[$name]+x}" ]]
}
upsert_label() {
local name="$1"
local color="$2"
local description="$3"
if [[ "$DRY_RUN" == "true" ]]; then
print_cmd gh label create "$name" --repo "$REPO" --color "$color" --description "$description" --force
return
fi
if label_exists "$name"; then
gh label edit "$name" --repo "$REPO" --color "$color" --description "$description" >/dev/null
else
gh label create "$name" --repo "$REPO" --color "$color" --description "$description" >/dev/null
EXISTING_LABELS["$name"]=1
fi
}
declare -A EXISTING_MILESTONES
gather_existing_milestones() {
local title
while IFS= read -r title; do
[[ -n "$title" ]] && EXISTING_MILESTONES["$title"]=1
done < <(gh api "repos/$REPO/milestones?state=all&per_page=100" --jq '.[].title')
}
milestone_exists() {
local title="$1"
[[ -n "${EXISTING_MILESTONES[$title]+x}" ]]
}
create_milestone_if_missing() {
local title="$1"
local desc="$2"
if [[ "$DRY_RUN" == "true" ]]; then
print_cmd gh api -X POST "repos/$REPO/milestones" -f title="$title" -f description="$desc"
return
fi
if milestone_exists "$title"; then
echo "[skip] milestone exists: $title"
return
fi
gh api -X POST "repos/$REPO/milestones" -f title="$title" -f description="$desc" >/dev/null
EXISTING_MILESTONES["$title"]=1
echo "[ok] milestone created: $title"
}
configure_codeowners() {
local file=".github/CODEOWNERS"
if [[ ! -f "$file" ]]; then
echo "[skip] CODEOWNERS not found: $file"
return
fi
if [[ "$DRY_RUN" == "true" ]]; then
print_cmd sed -i "s#@org/architects#$ARCHITECT_TEAM#g" "$file"
print_cmd sed -i "s#@org/security#$SECURITY_TEAM#g" "$file"
print_cmd sed -i "s#@org/dba#$DBA_TEAM#g" "$file"
print_cmd sed -i "s#@org/product#$PRODUCT_TEAM#g" "$file"
return
fi
sed -i "s#@org/architects#$ARCHITECT_TEAM#g" "$file"
sed -i "s#@org/security#$SECURITY_TEAM#g" "$file"
sed -i "s#@org/dba#$DBA_TEAM#g" "$file"
sed -i "s#@org/product#$PRODUCT_TEAM#g" "$file"
echo "[ok] CODEOWNERS teams applied"
}
print_inputs() {
cat <<INFO
Repository: $REPO
DRY_RUN: $DRY_RUN
PHASE_RANGE: $PHASE_RANGE
GATE_RULESET: $GATE_RULESET
REPORT_PATH: $REPORT_PATH
ARCHITECT_TEAM: $ARCHITECT_TEAM
SECURITY_TEAM: $SECURITY_TEAM
DBA_TEAM: $DBA_TEAM
PRODUCT_TEAM: $PRODUCT_TEAM
OFFICIAL_ONLY: $OFFICIAL_ONLY
INIT_MILESTONES: $INIT_MILESTONES
INFO
}
check_paths() {
local path
for path in scripts/gates scripts spec CLAUDE.md .github/workflows; do
if [[ -e "$path" ]]; then
echo "[ok] path detected: $path"
else
echo "[skip] path missing: $path"
fi
done
}
setup_labels() {
echo "==> Setup labels"
local phase
for ((phase=0; phase<=END_PHASE; phase++)); do
upsert_label "phase-$(printf '%02d' "$phase")" "0075ca" "Phase $(printf '%02d' "$phase")"
done
upsert_label "type:feature" "0e8a16" "New feature"
upsert_label "type:bugfix" "d73a4a" "Bug fix"
upsert_label "type:refactor" "fbca04" "Refactor"
upsert_label "type:test" "9b59b6" "Testing"
upsert_label "type:docs" "bfdadc" "Documentation"
upsert_label "type:gate-fix" "e67e22" "Fix for gate violations"
upsert_label "priority:P0" "b60205" "Blocker"
upsert_label "priority:P1" "d93f0b" "High priority"
upsert_label "priority:P2" "f9d0c4" "Normal priority"
upsert_label "gate:pass" "0e8a16" "Gate passed"
upsert_label "gate:fail" "d73a4a" "Gate failed"
upsert_label "gate:pending" "8a8a8a" "Gate pending"
upsert_label "gate:skip" "bfdadc" "Gate skipped"
upsert_label "workflow:brief" "5319e7" "Brief stage"
upsert_label "workflow:spec" "1d76db" "Spec stage"
upsert_label "workflow:coding" "0e8a16" "Coding stage"
upsert_label "workflow:gate" "e67e22" "Gate checking"
upsert_label "workflow:review" "8a8a8a" "Review stage"
upsert_label "workflow:done" "0e8a16" "Done stage"
upsert_label "workstream:baseline" "1d76db" "Baseline workstream"
upsert_label "workstream:regression" "5319e7" "Regression workstream"
upsert_label "workstream:coverage" "fbca04" "Coverage workstream"
upsert_label "workstream:infra" "8a8a8a" "Infra workstream"
upsert_label "suite:security" "d73a4a" "Security test suite"
upsert_label "suite:tenant" "e67e22" "Tenant test suite"
upsert_label "suite:crud" "0e8a16" "CRUD test suite"
upsert_label "suite:openapi" "1d76db" "OpenAPI test suite"
upsert_label "suite:perf" "5319e7" "Performance test suite"
upsert_label "suite:other" "bfdadc" "Other test suite"
upsert_label "gate-failure" "b60205" "Created from gate failure"
}
setup_milestones() {
if [[ "$INIT_MILESTONES" != "true" ]]; then
echo "==> Milestones init disabled (use --init-milestones to enable)"
return
fi
echo "==> Setup milestones"
local phase title desc
for ((phase=START_PHASE; phase<=END_PHASE; phase++)); do
title="Phase $(printf '%02d' "$phase")"
desc="Phase $(printf '%02d' "$phase") delivery tracking.\nGate ruleset: $GATE_RULESET\nReport path: $REPORT_PATH"
create_milestone_if_missing "$title" "$desc"
done
}
main() {
print_inputs
check_paths
if [[ "$DRY_RUN" == "false" ]]; then
gather_existing_labels
gather_existing_milestones
fi
configure_codeowners
setup_labels
setup_milestones
echo "==> Done"
if [[ "$DRY_RUN" == "true" ]]; then
echo "Dry-run mode: no GitHub resources were changed."
fi
}
main "$@"