Repository navigation
Expand file tree
/
Copy pathci_local.sh
More file actions
executable file
·234 lines (204 loc) · 7.71 KB
/
Copy pathci_local.sh
File metadata and controls
executable file
·234 lines (204 loc) · 7.71 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
#!/usr/bin/env bash
# Local CI reproduction — defense against GitHub Actions free-tier compression.
# See docs/maintenance/ci-local.md for the full guide.
#
# Strategy:
# 1. If `act` (nektos/act) is available + Docker is running, use it to run the
# actual workflow YAML locally.
# 2. Otherwise, run an explicit fail-closed subset of direct CI commands.
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "$REPO_ROOT"
USAGE=$(cat <<'EOF'
Usage: scripts/shell/ci_local.sh [OPTIONS]
Reproduce CI locally to validate changes before pushing.
OPTIONS:
-j, --job JOB Run only the named job (forwarded to act)
--list List all available jobs and exit
--dryrun Show what would execute without running (forwarded to act)
--no-act Skip act even if available; use direct-command fallback
-h, --help Show this help
Examples:
scripts/shell/ci_local.sh # Run full default workflow
scripts/shell/ci_local.sh -j lint # Run only the lint job
scripts/shell/ci_local.sh -j test-infra # Run an act workflow job
scripts/shell/ci_local.sh --dryrun # Show plan without running
scripts/shell/ci_local.sh --no-act # Force direct-command fallback
Direct-command fallback lanes (when act is unavailable):
- lint CI lint/type/export/generated/confidentiality/module gates
- health blocking static health registry
- verify-no-mocks lexical + zero semantic dependency-replacement policy
- security Bandit MEDIUM+ on the tracked public scope
- tests canonical template project test stage
- precommit all pre-commit and pre-push hooks via uv
- confid scripts/audit/check_tracked_all.py + check_mirror_symlinks.py
- public-readiness all public exemplar tests in isolated subprocesses
With no job selected, run health, tests, precommit, then public-readiness.
The complete health registry and retained hooks cover the shared lint, security,
no-mocks and confidentiality checks; named lanes remain available separately.
The fallback is intentionally described as a subset: workflow matrices,
service containers, dependency auditing, and platform-specific jobs require act
or GitHub Actions. Unsupported fallback job names fail instead of succeeding
without running anything.
EOF
)
USE_ACT=1
JOB=""
DRY_RUN=0
LIST_ONLY=0
while [[ $# -gt 0 ]]; do
case "$1" in
-j|--job)
JOB="$2"
shift 2
;;
--list)
LIST_ONLY=1
shift
;;
--dryrun|--dry-run)
DRY_RUN=1
shift
;;
--no-act)
USE_ACT=0
shift
;;
-h|--help)
echo "$USAGE"
exit 0
;;
*)
echo "Unknown option: $1" >&2
echo "$USAGE" >&2
exit 2
;;
esac
done
# Detect act availability
HAS_ACT=0
if [[ "$USE_ACT" -eq 1 ]] && command -v act >/dev/null 2>&1; then
if docker info >/dev/null 2>&1; then
HAS_ACT=1
else
echo "[ci_local] act is installed but Docker is not running — falling back to direct commands." >&2
fi
fi
# Path A: use act
if [[ "$HAS_ACT" -eq 1 ]]; then
echo "[ci_local] Repository root: $REPO_ROOT"
echo "[ci_local] Running via act (Docker)"
ACT_ARGS=()
# Avoid mutating the checkout merely by inspecting CI. A user-owned .actrc
# still takes precedence; otherwise pass stable runner mappings explicitly.
if [[ ! -f "$REPO_ROOT/.actrc" ]]; then
ACT_ARGS+=("-P" "ubuntu-latest=catthehacker/ubuntu:act-latest")
ACT_ARGS+=("-P" "ubuntu-22.04=catthehacker/ubuntu:act-22.04")
ACT_ARGS+=("-P" "ubuntu-20.04=catthehacker/ubuntu:act-20.04")
fi
if [[ "$LIST_ONLY" -eq 1 ]]; then
ACT_ARGS+=("--list")
fi
if [[ "$DRY_RUN" -eq 1 ]]; then
ACT_ARGS+=("--dryrun")
fi
if [[ -n "$JOB" ]]; then
ACT_ARGS+=("-j" "$JOB")
fi
exec act "${ACT_ARGS[@]}"
fi
# Path B: direct-command fallback
echo "[ci_local] Repository root: $REPO_ROOT"
if [[ "$LIST_ONLY" -eq 1 ]]; then
cat <<'EOF'
Direct-command fallback lanes (act not available):
- lint
- health
- verify-no-mocks
- security
- tests
- precommit
- confid
- public-readiness
EOF
exit 0
fi
if [[ -n "$JOB" ]]; then
case "$JOB" in
lint|health|verify-no-mocks|security|tests|precommit|confid|public-readiness) ;;
*)
echo "[ci_local] Unsupported direct-command fallback lane: $JOB" >&2
echo "[ci_local] Use --list to see supported lanes, or run with act for workflow job IDs." >&2
exit 2
;;
esac
fi
if [[ "$DRY_RUN" -eq 1 ]]; then
if [[ -n "$JOB" ]]; then
echo "[ci_local] DRYRUN — would run direct-command fallback lane: $JOB"
else
echo "[ci_local] DRYRUN — would run direct-command fallback lanes: health, tests, precommit, public-readiness"
fi
exit 0
fi
echo "[ci_local] act unavailable — running direct-command fallback"
echo
if [[ "$JOB" == "lint" ]]; then
echo "[ci_local] === CI lint job ==="
# Assignment preserves the producer's failure before any lint/type command.
PUBLIC_CI_PATHS=$(uv run python -m infrastructure.project.public_scope ci-paths)
PUBLIC_CI_LINT_ROW=${PUBLIC_CI_PATHS%%$'\n'*}
PUBLIC_CI_SOURCE_ROW=${PUBLIC_CI_PATHS#*$'\n'}
[[ "$PUBLIC_CI_LINT_ROW" == lint_paths=* && "$PUBLIC_CI_SOURCE_ROW" == source_paths=* ]]
read -r -a PUBLIC_CI_LINT_PATHS <<<"${PUBLIC_CI_LINT_ROW#lint_paths=}"
read -r -a PUBLIC_CI_SOURCE_PATHS <<<"${PUBLIC_CI_SOURCE_ROW#source_paths=}"
[[ ${#PUBLIC_CI_LINT_PATHS[@]} -gt 0 && ${#PUBLIC_CI_SOURCE_PATHS[@]} -gt 0 ]]
uv run ruff check "${PUBLIC_CI_LINT_PATHS[@]}"
uv run ruff format --check "${PUBLIC_CI_LINT_PATHS[@]}"
uv run python scripts/gates/mypy_ratchet.py "${PUBLIC_CI_SOURCE_PATHS[@]}"
uv run python -m infrastructure.skills check-all-exports
uv run python -m infrastructure.skills operations-check
uv run python scripts/audit/check_tracked_generated_artifacts.py
uv run python scripts/audit/check_tracked_all.py
uv run python scripts/audit/check_mirror_symlinks.py
uv run python scripts/gates/module_line_count_check.py
fi
if [[ -z "$JOB" || "$JOB" == "health" ]]; then
echo "[ci_local] === Blocking static health registry ==="
uv run python -m infrastructure.core.health
fi
if [[ "$JOB" == "verify-no-mocks" ]]; then
echo "[ci_local] === No-mocks and semantic stand-in policy ==="
uv run python scripts/audit/verify_no_mocks.py
uv run python scripts/audit/verify_no_mocks.py --inventory --max-dependency-replacements 0
fi
if [[ "$JOB" == "security" ]]; then
echo "[ci_local] === Bandit MEDIUM+ (public scope; dependency audit remains act-only) ==="
read -r -a PUBLIC_PROJECTS <<<"$(uv run python -m infrastructure.project.public_scope project-names)"
SECURITY_TARGETS=(infrastructure/ scripts/)
for project in "${PUBLIC_PROJECTS[@]}"; do
SECURITY_TARGETS+=("projects/$project/")
done
uv run bandit -c bandit.yaml -r -ll "${SECURITY_TARGETS[@]}"
fi
if [[ -z "$JOB" || "$JOB" == "tests" ]]; then
echo "[ci_local] === Canonical project test stage (matrix remains act-only) ==="
PROJECT="${TEMPLATE_CI_PROJECT:-templates/template_code_project}"
uv run python scripts/pipeline/stage_01_test.py --project "$PROJECT"
fi
if [[ -z "$JOB" || "$JOB" == "precommit" ]]; then
echo "[ci_local] === Pre-commit (all stages) ==="
uv run pre-commit run --all-files
uv run pre-commit run --hook-stage pre-push --all-files
fi
if [[ "$JOB" == "confid" ]]; then
echo "[ci_local] === Confidentiality check ==="
uv run python scripts/audit/check_tracked_all.py
uv run python scripts/audit/check_mirror_symlinks.py
fi
if [[ -z "$JOB" || "$JOB" == "public-readiness" ]]; then
echo "[ci_local] === Public exemplar readiness matrix ==="
uv run python scripts/gates/public_readiness.py
fi
echo
echo "[ci_local] All requested direct-command checks passed."