Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 123 additions & 36 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,83 +2,170 @@ name: CI

on:
push:
branches: [main, dev]
branches: [main, preview, dev]
pull_request:
workflow_dispatch:

permissions:
contents: read

# A pull request's newer push supersedes its older run; an integration-line push
# never cancels another, so every dev/main SHA keeps its own evidence.
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

env:
# GitHub runners preinstall uv, which would flip the repo-map smoke test
# from skip to a live network dependency resolve. Keep CI deterministic.
CODEXCLAW_SKIP_REPOMAP_SMOKE: "1"

jobs:
# The one lane that runs the whole suite in a single process. Its measured total
# is what the published tests badge is checked against; a shard cannot know it.
test:
strategy:
matrix:
# macOS is the primary development platform and was the only untested one;
# a path-case or BSD-tool difference would otherwise ship undetected.
os: [ubuntu-latest, windows-latest, macos-latest]
# autocrlf=true is what a default Windows git install does, and it is the
# configuration that turns CRLF-safe-today readers into broken ones.
# Testing only the eol=lf checkout tests a machine our users do not have.
autocrlf: [false]
include:
- os: windows-latest
autocrlf: true
fail-fast: false
runs-on: ${{ matrix.os }}
env:
# GitHub runners preinstall uv, which would flip the repo-map smoke test
# from skip to a live network dependency resolve. Keep CI deterministic.
CODEXCLAW_SKIP_REPOMAP_SMOKE: "1"
name: test (ubuntu-latest, false)
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Configure line endings
run: git config --global core.autocrlf ${{ matrix.autocrlf }}
run: git config --global core.autocrlf false
- uses: actions/checkout@v7
- uses: actions/setup-node@v6
with:
node-version: 24
cache: npm
cache-dependency-path: package-lock.json
- run: npm ci
# Measure the suite ONCE and reuse the total. bash is pinned on purpose: the
# windows runners default to PowerShell, which does not propagate a pipeline's
# exit status, so `npm test | tee` there would report success over a failing
# suite.
#
# The pattern anchors on the RIGHT, not on the leading glyph. `ℹ` is three UTF-8
# bytes, and `^.` only spans it in a UTF-8 locale: Git bash on the windows runners
# runs under C, where `^. tests` matches nothing. In a full log `tests <n>$` matches
# exactly one line — the TAP summary — because a test title never ends that way.
# Measure the suite ONCE and reuse the total. bash is pinned on purpose: a
# PowerShell pipeline does not propagate its exit status. The pattern anchors
# on the RIGHT: `ℹ` is three UTF-8 bytes and `^.` only spans it in a UTF-8
# locale; `tests <n>$` matches exactly the TAP summary line.
- name: Run the suite
id: suite
shell: bash
run: |
set -euo pipefail
npm test 2>&1 | tee /tmp/ci-suite.log
# `|| true` so a miss reaches the explicit check below instead of dying on set -e.
total="$(grep -Eo 'tests [0-9]+$' /tmp/ci-suite.log | tail -1 | grep -Eo '[0-9]+' || true)"
if [ -z "$total" ]; then
echo "could not parse a test total out of the suite output" >&2
exit 1
fi
echo "total=$total" >> "$GITHUB_OUTPUT"
# The tests badge is the one published count that cannot be derived from the
# payload, so it is checked against what the suite just measured. Skipping this
# is how 2,026 survived three versions and stopped the release gate instead.
# payload, so it is checked against what the suite just measured.
- name: Inventory and published counts
shell: bash
run: |
set -euo pipefail
node plugins/codexclaw/scripts/inventory.mjs --check --tests "${{ steps.suite.outputs.total }}"
- run: node plugins/codexclaw/scripts/gate.mjs
# The suites above are largely pure-function suites. This executes the spawn
# ladders, the bench, and the bundle - the surfaces where the audited win32
# defects lived (devlog/_plan/260821_win-linux-optimization/002).
- name: Platform smoke
run: node plugins/codexclaw/scripts/platform-smoke.mjs
- name: Upload receipts
if: always()
uses: actions/upload-artifact@v4
with:
name: receipts-${{ matrix.os }}-crlf${{ matrix.autocrlf }}
name: receipts-ubuntu-latest-crlffalse
path: .codexclaw/evidence/
if-no-files-found: warn

