382 lines
26 KiB
Markdown
382 lines
26 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.
|
|
|
|
Contract loading validates every registered command entry against the
|
|
authoritative root package-script graph and lifecycle/install policy before the
|
|
runner enters its execution callback. Foreign cwd/workspace script dispatch,
|
|
argument-sensitive dispatchers, and ineffective lifecycle suppression therefore
|
|
fail preflight even when the later `check:ci` command would never run.
|
|
For npm, that root-only boundary also parses options following an explicit
|
|
`run`/`run-script` target or implicit `test`/`start`/`stop`/`restart` target:
|
|
`--workspace`, `-w`, `--workspaces`, and `--prefix` are foreign manifest scope
|
|
and are rejected before execution. The first literal `--` ends npm option
|
|
parsing, so later tokens are ordinary script arguments. pnpm and Yarn differ:
|
|
their options following the selected script name are forwarded to that script;
|
|
their cwd/workspace selectors are rejected only where the manager consumes
|
|
them before script selection.
|
|
|
|
The npm graph includes existing root-manifest `pre<script>` and `post<script>`
|
|
hooks around every explicit `run`/`run-script` dependency and implicit
|
|
`start`/`stop`/`restart`/`test` dependency. Hooks are omitted only when ordered
|
|
npm options establish an unambiguous effective `--ignore-scripts` value before
|
|
the first literal `--`; bare and explicitly true forms omit hooks, while false,
|
|
negative, contradictory, malformed, and post-delimiter forms keep traversal or
|
|
fail closed. Case-insensitive `npm_config_workspace`, `npm_config_workspaces`,
|
|
`npm_config_prefix`, `npm_config_userconfig`, and `npm_config_globalconfig`
|
|
assignments are rejected whenever the tokenized command invokes npm. Direct
|
|
assignments, static paths whose basename is `env`, exact `command`/`exec`
|
|
prefix chains, prior exports, and `set -a` assignments share one prefix grammar.
|
|
Only modeled non-scope `env -i`, `env -u`, `env --unset`, and `env --` forms are
|
|
allowed; cwd-changing or unknown options fail closed. Dynamic assignment names,
|
|
unmodeled environment mutation, npm `--userconfig`/`--globalconfig`, and
|
|
unquoted pathname expansion before the npm argument delimiter are rejected.
|
|
The gate runner checks the same inherited environment names before loading the
|
|
contract or entering the execution callback, so they cannot reach a child gate
|
|
process.
|
|
|
|
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 자체를 변경 없이
|
|
그대로 승격한다. Local evidence 관계 검증에 필요한 policy와 verifier source도
|
|
archive member로 고정되며, checkout 밖의 격리된 cwd에서도 archive path와 기대
|
|
digest만으로 candidate-internal check를 재계산하고 archived secret-scan
|
|
policy/rule/SARIF/zero-finding/digest 관계를 검증한다. Archive에 없는 checkout
|
|
source를 다시 scan했다고 주장하지 않는다.
|
|
|
|
Provider baseline은 Gitea 1.26.4 이상과 Gitea Runner 1.0.0 이상이다. Linux
|
|
runner에는 실행 가능한 `/usr/bin/bwrap`, `/usr/bin/prlimit`,
|
|
`/usr/bin/systemd-run`, `/usr/bin/systemctl`, `bwrap --size` 지원, active user
|
|
bus, systemd user manager 254 이상, unified cgroup v2와 delegated
|
|
memory/pids/CPU controller가 모두 필요하다. 실행 파일, user manager/version,
|
|
trust/archive/path 같은 host-side preflight 실패는 raw report 생성 전에 차단된다.
|
|
`bwrap --size` 수용 여부와 실제 delegated controller/limit 값은 owned raw inode를
|
|
만든 뒤 scope 안에서만 확정할 수 있으며, 이 단계의 실패는 해당 inode를
|
|
identity-bound cleanup하고 fail closed한다.
|
|
|
|
각 provider는 고유한 collected user scope에서 실행된다. Supervisor는 실행 전에
|
|
실제 cgroup membership과 `memory.max=1073741824`, `memory.swap.max=0`,
|
|
`pids.max=64`, `cpu.max="100000 100000"`을 확인한다. 내부 process에는 core 0,
|
|
file-size 8,388,607 bytes, open FD 64, CPU 1,200초 상한도 적용된다.
|
|
같은 UID 전체에 합산되는 `RLIMIT_NPROC`로 provider별 32개를 보장한다고 주장하지
|
|
않으며 aggregate PID authority는 cgroup `TasksMax=64`다.
|
|
Bubblewrap는 network namespace를 분리해 완전 offline으로 실행하고 workspace,
|
|
verified candidate, `.git` mask, `/tmp`, `/etc`, `/proc`, `/dev`를 read-only로
|
|
유지한다. Archive 검증·추출과 sandbox/trust preflight가 끝난 뒤 supervisor가
|
|
생성하고 inode를 고정한 정확한 raw report 파일 하나만 read-write bind된다.
|
|
주변 `untrusted` directory 전체는 writable이 아니다. 실행 전 또는 provider
|
|
실패 시, 그리고 성공적으로 sealed evidence를 게시한 뒤에도 supervisor가 소유한
|
|
inode만 atomic quarantine을 거쳐 제거하므로 빈 stale report 없이 재시도할 수 있다.
|
|
|
|
Provider command와 provider-prefixed environment는 bounded length-prefixed bwrap vector로
|
|
`systemd-run` stdin에 전달되어 supervisor/systemd/bwrap wrapper argv나 unit
|
|
metadata에 노출되지 않는다. 단, 최종 provider executable의 일반 argv는 같은
|
|
UID의 process inspection에 보일 수 있으므로 command 문자열과 인자에 token,
|
|
password, private-key material을 넣으면 안 된다. Credential은 반드시 해당 종류의
|
|
`VULNERABILITY_PROVIDER_*` 또는 `PROVENANCE_PROVIDER_*` environment로만 전달하고
|
|
`*_COMMAND`에는 넣지 않는다. 정상 provider 종료까지 같은 stdin을 parent-liveness
|
|
pipe로 열어 두며 supervisor hard death의 EOF를 받은 in-scope wrapper는 provider
|
|
process group 전체를 종료하고 dev/inode가 일치하는 raw report만 정리한다.
|
|
|
|
별도의 trusted guardian child는 provider scope 밖에서 filesystem transaction 전체를
|
|
소유한다. Client는 spawn 전에 canonical raw/evidence directory를
|
|
`O_DIRECTORY|O_NOFOLLOW`로 열고 identity를 확인한 뒤 provider kind와 nonce에서
|
|
canonical raw/final 및 nonce-private raw-staging/sealed-temp exact leaf를 확정한다.
|
|
Client가 두 private file을 `O_RDWR|O_CREAT|O_EXCL|O_NOFOLLOW` mode `0600`으로
|
|
미리 할당하고 dev/inode를 기록한다. Directory descriptor는 guardian fd 3/4,
|
|
private file descriptor는 fd 5/6으로 상속되며 guardian argv에는 Node와 helper
|
|
경로만 있다. 최초 canonical v2 frame에는 provider kind, absolute deadline,
|
|
32-byte nonce만 전달한다.
|
|
|
|
Guardian bootstrap은 procfs link를 읽기 전에 fd 5/6을 fstat한다. Procfs pathname은
|
|
canonical directory의 exact direct-child grammar를 만족하고 descriptor-relative
|
|
lstat이 이미 확보한 fd identity/type/mode/size/link count와 일치할 때만 해당
|
|
identity의 cleanup alias가 된다. Guard frame 검증 뒤 raw staging을 fixed raw leaf에
|
|
no-overwrite hard link하고 두 alias의 link count 2를 확인한 다음 private alias를
|
|
제거하고 raw directory를 sync한다. Canonical raw가 같은 identity와 link count 1로
|
|
남은 뒤에만 READY를 응답한다. READY 전 종료 시 client는 pre-spawn raw identity로
|
|
raw staging/canonical을, sealed identity로 temp/final을 각각 확인해 일치하는 alias만
|
|
정리한다. 현재 canonical pathname을 새 ownership identity로 승격하지 않으므로 외부
|
|
file과 concurrent same-kind winner를 보존하면서 같은 workspace를 즉시 재시도할 수
|
|
있다. Supervisor는 READY identity와 canonical target도 정확히 확인한다.
|
|
|
|
검증된 JSON bytes는 pinned temp inode에만 기록하고 `0400` 적용과 file sync를 마친다.
|
|
Guardian은 authenticated publish의 size/hash/identity를 재검증하고 같은 directory에서
|
|
no-replace `link(temp, final)`, temp unlink, directory sync를 수행한 뒤 PUBLISHED를
|
|
응답한다. `GITHUB_OUTPUT` append 이후 commit은 raw inode를 제거하고
|
|
`commitPending`으로만 전이하며, 깨끗한 control EOF까지 확인해야 PASS가 된다. 그 전의
|
|
EOF, deadline, 잘못된 frame/nonce, commit 뒤 추가 byte 또는 guardian 조기 종료는
|
|
identity가 일치하는 raw/temp/final을 모두 정리하고 fail closed한다. Provider wall
|
|
timeout 30분에 post-processing 10분을 더한 guardian lease 상한은 40분이다.
|
|
|
|
Client가 private allocation 뒤 guardian spawn 전에 hard stop되면 empty mode-`0600`
|
|
nonce-private leaf만 남을 수 있다. 기록되지 않은 pathname은 ownership 근거가 아니므로
|
|
자동 sweeping하지 않는다. 이 private leaf는 fixed raw/final name을 점유하지 않아
|
|
same-kind retry를 막지 않는다.
|
|
|
|
`GITHUB_OUTPUT`은 runner가 소유한 regular file이라는 실행기 계약을 전제로 한다.
|
|
Sealing/output I/O의 OS-level cancellation을 보장하지 않는다. Provider scope가 이미
|
|
수집된 뒤 guardian이 종료되면 scope-active latch는 늦은 kill을 시작하지 않고 lifecycle
|
|
error만 기록한다. 이후 publish/commit의 awaited failure가 identity가 고정된
|
|
raw/temp/final fallback을 모두 정리하고 fail closed한다.
|
|
|
|
Provider stdout/stderr는 credential을 포함할 수 있는 untrusted bytes이므로 CI log로
|
|
재전송하거나 보관하지 않고, byte 수만 합산해 1 MiB 상한을 적용한다. Guardian의
|
|
stderr/control fd가 닫혀 진단 출력이 `EPIPE`/`EBADF`가 되어도 cleanup 뒤 nonzero
|
|
종료는 생략되지 않는다. Provider wall-clock 상한은 30분이다. Wall
|
|
timeout, aggregate output 초과, parent-liveness loss, 실행 중 guardian loss는 provider process group을
|
|
명시적으로 SIGKILL한다. 일반 command 실패와 FD/CPU RLIMIT 종료는 실제 exit/signal로
|
|
systemd completion을 거치며, 모든 경로에서 wrapper 종료와 systemd unit/cgroup
|
|
collection을 확인한다. Adapter는 사전에 배치된 offline data와 supervisor candidate binding만
|
|
읽어 정확한 report inode에 기록해야 한다. Scope/cgroup limit drift, residual unit,
|
|
workspace·host home/toolcache·sealed evidence 접근 의존성은 모두 blocking failure다.
|
|
|
|
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
|
|
|
|
두 provider role은 서로 다른 key ID뿐 아니라 canonical DER-SPKI public-key bytes도
|
|
사용해야 한다. 동일 key를 서로 다른 ID로 재등록한 구성도 finalizer가 거절한다.
|
|
|
|
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.
|
|
|
|
Each provider command must atomically emit strict provider evidence v2 from
|
|
the supervisor bindings for evidence type, issued/expires timestamps, run
|
|
ID/attempt, `PROVIDER_INVOCATION_NONCE`, source identity, candidate digests,
|
|
key ID, and DER-SPKI key fingerprint. Promotion receives the two supervisor
|
|
job outputs as `VULNERABILITY_INVOCATION_NONCE` and
|
|
`PROVENANCE_INVOCATION_NONCE`; do not replace them with constants or values
|
|
parsed from provider reports.
|
|
|
|
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을 대신하지 않는다.
|
|
Promotion job에는 job-level `if`가 없다. 기본 `needs` 성공 의미론으로 immutable
|
|
build, vulnerability provider, provenance provider 세 job이 모두 성공해야 하며,
|
|
`always()`나 `cancelled()`로 cancellation을 덮어쓰지 않는다. Bare `always()`는
|
|
step cleanup에만 사용된다. 다만 cancellation 시 cleanup 실행 여부는 workflow
|
|
정적 타입이나 단위 테스트로 증명하지 않았으며 runner/native smoke에서 확인해야
|
|
하는 신뢰 경계다.
|
|
Finalizer output은 `RUNNER_TEMP` 아래 random private directory이며 exact-five
|
|
upload는 `${{ steps.finalize.outputs.staging_root }}` 아래 다음 다섯 canonical
|
|
pathname만 사용한다: `release-candidate.tar.gz`, `vulnerability-report.json`,
|
|
`provenance-attestation.json`, `provider-verification.json`,
|
|
`promotion-verification.json`. 바로 다음
|
|
`always()` cleanup은 staging path/token, runner-temp device/inode와 staging-leaf
|
|
device/inode 여섯 output을 모두 요구한다. cleanup은 pin한 leaf descriptor에서
|
|
exact-five name만 unlink하고 non-recursive `rmdir`만 사용하므로 교체된 directory나
|
|
canary tree를 recursive 삭제하지 않는다. stable `.release/promoted-staging`
|
|
directory를 만들거나 재사용하지 않는다. exact five는 captured archive 한 개,
|
|
captured report 두 개와 process 안에서 생성한 provider/promotion verification v3
|
|
두 개이며 promotion
|
|
record는 provider record, local assessment, report hashes와 run/source/candidate/
|
|
nonces/key identities/trust-policy hash 및 signed `secretScanAttestation`을 함께
|
|
bind한다. 이 attestation은 PASS와 local-assessment/source-set/policy/SARIF/
|
|
scan-input digest를 포함한다. Supervisor/finalizer는 captured archive에서 기대
|
|
tuple을 유도해 exact equality를 확인하지만, 실제로 같은 source-set 전체를
|
|
독립 스캔하고 forged empty SARIF에 서명하지 않을 책임은 trusted vulnerability
|
|
provider에 있다. Staging은 restrictive
|
|
umask와 무관하게 directory `0700`, file `0400`을 강제하고, 모든 write 뒤 live
|
|
time으로 exact-five signature/freshness를 다시 확인한 뒤에만 output을 공개한다.
|
|
Descriptor-relative 정리는 ancestor/leaf 교체와 symlink를 fail-closed로 처리하지만
|
|
upload action의 same-UID pathname reopen 또는 atomic
|
|
`renameat2` handoff를 보장하지 않는다. staging Gitea smoke/native adapter 확인
|
|
전에는 그 경계를 닫았다고 보고하지 않는다. 실제 smoke는 exact-five
|
|
upload-download와 success, validation failure, upload failure, cancellation 각각의
|
|
cleanup을 관찰해야 한다. 현재 repository에는 native uploader나 `renameat2`
|
|
보장이 없다.
|
|
또한 portable Node의 `mkdir`와 최초 pathname `lstat`는 atomic하지 않다. 구현은
|
|
mkdir 직후 metadata를 저장하고 이후 `O_DIRECTORY|O_NOFOLLOW` descriptor의
|
|
device/inode와 비교한 뒤에만 permission을 바꾸지만, 최초 lstat보다 앞서 성공한
|
|
malicious same-UID 교체는 native/privilege 경계로 남는다. 따라서 `RUNNER_TEMP`
|
|
private `0700` ancestor와 exclusive single-tenant runner가 필수다.
|
|
실패 cleanup도 created device/inode와 opened descriptor가 일치한 뒤에만 활성화된다.
|
|
불일치 descriptor는 close만 수행하며 현재 visible replacement pathname은 unlink나
|
|
`rmdir`하지 않는다. 공격자가 original directory를 다른 이름이나 parent 밖으로
|
|
이동한 경우 portable Node parent scan으로 안전하게 회수할 수 없으므로, 공격자를
|
|
배제한 trusted runner/native cleanup 또는 격리된 test fixture가 잔여 directory를
|
|
후처리해야 한다.
|
|
|
|
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.
|