214 lines
13 KiB
Markdown
214 lines
13 KiB
Markdown
# CI quality-gate orchestration
|
|
|
|
`config/ci/gates.json` is the executable registry for all 26 gates and the sole
|
|
workflow model. `.gitea/workflows/quality-gates.yml` is generated from that
|
|
strict v2 contract; it is not an independent configuration authority. Update
|
|
the registry, run `corepack pnpm generate:ci-workflow`, and commit the generated
|
|
bytes together. `corepack pnpm check:ci-workflow` rejects missing, stale, CRLF,
|
|
or otherwise byte-different output. `corepack pnpm check:ci` runs artifact-schema
|
|
checking, the typed contract checker, and this workflow drift check. The Gitea
|
|
adapter runs each gate as an independent matrix check with full fan-out and no
|
|
soft-fail wiring.
|
|
|
|
The dependency graph is:
|
|
|
|
```text
|
|
MERGE_READY
|
|
-> RELEASE_READY
|
|
-> PROD_PROMOTION_READY
|
|
-> FIELD_SLO_READY
|
|
|
|
DOCUMENTATION_READY (off-chain)
|
|
```
|
|
|
|
`check:architecture`는 dependency-cruiser 결과와 별도로 Babel parser/Node
|
|
resolver 기반 `staticImportGraph`를 `artifacts/quality/dependency-report.json`에
|
|
기록한다. TS/TSX의 static, dynamic, type, CommonJS 및 JSDoc import를 검사하고,
|
|
로컬 source import에는 실제 TypeScript 확장자를 요구한다. 실행 source나 로컬
|
|
specifier에 `.js/.jsx/.mjs/.cjs`가 있거나 unresolved dependency, parse failure,
|
|
error-severity layer violation, cycle 또는 지원하지 않는 architecture rule
|
|
shape가 하나라도 있으면 gate는 실패한다. resolver, unresolved import, layer
|
|
edge와 cycle regression fixture도 같은 명령에서 실행된다.
|
|
|
|
Gitea workflow는 run ID/attempt, checkout SHA와 ref에서 build/release
|
|
식별자를 만들고 최소 `contents: read` 권한만 요청한다. Gate runner는 `HEAD`의
|
|
full commit ID와 commit timestamp를 한 번 읽어 `SOURCE_DATE_EPOCH`를 유도한
|
|
뒤 workflow의 SHA와 실제 checkout이 일치하는지 확인한다. `CI=true`에서 이
|
|
식별자 중 하나라도 없거나 불일치하면 build step 전에 fail-closed하며, build와
|
|
release manifest도 같은 build ID, commit SHA, release ID와 timestamp를 가져야
|
|
한다.
|
|
|
|
Release tier의 `immutable_build`는 `dist`의 hidden `.vite` entries, raw
|
|
`pnpm-lock.yaml`, build/module inventory와 local release/security evidence를 한
|
|
번 생성해 run ID/attempt로 구분된 archive로 게시한다. Raw lock bytes의 SHA-256은
|
|
dependency inventory와 candidate manifest 양쪽의 `lockfileSha256`과 같아야 한다.
|
|
Producer는 게시 전에 manifest가 선언한 exact member set과 각 byte digest를
|
|
검증하고 archive SHA-256을 job output으로 고정한다. Vulnerability/provenance
|
|
provider job은 같은 archive를 각각 내려받아 output SHA-256과 exact member set을
|
|
확인한 뒤 checkout과 분리된 root에만 추출하고, `CANDIDATE_LOCKFILE_PATH`와
|
|
`CANDIDATE_DIST_SHA256`를 외부 command에 전달한다. Provider command 실행 후에는
|
|
archive와 추출된 candidate를 다시 검증하고, strict provider JSON과 candidate
|
|
digest가 일치할 때만 report를 업로드한다. Promotion은 같은 archive와 두 report를
|
|
별도 경로로 내려받고 SHA/member 검증을 마친 뒤 격리된 root에 추출하여 local
|
|
evidence를 read-only로 다시 계산하고 Ed25519 signature/digest를 확인한다.
|
|
Promotion job에는 build/rebuild command가 없으며 검증한 archive 자체를 변경 없이
|
|
그대로 승격한다.
|
|
|
|
Provider baseline은 Gitea 1.26.4 이상과 Gitea Runner 1.0.0 이상이다. 이
|
|
workflow의 provider job은 Linux runner에서 실행 권한이 있는
|
|
`/usr/bin/bwrap`를 필수로 요구하며, trusted `process.execPath`를 sandbox 안의
|
|
`/tmp/node`에 read-only bind한다. Provider command는 bubblewrap 안에서
|
|
`/bin/sh -eu -c`로 비대화식 실행되고 30분 안에 종료되어야 한다. Sandbox는
|
|
workspace를 read-only로 bind하고 `.git`을 가리며, 별도의 `untrusted`
|
|
raw-evidence 하위 디렉터리만 writable로 노출한다. 따라서 command는 전달된
|
|
candidate/environment 값을 읽고 지정된 raw report 하나만 기록해야 하며,
|
|
workspace 수정, host home/toolcache 접근, sealed evidence 직접 기록에 의존하면
|
|
안 된다. Supervisor는 provider 종류에 해당하는 credential prefix와 제한된
|
|
환경만 전달하고, sandbox 또는 출력 경계를 만들 수 없으면 fail closed한다.
|
|
|
|
Workflow가 실행하는 action은 `scripts/contracts/ci-gates.ts`의 단일 typed,
|
|
runtime-frozen registry에서만 resolve된다. `uses:`에는 repository 별칭, tag,
|
|
branch 또는 short SHA를 허용하지 않고 다음 absolute upstream URL과 full 40-hex
|
|
commit만 사용한다.
|
|
|
|
- `https://github.com/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5`
|
|
(`v4.3.1`)
|
|
- `https://github.com/actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020`
|
|
(`v4.4.0`)
|
|
- `https://github.com/ChristopherHX/gitea-upload-artifact@81f940d004763f986ba3582c007fd842dd5cb0d7`
|
|
(patched `v4` branch)
|
|
- `https://github.com/ChristopherHX/gitea-download-artifact@75635f32b4c1c41c4b3d64e8f85210112ed4c9c7`
|
|
(patched `v4` branch)
|
|
|
|
Gitea 1.22 공식 Actions 문서는 artifact v4 호환 경로로 이
|
|
`ChristopherHX` patched upload/download fork를 권고한다. 이 권고는 실제
|
|
설치 환경의 호환성을 증명하지 않으므로, 지원 baseline은 계속 Gitea 1.26.4+
|
|
및 Gitea Runner 1.0.0+이며 action 변경은 registry, semantic tests, snapshot과
|
|
generated workflow를 함께 갱신해야 한다.
|
|
|
|
이 workflow를 required check로 전환하기 전에 staging instance에서
|
|
`permissions`, `gitea.run_attempt`, 위 digest-pinned patched artifact action을
|
|
포함한 한 번의 실제 end-to-end provider smoke를 반드시 통과시켜야 한다.
|
|
mock/local generation 결과는 required-check enablement evidence가 아니다. 모든 setup step은
|
|
`node-version-file: .nvmrc`를 사용하므로 CI Node.js 버전은 `.nvmrc`의 exact
|
|
pin과 같다. CI contract는 `.nvmrc`가 full semantic version인지, 모든 job이 이
|
|
파일을 사용하는지 함께 검사한다. `ubuntu-latest` runner label은
|
|
관리자가 임의 환경에 매핑할 수 있으므로 provenance로 사용하지 않는다. 대신
|
|
그 label을 immutable container image에 매핑하고 동일한 image digest를
|
|
repository variable `RUNNER_IMAGE_DIGEST`에 설정한다. Workflow는 이 값을
|
|
runtime environment의 `CI_RUNNER_IMAGE`로 전달하며, 값이 비어 있으면 모든
|
|
gate가 실행 전에 실패한다. Gitea repository variable 이름에는 `CI` prefix를
|
|
사용할 수 없으므로 두 이름을 의도적으로 구분한다.
|
|
|
|
Pull requests and `develop` pushes evaluate merge readiness. Version tags
|
|
evaluate merge then release readiness. Production and field evaluation require
|
|
an explicit workflow dispatch. The field tier cannot pass until the 28-day
|
|
sample threshold decision is recorded. Documentation readiness consumes the
|
|
canonical project-note evidence in which both scoped diagrams already received
|
|
100/100 `PASS_SCOPED`; the repo ledger preserves the evidence scope and
|
|
canonical digests.
|
|
|
|
All jobs upload the shared `artifacts/` tree even after failure. Numeric
|
|
retention remains an organization/provider decision; the workflow intentionally
|
|
does not invent `retention-days`. The relative minimums are recorded in the
|
|
registry: merge evidence through the PR decision, coherent release evidence
|
|
through the next release promotion, drill evidence through the next production
|
|
promotion, and field evidence through aggregation.
|
|
|
|
Runbook jobs write to the stable path
|
|
`artifacts/runbooks/<runbook-id>/record.json`; the dynamic release identity is
|
|
stored inside the record. This keeps gate evidence lookup independent from
|
|
slashes or other provider-specific characters in `RELEASE_ID`, while each
|
|
workflow artifact remains scoped to its own run.
|
|
|
|
Browser-backed merge gates install and execute the pinned Chromium, Firefox,
|
|
and WebKit engines. This makes route behavior, reflow, native dialog semantics,
|
|
theme persistence, and automated accessibility a cross-engine contract rather
|
|
than a Chromium-only smoke check.
|
|
|
|
`FE-GATE-005`의 unit suite에는 client cache/storage의 deterministic contract가
|
|
포함된다. `storage-registry.test.ts`는 key별 codec, schema/TTL, 16,384-byte
|
|
상한, quota/security/corrupt cleanup과 memory fallback을,
|
|
`cross-tab-invalidation.test.ts`는 2,048-byte exact wire, topic/release epoch,
|
|
self/duplicate/out-of-order/gap, BroadcastChannel → localStorage → local-only
|
|
degrade와 cleanup을 검증한다. `tanstack-cache-coordinator.test.ts`는 topic을
|
|
local namespace로만 resolve하고 query key/data를 wire에 보내지 않으며 mutation
|
|
중 remote hint를 coalesce하는지 검증한다. 결과는 기존
|
|
`artifacts/tests/unit.xml`과 coverage evidence에 포함된다.
|
|
|
|
`FE-GATE-006`과 runtime adapter unit은
|
|
`QueryInvalidationProvider`/coordinator가 실제 production provider tree와
|
|
bootstrap에 존재하고 browser capability getter가 실패해도 boot가
|
|
`DEGRADED_LOCAL_ONLY`로 계속되는지 확인한다.
|
|
|
|
`FE-GATE-008`의 `test:browser-capabilities` suite는 File/Blob, native file
|
|
input, IndexedDB, OPFS, Cache Storage와 StorageManager reference runtime을 Vite
|
|
dev origin의 실제 browser API에 연결한다. API가 없는 engine에서는 skip하지 않고
|
|
adapter의 명시적인 `UNSUPPORTED` fallback을 검증한다. 생성한 database, OPFS
|
|
namespace와 owned cache는 각 test가 자신이 만든 opaque namespace만 정리한다.
|
|
|
|
`FE-GATE-010`은 application/domain/presentation에서 raw browser storage와
|
|
picker global 접근을 막고, 선택되지 않은 reference runtime을 bootstrap이나
|
|
installed feature가 import하는 것도 거절한다. catalog의
|
|
`referenceRuntime.sourceRoots`와 conformance script가 실제로 존재해야 하며,
|
|
`productionComposition`은 project capability 결정 전까지 `false`다. 같은
|
|
gate의 realtime source/fixture 검사는 native SSE/WebSocket/Web Push API가
|
|
소유 adapter 밖으로 새는 것, presentation timer owner, 미선택 runtime의
|
|
bootstrap/installed-feature 조합을 차단한다. optional recipe source gate는
|
|
uncomposed runtime 전체를 tree-shaking 없이 합성해 catalog의 gzip 예산도
|
|
blocking으로 검증한다.
|
|
`FE-GATE-020`은 reference runtime source·전용 test·catalog metadata를 제거한
|
|
임시 repository에서도 base typecheck, architecture, test, build가 통과하는지
|
|
검증해 skeleton의 선택성을 유지한다. browser file/storage와 realtime runtime은
|
|
각각 독립 removal fixture와 JUnit evidence를 가지며, realtime fixture는 공통
|
|
event authority, SSE, WebSocket, bounded Polling, Web Push source와 공개 export,
|
|
전용 boundary script를 제거한 뒤 base gate를 다시 실행한다.
|
|
|
|
같은 `FE-GATE-010`의 `FE-REG-QUERY` governance는 installed query마다
|
|
namespace, serialization/identity, invalidation topic, version,
|
|
`crossContext: "invalidate-only"`와 `persistence: "disabled"`를 요구한다.
|
|
따라서 cross-tab invalidation은 조립됐지만 query persistence와 기존 IndexedDB
|
|
reference runtime은 계속 미조립이다.
|
|
|
|
현재 `FE-GATE-008` browser-capability suite에는 실제 두 page의
|
|
BroadcastChannel/localStorage fallback scenario가 아직 없다. unit fake 통과를
|
|
native multi-tab promotion evidence로 간주하지 않는다. 이 gap은
|
|
[Client cache and browser storage platform](../architecture/client-cache-and-storage.md)의
|
|
완료 기준에 미완료로 남아 있다. 현재 장애 분류와 안전한 local-only 복구 절차는
|
|
[Client cache and Web Storage recovery](./client-cache-and-storage-recovery.md)를
|
|
따른다.
|
|
|
|
Repository variables required by higher tiers:
|
|
|
|
- `RUNNER_IMAGE_DIGEST` for the immutable job-container image digest used by the
|
|
`ubuntu-latest` runner label; the workflow exposes it to gates as
|
|
`CI_RUNNER_IMAGE` (required by every tier)
|
|
- `HOSTING_BASE_URL` for live header verification
|
|
- `FIELD_WEB_VITALS_INPUT` for the privacy-approved field sample document
|
|
- `MIN_ELIGIBLE_SAMPLES` after the baseline decision
|
|
- `VULNERABILITY_PROVIDER_COMMAND` and `PROVENANCE_PROVIDER_COMMAND` for the
|
|
independently operated scanner and attestor commands
|
|
- `VULNERABILITY_PUBLIC_KEY_PATH`, `VULNERABILITY_KEY_ID`,
|
|
`PROVENANCE_PUBLIC_KEY_PATH`, and `PROVENANCE_KEY_ID` for separately managed
|
|
trusted Ed25519 verification material
|
|
|
|
If any external provider command, report, trust path, or key ID is absent,
|
|
promotion remains unavailable with `FAIL_UNVERIFIED`; there is no local
|
|
generator/restore fallback.
|
|
|
|
Promotion verification/staging step과 promoted-release upload action 사이에는
|
|
어떤 step도 둘 수 없고 upload에는 `if: always()`를 사용할 수 없다. 이 인접성은
|
|
실패한 검증의 publication을 막지만 staging path를 upload action에
|
|
cryptographically hand off하지는 않는다. 따라서 promotion runner는 trusted,
|
|
exclusive, single-tenant여야 하고 staging step 종료부터 바로 다음 upload action이
|
|
파일을 여는 시점까지 같은 UID로 실행되는 provider command나 다른 process가
|
|
살아남지 않는다는 운영 가정을 만족해야 한다. 업로드된 artifact의 downstream
|
|
consumer도 artifact service나 transfer action을 신뢰 경계 밖으로 보고, candidate
|
|
manifest와 signed provider evidence에 바인딩된 digest를 다운로드 후 다시
|
|
검증해야 한다. 현재 producer-side adjacency 자체는 consumer-side digest
|
|
revalidation을 대신하지 않는다.
|
|
|
|
Branch protection must mark each `FE-GATE-* / <name>` check required for its
|
|
declared tier. This repository cannot configure server-side protection by
|
|
committing a file.
|