# macOS is the primary development platform; a path-case or BSD-tool difference
# would otherwise ship undetected. Three minutes unsharded, so it stays whole.
test-macos:
name: test (macos-latest, false)
runs-on: macos-latest
timeout-minutes: 12
steps:
- name: Configure line endings
run: git config --global core.autocrlf false
- uses: actions/checkout@v7
- uses: actions/setup-node@v6
with:
node-version: 24
cache: npm
cache-dependency-path: package-lock.json
- run: npm ci
- run: npm test
- run: node plugins/codexclaw/scripts/gate.mjs
- name: Platform smoke
run: node plugins/codexclaw/scripts/platform-smoke.mjs
- name: Upload receipts
if: always()
uses: actions/upload-artifact@v4
with:
name: receipts-macos-latest-crlffalse
path: .codexclaw/evidence/
if-no-files-found: warn

# Windows was the critical path at 9 minutes per leg. Each autocrlf variant runs
# as two shards of scripts/test.mjs --shard i/2 (sorted round-robin over the file
# list, see plugins/codexclaw/test/test-shard.test.mjs), so a leg finishes in
# about half the time. autocrlf=true is what a default Windows git install does;
# testing only eol=lf tests a machine our users do not have.
test-windows:
name: test (windows-latest, ${{ matrix.autocrlf }}, shard ${{ matrix.shard }}/2)
runs-on: windows-latest
timeout-minutes: 12
strategy:
fail-fast: false
matrix:
autocrlf: [false, true]
shard: [1, 2]
steps:
- name: Configure line endings
run: git config --global core.autocrlf ${{ matrix.autocrlf }}
- uses: actions/checkout@v7
- uses: actions/setup-node@v6
with:
node-version: 24
cache: npm
cache-dependency-path: package-lock.json
- run: npm ci
- name: Run shard ${{ matrix.shard }}/2
shell: bash
run: npm test -- --shard ${{ matrix.shard }}/2
- run: node plugins/codexclaw/scripts/gate.mjs
# The spawn ladders, the bench and the bundle run once per autocrlf variant;
# they are fixed cost and paying them per shard would eat what sharding saves.
- name: Platform smoke
if: matrix.shard == 1
run: node plugins/codexclaw/scripts/platform-smoke.mjs
- name: Upload receipts
if: always()
uses: actions/upload-artifact@v4
with:
name: receipts-windows-latest-crlf${{ matrix.autocrlf }}-shard${{ matrix.shard }}
path: .codexclaw/evidence/
if-no-files-found: warn

# One check to require. `if: always()` is load-bearing: without it a failed or
# skipped dependency skips this job, and a skipped required check reads as green
# in the merge box. Every leg must be success; skipped and cancelled fail here.
ci:
name: ci
if: always()
needs: [test, test-macos, test-windows]
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Every leg succeeded
env:
RESULTS: ${{ toJSON(needs) }}
shell: bash
run: |
set -euo pipefail
echo "$RESULTS"
bad="$(printf '%s' "$RESULTS" | node -e '
let s = ""; process.stdin.on("data", (d) => (s += d)).on("end", () => {
const needs = JSON.parse(s);
const bad = Object.entries(needs).filter(([, v]) => v.result !== "success").map(([k, v]) => k + "=" + v.result);
process.stdout.write(bad.join(" "));
});')"
if [ -n "$bad" ]; then
echo "::error::legs not successful: $bad"
exit 1
fi
echo "all legs succeeded"

56 changes: 40 additions & 16 deletions .github/workflows/wsl.yml
Original file line number Diff line number Diff line change
@@ -1,22 +1,31 @@
name: WSL

