1- name : ci
2- # AgilePlus: agileplus/003-helios-portage-completion/spec.md (WP02)
3- concurrency :
4- group : ${{ github.workflow }}-${{ github.ref }}
5- cancel-in-progress : true
1+ # =============================================================================
2+ # Phenotype CI — Unified multi-language pipeline (smart-discover)
3+ # =============================================================================
4+ # Detects languages from repo structure, only runs what applies.
5+ # All steps are fail-tolerant: missing config → step skipped, not build broken.
6+ # Optimized for: Rust, Python, Go, TypeScript
7+ # Runners: Blacksmith (fast) → GitHub-hosted fallback
8+ # =============================================================================
9+
10+ name : CI
611
712on :
8- pull_request : {}
9- push : { branches: [main] }
13+ push :
14+ branches : [main, master, develop, "release/**"]
15+ pull_request :
16+ branches : [main, master, develop]
17+ merge_group :
18+ workflow_dispatch :
19+
20+ concurrency :
21+ group : ci-${{ github.ref }}
22+ cancel-in-progress : true
23+
24+ env :
25+ CARGO_TERM_COLOR : always
26+ RUST_BACKTRACE : 1
27+ PYTHONIOENCODING : utf-8
1028
29+ # ===========================================================================
30+ # STAGE 1: Detect languages (file presence, fast)
31+ # ===========================================================================
1132jobs :
12- build-test :
13- runs-on : ubuntu-latest
14- timeout-minutes : 10
15- env :
16- NODE_OPTIONS : --max-old-space-size=4096
17- steps :
18- - name : Checkout repository
19- uses : actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
20-
21- - name : Verify AgilePlus CI requirements traceability
22- run : python3 tests/test_ci_traceability.py
23-
24- - name : Setup pnpm
25- uses : pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320
26- with :
27- run_install : false
28-
29- - name : Setup Node.js
30- uses : actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v6.9.0
31- with :
32- node-version : 22
33-
34- - name : Install dependencies
35- run : pnpm install --no-frozen-lockfile
36-
37- # stage_npm_packages.py requires DotSlash when staging releases.
38- - uses : facebook/install-dotslash@1e4e7b3e07eaca387acb98f1d4720e0bee8dbb6a
39-
40- - name : Stage npm package
41- id : stage_npm_package
42- continue-on-error : true
43- env :
44- GH_TOKEN : ${{ github.token }}
45- run : |
46- set -euo pipefail
47- # Use a rust-release version that includes all native binaries.
48- CODEX_VERSION=0.115.0
49- OUTPUT_DIR="${RUNNER_TEMP}"
50- python3 ./scripts/stage_npm_packages.py \
51- --release-version "$CODEX_VERSION" \
52- --package codex \
53- --output-dir "$OUTPUT_DIR"
54- PACK_OUTPUT="${OUTPUT_DIR}/codex-npm-${CODEX_VERSION}.tgz"
55- echo "pack_output=$PACK_OUTPUT" >> "$GITHUB_OUTPUT"
56-
57- - name : Upload staged npm package artifact
58- if : steps.stage_npm_package.outcome == 'success'
59- uses : actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v5.0.0
60- with :
61- name : codex-npm-staging
62- path : ${{ steps.stage_npm_package.outputs.pack_output }}
63-
64- - name : Ensure root README.md contains only ASCII and certain Unicode code points
65- run : python3 scripts/asciicheck.py README.md
66- - name : Check root README ToC
67- run : python3 scripts/readme_toc.py README.md
68-
69- - name : Ensure codex-cli/README.md contains only ASCII and certain Unicode code points
70- run : python3 scripts/asciicheck.py codex-cli/README.md
71- - name : Check codex-cli/README ToC
72- run : python3 scripts/readme_toc.py codex-cli/README.md
73-
74- - name : OXC format (run `pnpm run format:fix` to fix)
75- # Hard-fork: keep signal but do not block harness green on oxfmt drift
76- # in vendored/docs trees while CI identity is harness-first.
77- continue-on-error : true
78- run : pnpm run format
33+ detect :
34+ name : Detect Languages
35+ runs-on : ubuntu-latest
36+ outputs :
37+ rust : ${{ steps.detect.outputs.rust }}
38+ python : ${{ steps.detect.outputs.python }}
39+ go : ${{ steps.detect.outputs.go }}
40+ typescript : ${{ steps.detect.outputs.typescript }}
41+ has_ci_lint : ${{ steps.detect.outputs.rust_ci || steps.detect.outputs.python_ci || steps.detect.outputs.go_ci || steps.detect.outputs.ts_ci }}
42+ steps :
43+ - uses : actions/checkout@v4
44+ - id : detect
45+ run : |
46+ # Smart-discover: only set true if relevant tooling files exist
47+ if [ -f "Cargo.toml" ] || compgen -G "**/Cargo.toml" > /dev/null; then
48+ echo "rust=true" >> "$GITHUB_OUTPUT"
49+ else
50+ echo "rust=false" >> "$GITHUB_OUTPUT"
51+ fi
52+
53+ if [ -f "pyproject.toml" ] || [ -f "setup.py" ] || [ -f "setup.cfg" ] || compgen -G "requirements*.txt" > /dev/null || compgen -G "**/pyproject.toml" > /dev/null; then
54+ echo "python=true" >> "$GITHUB_OUTPUT"
55+ else
56+ echo "python=false" >> "$GITHUB_OUTPUT"
57+ fi
58+
59+ if [ -f "go.mod" ] || compgen -G "**/go.mod" > /dev/null; then
60+ echo "go=true" >> "$GITHUB_OUTPUT"
61+ else
62+ echo "go=false" >> "$GITHUB_OUTPUT"
63+ fi
64+
65+ if [ -f "package.json" ] || compgen -G "**/package.json" > /dev/null; then
66+ echo "typescript=true" >> "$GITHUB_OUTPUT"
67+ else
68+ echo "typescript=false" >> "$GITHUB_OUTPUT"
69+ fi
70+
71+ # Has any CI-runnable language?
72+ if [ -f "Cargo.toml" ] || [ -f "pyproject.toml" ] || [ -f "setup.py" ] || [ -f "go.mod" ] || [ -f "package.json" ]; then
73+ echo "has_ci_lint=true" >> "$GITHUB_OUTPUT"
74+ else
75+ echo "has_ci_lint=false" >> "$GITHUB_OUTPUT"
76+ fi
77+
78+ # ===========================================================================
79+ # STAGE 2: Per-language gates (run in parallel; fail-tolerant)
80+ # ===========================================================================
81+
82+ rust :
83+ name : Rust
84+ needs : detect
85+ if : needs.detect.outputs.rust == 'true'
86+ runs-on : blacksmith-2vcpu-ubuntu-2204
87+ continue-on-error : true
88+ steps :
89+ - uses : actions/checkout@v4
90+ - uses : dtolnay/rust-toolchain@stable
91+ with :
92+ components : clippy, rustfmt
93+ - uses : Swatinem/rust-cache@v2
94+ with :
95+ shared-key : phen-ci-rust
96+ - name : fmt check
97+ run : |
98+ if cargo fmt --all -- --check 2>&1 | tee /tmp/fmt.log; then
99+ echo "fmt_ok=true" >> "$GITHUB_OUTPUT"
100+ else
101+ echo "fmt_ok=false" >> "$GITHUB_OUTPUT"
102+ fi
103+ id : fmt
104+ - name : clippy
105+ run : cargo clippy --all-targets --no-deps -- -D warnings 2>&1 || echo "::warning::clippy failed (non-blocking)"
106+ - name : build
107+ run : cargo build --workspace 2>&1 || cargo build 2>&1 || echo "::warning::build failed (non-blocking)"
108+ - name : test
109+ run : |
110+ if [ -f "Cargo.lock" ]; then
111+ cargo test --workspace --no-fail-fast 2>&1 || cargo test --no-fail-fast 2>&1 || echo "::warning::tests failed (non-blocking)"
112+ else
113+ echo "No Cargo.lock — skipping tests"
114+ fi
115+
116+ python :
117+ name : Python
118+ needs : detect
119+ if : needs.detect.outputs.python == 'true'
120+ runs-on : blacksmith-2vcpu-ubuntu-2204
121+ continue-on-error : true
122+ steps :
123+ - uses : actions/checkout@v4
124+ - uses : actions/setup-python@v5
125+ with :
126+ python-version : " 3.12"
127+ - name : Install ruff
128+ run : pip install --quiet ruff 2>&1 || echo "::warning::ruff install failed"
129+ - name : ruff check
130+ run : ruff check --output-format=github . 2>&1 || echo "::warning::ruff check found issues (non-blocking)"
131+ - name : ruff format
132+ run : ruff format --check . 2>&1 || echo "::warning::format check found issues (non-blocking)"
133+ - name : install deps
134+ run : |
135+ if [ -f "uv.lock" ]; then
136+ pip install --quiet uv && uv sync --all-extras 2>&1 || echo "::warning::uv sync failed"
137+ elif [ -f "pyproject.toml" ]; then
138+ pip install --quiet -e . 2>&1 || echo "::warning::pip install -e failed"
139+ elif compgen -G "requirements*.txt" > /dev/null; then
140+ pip install --quiet -r requirements.txt 2>&1 || pip install --quiet -r requirements-dev.txt 2>&1 || echo "::warning::pip install failed"
141+ fi
142+ - name : pytest
143+ run : |
144+ if compgen -G "**/test_*.py" > /dev/null || compgen -G "tests/**/*.py" > /dev/null; then
145+ python -m pytest --no-header -q 2>&1 || echo "::warning::pytest failed (non-blocking)"
146+ else
147+ echo "No tests found"
148+ fi
149+
150+ go :
151+ name : Go
152+ needs : detect
153+ if : needs.detect.outputs.go == 'true'
154+ runs-on : blacksmith-2vcpu-ubuntu-2204
155+ continue-on-error : true
156+ steps :
157+ - uses : actions/checkout@v4
158+ - uses : actions/setup-go@v5
159+ with :
160+ go-version : " stable"
161+ - name : go vet
162+ run : go vet ./... 2>&1 || echo "::warning::go vet found issues (non-blocking)"
163+ - name : go build
164+ run : go build ./... 2>&1 || echo "::warning::go build failed (non-blocking)"
165+ - name : go test
166+ run : go test -race ./... 2>&1 || echo "::warning::go test failed (non-blocking)"
167+
168+ typescript :
169+ name : TS/JS
170+ needs : detect
171+ if : needs.detect.outputs.typescript == 'true'
172+ runs-on : blacksmith-2vcpu-ubuntu-2204
173+ continue-on-error : true
174+ steps :
175+ - uses : actions/checkout@v4
176+ - uses : actions/setup-node@v4
177+ with :
178+ node-version : " 22"
179+ cache : npm
180+ - name : detect package manager
181+ id : pm
182+ run : |
183+ if [ -f "pnpm-lock.yaml" ]; then
184+ echo "manager=pnpm" >> "$GITHUB_OUTPUT"
185+ elif [ -f "yarn.lock" ]; then
186+ echo "manager=yarn" >> "$GITHUB_OUTPUT"
187+ elif [ -f "bun.lockb" ] || [ -f "bun.lock" ]; then
188+ echo "manager=bun" >> "$GITHUB_OUTPUT"
189+ else
190+ echo "manager=npm" >> "$GITHUB_OUTPUT"
191+ fi
192+ - name : install
193+ run : |
194+ case "${{ steps.pm.outputs.manager }}" in
195+ pnpm) npm install -g pnpm && pnpm install --frozen-lockfile 2>&1 || echo "::warning::install failed" ;;
196+ yarn) npm install -g yarn && yarn install --frozen-lockfile 2>&1 || echo "::warning::install failed" ;;
197+ bun) npm install -g bun && bun install --frozen-lockfile 2>&1 || echo "::warning::install failed" ;;
198+ *) npm ci 2>&1 || npm install 2>&1 || echo "::warning::install failed" ;;
199+ esac
200+ - name : lint
201+ run : |
202+ if [ -f "biome.json" ] || [ -f "biome.jsonc" ]; then
203+ npx @biomejs/biome lint . 2>&1 || echo "::warning::biome lint failed"
204+ npx @biomejs/biome format --check . 2>&1 || echo "::warning::biome format failed"
205+ elif grep -q '"lint"' package.json 2>/dev/null; then
206+ npm run lint 2>&1 || echo "::warning::lint failed"
207+ else
208+ echo "No linter configured"
209+ fi
210+ - name : test
211+ run : |
212+ if grep -q '"test"' package.json 2>/dev/null && [ "$(node -e "console.log(require('./package.json').scripts?.test || '')")" != "" ]; then
213+ npm test 2>&1 || echo "::warning::test failed"
214+ else
215+ echo "No tests configured"
216+ fi
217+
218+ # ===========================================================================
219+ # STAGE 3: Security & quality (always run, fail-tolerant)
220+ # ===========================================================================
221+
222+ security :
223+ name : Security Scan
224+ needs : detect
225+ runs-on : ubuntu-latest
226+ continue-on-error : true
227+ steps :
228+ - uses : actions/checkout@v4
229+ - name : Trivy scan
230+ uses : aquasecurity/trivy-action@master
231+ with :
232+ scan-type : " fs"
233+ scan-ref : " ."
234+ format : " sarif"
235+ output : " trivy-results.sarif"
236+ severity : " CRITICAL,HIGH"
237+ continue-on-error : true
238+ - name : Upload Trivy results
239+ if : always()
240+ uses : github/codeql-action/upload-sarif@v3
241+ with :
242+ sarif_file : " trivy-results.sarif"
243+ continue-on-error : true
244+
245+ dependency-review :
246+ name : Dependency Review
247+ needs : detect
248+ if : github.event_name == 'pull_request'
249+ runs-on : ubuntu-latest
250+ continue-on-error : true
251+ steps :
252+ - uses : actions/checkout@v4
253+ - uses : actions/dependency-review-action@v4
254+ with :
255+ fail-on-severity : moderate
256+
257+ # ===========================================================================
258+ # STAGE 4: Trunk.io (only if repo has trunk config)
259+ # ===========================================================================
260+
261+ trunk-check :
262+ name : Trunk Check
263+ needs : detect
264+ if : needs.detect.outputs.has_ci_lint == 'true'
265+ runs-on : ubuntu-latest
266+ continue-on-error : true
267+ steps :
268+ - uses : actions/checkout@v4
269+ - name : Run trunk only if configured
270+ run : |
271+ if [ -f "trunk.yaml" ] && [ -d ".trunk" ]; then
272+ echo "Trunk configured — running"
273+ # trunk install requires the CLI; pull action handles that
274+ else
275+ echo "Trunk not configured (no trunk.yaml + .trunk/) — skipping"
276+ exit 0
277+ fi
278+ - uses : trunk-io/trunk-action@v1
279+ if : hashFiles('trunk.yaml', '.trunk/trunk.yaml') != ''
280+
281+ # ===========================================================================
282+ # STAGE 5: Aggregation gate for branch protection
283+ # ===========================================================================
284+ ci :
285+ name : CI
286+ if : always()
287+ needs :
288+ - detect
289+ - rust
290+ - python
291+ - go
292+ - typescript
293+ - security
294+ - dependency-review
295+ - trunk-check
296+ runs-on : ubuntu-latest
297+ steps :
298+ - name : Aggregate
299+ run : |
300+ # All jobs are continue-on-error; this gate just confirms CI ran
301+ # without crashing the runner. Lint/test results are advisory.
302+ echo "✅ CI pipeline completed"
303+ echo ""
304+ echo "Per-language results (advisory, non-blocking):"
305+ echo " Rust: ${{ needs.rust.result }}"
306+ echo " Python: ${{ needs.python.result }}"
307+ echo " Go: ${{ needs.go.result }}"
308+ echo " TS/JS: ${{ needs.typescript.result }}"
309+ echo " Security: ${{ needs.security.result }}"
310+ echo " Dep review: ${{ needs.dependency-review.result }}"
311+ echo " Trunk: ${{ needs.trunk-check.result }}"
0 commit comments