# A real WSL2 kernel on a Windows runner: /proc/version carries "microsoft" and
# /proc/mounts reports drvfs for the checkout, the two signals wp07 keys on that
# ubuntu-latest cannot produce. At 13-14 minutes this is the longest lane in the
# repository, and it proves an installation property rather than a code-review
# property, so it runs on the integration lines and on demand, not on every
# pull request (cxc-dev-devops §2.1). A regression surfaces on the dev push that
# merged it and is fixed forward.
on:
push:
branches: [main, dev]
pull_request:
branches: [main, preview, dev]
workflow_dispatch:

permissions:
contents: read

jobs:
wsl:
# Two checkouts, deliberately, as two jobs so they run side by side. /mnt/c is
# drvfs and is where the lock and publish guarantees get weaker; ~ is native
# ext4 and is what the docs recommend. Both must pass, and the doctor must
# TELL THEM APART.
wsl-drvfs:
name: wsl (drvfs /mnt/c)
runs-on: windows-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v7
# A real WSL2 kernel on a Windows runner, so /proc/version carries
# "microsoft" and /proc/mounts reports drvfs for the checkout - the two
# signals wp07 keys on and that ubuntu-latest cannot produce.
- uses: Vampire/setup-wsl@v6
with:
distribution: Ubuntu-24.04
Expand All @@ -27,16 +36,37 @@ jobs:
curl -fsSL https://deb.nodesource.com/setup_24.x | bash -
apt-get install -y nodejs
node --version
# Two checkouts, deliberately. /mnt/c is drvfs and is where the lock and
# publish guarantees get weaker; ~ is native ext4 and is what the docs
# recommend. Both must pass, and the doctor must TELL THEM APART.
- name: Test on drvfs (/mnt/c)
shell: wsl-bash {0}
run: |
cd "$(wslpath '${{ github.workspace }}')"
npm ci
npm test
node bin/codexclaw.mjs doctor | grep -iE 'drvfs|9p|wsl'
- name: No wsl.exe subprocess parsing
shell: wsl-bash {0}
run: |
cd "$(wslpath '${{ github.workspace }}')"
! grep -rn "wsl\.exe\|wslpath" plugins/codexclaw/components plugins/codexclaw/scripts bin cli scripts \
--include="*.ts" --include="*.mjs" --include="*.js" \
| grep -v "/dist/" | grep -v "/test/" | grep -vE "^[^:]+:[0-9]+: *(//\|[*]\|/[*])" | grep -vE "^[^:]+:[0-9]+: *(//|\*|/\*)" || true

wsl-ext4:
name: wsl (native ext4 ~)
runs-on: windows-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v7
- uses: Vampire/setup-wsl@v6
with:
distribution: Ubuntu-24.04
additional-packages: curl ca-certificates
- name: Install Node in the distro
shell: wsl-bash {0}
run: |
curl -fsSL https://deb.nodesource.com/setup_24.x | bash -
apt-get install -y nodejs
node --version
- name: Test on native ext4 (~)
shell: wsl-bash {0}
run: |
Expand All @@ -45,10 +75,4 @@ jobs:
npm ci
npm test
node plugins/codexclaw/scripts/platform-smoke.mjs
- name: No wsl.exe subprocess parsing
shell: wsl-bash {0}
run: |
cd "$(wslpath '${{ github.workspace }}')"
! grep -rn "wsl\.exe\|wslpath" plugins/codexclaw/components plugins/codexclaw/scripts bin cli scripts \
--include="*.ts" --include="*.mjs" --include="*.js" \
| grep -v "/dist/" | grep -v "/test/" | grep -vE "^[^:]+:[0-9]+: *(//\|[*]\|/[*])" | grep -vE "^[^:]+:[0-9]+: *(//|\*|/\*)" || true

2 changes: 1 addition & 1 deletion README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

<p align="center">
<a href="https://github.com/lidge-jun/codexclaw/actions/workflows/ci.yml"><img src="https://github.com/lidge-jun/codexclaw/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
<img src="https://img.shields.io/badge/tests-2%2C941_passing-brightgreen" alt="2,941 tests passing">
<img src="https://img.shields.io/badge/tests-2%2C945_passing-brightgreen" alt="2,945 tests passing">
<img src="https://img.shields.io/badge/skills-29-blue" alt="29 skills">
<img src="https://img.shields.io/badge/hooks-28-blue" alt="28 hooks">
<a href="https://lidge-jun.github.io/codexclaw/"><img src="https://img.shields.io/badge/docs-codexclaw-black" alt="Documentation"></a>
Expand Down
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

<p align="center">
<a href="https://github.com/lidge-jun/codexclaw/actions/workflows/ci.yml"><img src="https://github.com/lidge-jun/codexclaw/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
<img src="https://img.shields.io/badge/tests-2%2C941_passing-brightgreen" alt="2,941 tests passing">
<img src="https://img.shields.io/badge/tests-2%2C945_passing-brightgreen" alt="2,945 tests passing">
<img src="https://img.shields.io/badge/skills-29-blue" alt="29 skills">
<img src="https://img.shields.io/badge/hooks-28-blue" alt="28 hooks">
<a href="https://lidge-jun.github.io/codexclaw/"><img src="https://img.shields.io/badge/docs-codexclaw-black" alt="Documentation"></a>
Expand Down Expand Up @@ -268,6 +268,17 @@ Methodology and research provenance: **[lidge-jun.github.io/pabcd_initiative](ht

Pull requests target the `dev` integration branch; `main` moves by maintainer promotion and carries releases.

CI on a pull request runs the checks a reviewer needs; the slow installation lanes run when a change lands on an integration line.

| Check | Pull request | Push to `dev` / `preview` / `main` |
|---|---|---|
| `ci` (aggregate of `test (ubuntu-latest, false)`, `test (macos-latest, false)`, four Windows shards) | yes | yes |
| `artifact (…)` / `install (…)` packed-install lifecycle | yes | yes |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Add preview to the packed-install trigger

On pushes to preview, this row promises that the packed-install artifact (…) and install (…) jobs run, but .github/workflows/packed-install.yml still limits its push trigger to [main, dev]. Consequently, preview commits receive no packed-install lifecycle coverage despite being documented as an integration-line push; either include preview in that workflow's trigger or correct the table.

Useful? React with 👍 / 👎.

| `enforce-target` | yes | — |
| `wsl (drvfs /mnt/c)`, `wsl (native ext4 ~)` | no (also `workflow_dispatch`) | yes |

`ci` fails when any leg fails, is cancelled or is skipped; it is the one check to require. The ubuntu lane runs the whole suite in one process and is where the tests badge total is measured; the Windows legs run `scripts/test.mjs --shard i/2`.

## License

[MIT](LICENSE). Copyright, upstream provenance and third-party scope: [NOTICE.md](NOTICE.md).
Expand Down
2 changes: 1 addition & 1 deletion README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

<p align="center">
<a href="https://github.com/lidge-jun/codexclaw/actions/workflows/ci.yml"><img src="https://github.com/lidge-jun/codexclaw/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
<img src="https://img.shields.io/badge/tests-2%2C941_passing-brightgreen" alt="2,941 tests passing">
<img src="https://img.shields.io/badge/tests-2%2C945_passing-brightgreen" alt="2,945 tests passing">
<img src="https://img.shields.io/badge/skills-29-blue" alt="29 skills">
<img src="https://img.shields.io/badge/hooks-28-blue" alt="28 hooks">
<a href="https://lidge-jun.github.io/codexclaw/"><img src="https://img.shields.io/badge/docs-codexclaw-black" alt="Documentation"></a>
Expand Down
42 changes: 42 additions & 0 deletions devlog/_plan/260910_ci_modernization/000_plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# CI 현대화: 샤딩, WSL 게이팅, 집계 체크 (HOTL 로드맵)

기준 HEAD: `origin/dev` = `a4396f28`. 세션: `01a08654-a5d0-71c0-a348-922a0b3ed7ff`. goalplan: `hotl-modernize-codexclaw-ci-per-cxc-dev-devops-1`.
요청: cxc-dev-devops 규칙대로 오래 걸리는 잡(WSL)은 dev·preview·main push에서만 돌리고, 오래 걸리는 스위트는 opencodex 사례처럼 샤딩. 현재 PR(#118)의 CI가 안정된 뒤 시작한다(확인: #118 헤드 b1e76d34 전 체크 SUCCESS).
이전 goal의 D 결론: 여덟 PR을 dev에 넣는 동안 매 PR마다 WSL 잡(13~14분)이 임계 경로였고, Windows 테스트 잡(9분)이 그다음이었다. 이 unit은 그 비용을 줄이는 쪽으로 방향을 잇는다.

## 독자 요약

PR 한 건의 CI 벽시계 시간은 WSL 잡이 정한다(817초). WSL은 wp07의 drvfs/ext4 신호를 검증하는 통합 라인용 증거이지 PR 리뷰용이 아니므로 push(main·preview·dev)와 dispatch에서만 돌린다. Windows 테스트(515~553초)는 2샤드로 나눠 절반으로 줄이고, 샤드는 `scripts/test.mjs --shard i/n`으로 파일 목록을 결정론적으로 나눈다. Ubuntu 전체 스위트 1개 레인은 그대로 두어 테스트 뱃지 총계를 잰다. 잡이 늘어나므로 `if: always()` 집계 잡 `ci` 하나가 모든 레그를 needs로 묶어 실패·취소·예상 밖 skip을 하나의 체크로 드러낸다(opencodex ci.yml의 `ci` 집계 잡과 같은 이유).

## 측정 기준선 (dev push, 2026-09-10)

| 워크플로 / 잡 | 시간 |
|---|---|
| ci.yml test (ubuntu, autocrlf=false) | 120s |
| ci.yml test (macos) | 186s |
| ci.yml test (windows, false) | 515s |
| ci.yml test (windows, true) | 553s |
| wsl.yml wsl | 817s |
| packed-install artifact ×3 / install ×2 | 12~41s |

## Loop spec

| 항목 | 계약 |
|---|---|
| Class | C3 (CI 워크플로 + 테스트 러너 스크립트; 제품 런타임 코드 없음, 릴리스 경로 영향 있음) |
| Verifier | actionlint(있으면), `test.mjs --shard` 단위 테스트, PR 최종 헤드에서 새 ci.yml 전 레그 SUCCESS + 집계 `ci` SUCCESS, 머지 후 dev push 런에서 WSL 두 잡 SUCCESS |
| Non-goals | self-hosted runner, paths-filter 기반 스킵(작은 레포라 불필요), release.yml 변경, preview 브랜치 생성 |
| Stop | wp4 D: 머지 + dev push 증거 |
| Escalation | 새 ci.yml이 PR에서 빨간불이고 원인이 워크플로 밖이면 NEEDS_HUMAN |

## 단계 지도

| Work phase | 문서 | 산출물 |
|---|---|---|
| wp1 | 이 파일, 010, 020 | 로드맵 잠금 |
| wp2 | [010_shard_and_aggregate.md](010_shard_and_aggregate.md) | test.mjs --shard + 테스트, ci.yml 샤딩·타임아웃·집계 잡 |
| wp3 | [020_wsl_gating.md](020_wsl_gating.md) | wsl.yml 트리거·분할·타임아웃, README CI 절 |
| wp4 | 020 §머지 | PR·CI·머지·dev push 증거 |

Privacy gate(DEV-PRIVACY-01): 이 unit은 CI 설정만 다루며 고객·개인 데이터가 없다.

Loading
Loading