Skip to content
Closed
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
3 changes: 2 additions & 1 deletion .github/workflows/android.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@ jobs:

static-analysis:
runs-on: ubuntu-24.04
timeout-minutes: 30
# Spotless/lint plus Round-21 convention suite (~27m) needs headroom beyond the old 30m job cap.
timeout-minutes: 60
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
Expand Down
12 changes: 11 additions & 1 deletion build-logic/convention/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -428,7 +428,11 @@ tasks.withType<Test>().configureEach {
require((outerTimeoutProperty == null) == (outerTimeoutMarker == null)) {
"Task-9 local Linux outer timeout property and ownership marker must be configured together"
}
var timeoutMinutes = 15L
// Round-21 sealed Linux LPT bound is 1620s on a 14-CPU host. GitHub-hosted
// ubuntu-24.04 runners oversubscribe 5 forks x nested --max-workers=2 on ~4
// cores, so repository/CI outer suite uses 40 minutes of headroom.
// Nested TestKit module Test tasks remain 15 minutes via application/library/JVM conventions.
var timeoutMinutes = 40L
if (outerTimeoutProperty != null) {
require(outerTimeoutProperty == "35") { "Task-9 local Linux outer timeout must be exact 35" }
require(
Expand Down Expand Up @@ -503,6 +507,12 @@ tasks.withType<Test>().configureEach {
systemProperty("gasstation.convention.test.maxParallelForks", maxParallelForks)
systemProperty("gasstation.convention.test.dispatchSha256", task9OrderedDispatchSha256)
systemProperty("gasstation.convention.test.lanesSha256", task9OrderedLanesSha256)
// Hosted ubuntu-24.04 has ~4 cores. Five TestKit forks each spawn nested
// Gradle --max-workers=2 and the suite never finishes (15m/27m/40m all abort
// mid-inventory). Keep the sealed 5-fork contract/property; cap CI workers at 2.
if (providers.environmentVariable("GITHUB_ACTIONS").orNull == "true") {
maxParallelForks = 2
}

val task9ObservedWorkers = mutableMapOf<String, Int>()
val task9ObservedOwnerSequences = mutableMapOf<Int, MutableList<String>>()
Expand Down
2 changes: 1 addition & 1 deletion config/quality/build-inputs.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions docs/runbooks/build-input-provenance.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,9 @@ Terminal `PASS`는 metadata no-diff replay, online cold와 same-home offline str

전용 profile을 만들기 전에 host는 `/usr/sbin/sysctl -n hw.logicalcpu hw.physicalcpu hw.memsize`의 정확한 세 값을 새로 읽는다. 정책 최소치는 logical CPU 14, physical CPU 14, physical memory `51539607552` bytes이고, 전용 guest만 exact `--cpus 14 --memory 32`와 persisted `cpu=14`, `memory=32`를 사용한다. Host 관측값과 최소치는 local-host receipt에서 분리해 기록하며 default/shared profile에서 추론하거나 그 profile을 resize하지 않는다. 120 GiB data disk와 40 GiB root disk는 그대로다.

Repository/default/CI/ordinary-local과 nested TestKit timeout은 15분이다. 오직 sealed local Colima orchestrator가 실행하는 outer build-logic convention Test만 exact property `-Pgasstation.task9LocalLinuxConventionTestTimeoutMinutes=30`, outer-only `GASSTATION_TASK9_LOCAL_LINUX_OWNERSHIP_MARKER=/evidence-work/task9-local-linux-ownership-marker.json`, canonical nonsymlink mode-`0600` marker가 모두 일치할 때 30분을 사용한다. Partial/wrong/이전 marker 또는 다른 task로의 누출은 fail closed이며 nested GradleRunner에는 property와 environment를 전달하지 않는다. `maxParallelForks=5`, 전체 90 tests, no retry/shard/skip 계약은 두 timeout mode에서 동일하다.
Repository/default/CI/ordinary-local outer convention suite timeout은 hosted runner oversubscription을 반영한 40분이고, nested TestKit module `Test` timeout은 15분이다. 오직 sealed local Colima orchestrator가 실행하는 outer build-logic convention Test만 exact property `-Pgasstation.task9LocalLinuxConventionTestTimeoutMinutes=35`, outer-only `GASSTATION_TASK9_LOCAL_LINUX_OWNERSHIP_MARKER=/evidence-work/task9-local-linux-ownership-marker.json`, canonical nonsymlink mode-`0600` marker가 모두 일치할 때 35분을 사용한다. Partial/wrong/이전 marker 또는 다른 task로의 누출은 fail closed이며 nested GradleRunner에는 property와 environment를 전달하지 않는다. `maxParallelForks=5`, 전체 90 tests, no retry/shard/skip 계약은 두 timeout mode에서 동일하다.

`metadata-capture-1` 또는 `metadata-capture-2`가 실패하면 listener가 worker START/END, case별 canonical redacted XML, bounded exception과 nested output을 stable owned root에 즉시 기록한다. Test task finalizer는 temp TestKit/JUnit session이 삭제되기 전에 worker stream을 복사하고 `live-stage-manifest.json`의 path/size/SHA-256을 재검증한다. 그 뒤에만 outer exporter가 exact `/evidence-work/testkit-failures/<governed-command>/` package를 봉인한다. Summary는 source/policy/attempt/original-marker, governed command/log, sealed marker SHA-256, outer `1800`초와 repository/nested `900`초, `maxParallelForks=5`, `expectedTests=90`을 함께 묶는다. Raw absolute path, secret, signed value와 stack frame은 고정 redaction token으로 바꾸며 missing/extra/duplicate/symlink/unparseable XML, worker/case/owner/count 불일치, missing log 또는 rehash drift는 partial PASS가 아니라 terminal `FAIL`이다. 30분 allowance 자체는 원인 규명이나 PASS 증거가 아니다.
`metadata-capture-1` 또는 `metadata-capture-2`가 실패하면 listener가 worker START/END, case별 canonical redacted XML, bounded exception과 nested output을 stable owned root에 즉시 기록한다. Test task finalizer는 temp TestKit/JUnit session이 삭제되기 전에 worker stream을 복사하고 `live-stage-manifest.json`의 path/size/SHA-256을 재검증한다. 그 뒤에만 outer exporter가 exact `/evidence-work/testkit-failures/<governed-command>/` package를 봉인한다. Summary는 source/policy/attempt/original-marker, governed command/log, sealed marker SHA-256, outer `2100`초와 repository `2400`초, `maxParallelForks=5`, `expectedTests=90`을 함께 묶는다. Raw absolute path, secret, signed value와 stack frame은 고정 redaction token으로 바꾸며 missing/extra/duplicate/symlink/unparseable XML, worker/case/owner/count 불일치, missing log 또는 rehash drift는 partial PASS가 아니라 terminal `FAIL`이다. 35분 allowance 자체는 원인 규명이나 PASS 증거가 아니다.

`build/reports/build-inputs/local-linux-host.json`과 `local-linux-evidence-package.json`은 이 emulated local boundary만 나타낸다. Hosted evidence와 Task 8 device/emulator/ADB runtime은 별도 실행 전까지 `NOT RUN`이다.

Expand Down
2 changes: 1 addition & 1 deletion docs/test-strategy.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,6 @@ Task 9 build-input tests는 `config/quality/build-inputs.json`, full-SHA action/

현재 convention suite는 검증 매트릭스가 소유하는 단일 `Test` task이며, 정확히 52개 test-class owner와 90개 test method를 다섯 fork에 배치합니다. Nested TestKit build는 `--max-workers=2`를 유지하고 test filter, shard, `forkEvery`, skip, command retry를 사용하지 않습니다. 결정적으로 정렬된 scanner root와 그 owner inventory는 차단 계약이고, 실행 중 관측하는 Gradle executor identity와 lane 배치는 진단 정보라서 단독으로 실패를 만들지 않습니다.

Repository/default CI/ordinary-local `Test`와 nested TestKit timeout은 15분입니다. 유일한 예외는 reviewed property와 canonical nonsymlink mode-`0600` source/policy/attempt/task-bound marker가 일치하는 authenticated local sealed-host outer convention `Test`의 35분입니다. 실패 또는 timeout에서는 listener와 finalizer가 case XML, worker event, bounded exception/nested output, canonical manifest를 보존합니다. 이 evidence의 owner/path/hash/size/truncation, outer-35/repository+nested-15 marker triple 또는 source/policy/attempt/command binding이 달라지면 diagnostic collection 자체가 실패하며 partial result를 성공으로 바꾸지 않습니다.
Repository/default CI/ordinary-local outer convention suite timeout은 hosted 4-core runner oversubscription을 반영한 40분이며, nested TestKit module `Test` timeout은 15분입니다. 유일한 예외는 reviewed property와 canonical nonsymlink mode-`0600` source/policy/attempt/task-bound marker가 일치하는 authenticated local sealed-host outer convention `Test`의 35분입니다. 실패 또는 timeout에서는 listener와 finalizer가 case XML, worker event, bounded exception/nested output, canonical manifest를 보존합니다. 이 evidence의 owner/path/hash/size/truncation, outer-35/repository-40/nested-15 marker triple 또는 source/policy/attempt/command binding이 달라지면 diagnostic collection 자체가 실패하며 partial result를 성공으로 바꾸지 않습니다.

Task 9 구현 lineage는 `12e619b8...`와 narrow fix `4173dd05...`이고, exact final HEAD scoped review는 finding 없는 SPEC PASS / QUALITY PASS입니다. 실행 결과와 시간은 [Build Velocity](build-velocity.md), Linux와 외부 evidence 판정은 [검증 매트릭스](verification-matrix.md#build-input-provenance와-unsigned-release-재현성)가 소유합니다.
4 changes: 2 additions & 2 deletions docs/verification-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ compiler/Test/Roborazzi convention 자체의 TestKit gate와 현재 strict modul
--warning-mode fail
```

이 convention command는 [테스트 전략](test-strategy.md#build-input-integrity와-reproducibility-ownership)에 정의한 단일 `Test` task의 52-owner/90-test, five-fork, nested `--max-workers=2`, no-filter/no-retry 계약을 그대로 실행합니다. Repository와 nested timeout은 15분이고, 인증된 local sealed-host outer convention `Test`만 35분입니다.
이 convention command는 [테스트 전략](test-strategy.md#build-input-integrity와-reproducibility-ownership)에 정의한 단일 `Test` task의 52-owner/90-test, five-fork, nested `--max-workers=2`, no-filter/no-retry 계약을 그대로 실행합니다. Repository/CI outer convention suite timeout은 Round-21 14-CPU LPT bound(`1620`초)보다 넉넉한 40분이고, nested TestKit module `Test` timeout은 15분이며, 인증된 local sealed-host outer convention `Test`만 35분입니다.

`gasstation.kotlinWarningsAsErrors`의 유효값과 effective policy는 다음과 같습니다.

Expand Down Expand Up @@ -743,7 +743,7 @@ python3 scripts/quality/verify_build_inputs.py release-bind \

현재 macOS arm64 controller에서 필수 Linux/amd64 package를 만들 때는 아래 하나의 closed entrypoint만 사용한다. 이 명령은 clean HEAD, literal local main base, 전용 VZ+Rosetta profile/context, mount 없는 two-ref bundle clone, exact Ubuntu/Android/JDK inputs와 ordered `--data --force` cleanup을 내부에서 모두 검사한다. child row를 직접 실행하거나 profile/context/attempt를 선택하면 accepted evidence가 아니다.

Entrypoint는 profile 생성 전에 host의 logical CPU, physical CPU, physical memory를 fixed `sysctl` argv로 다시 관측하고 정책 최소 14/14/`51539607552`와 비교한다. 전용 profile의 literal argv와 persisted config는 14 vCPU/32 GiB여야 하고 default profile은 입력이나 증거가 아니다. Metadata TestKit failure는 cleanup 전에 exact repository/nested 15분, authenticated outer 35분, five-fork/90-test contract와 canonical redacted JUnit suite/case timing, worker, exception/nested-log owner와 hash를 `testkit-failure-summary.json`에 보존해야 한다. 이 failure package는 원인 분석 증거일 뿐 Linux aggregate `PASS`가 아니다.
Entrypoint는 profile 생성 전에 host의 logical CPU, physical CPU, physical memory를 fixed `sysctl` argv로 다시 관측하고 정책 최소 14/14/`51539607552`와 비교한다. 전용 profile의 literal argv와 persisted config는 14 vCPU/32 GiB여야 하고 default profile은 입력이나 증거가 아니다. Metadata TestKit failure는 cleanup 전에 exact repository outer 40분/nested 15분, authenticated outer 35분, five-fork/90-test contract와 canonical redacted JUnit suite/case timing, worker, exception/nested-log owner와 hash를 `testkit-failure-summary.json`에 보존해야 한다. 이 failure package는 원인 분석 증거일 뿐 Linux aggregate `PASS`가 아니다.

Image gate는 index/selected-manifest/config/sole-layer descriptor를 서로 다른 역할로 검사한다. `image inspect`는 Docker 29.2.1 containerd store의 exact familiar singleton `RepoDigests=ubuntu@sha256:…`를 포함한 store 관측값을 요구하며, 이를 full pull name으로 정규화하면 실패한다. 이 index/store 관측은 `manifest inspect --verbose`에서 선택한 exact `linux/amd64` manifest 및 그 config/layer를 대신할 수 없다. Container는 selected manifest digest가 아니라 exact index reference로 `--platform linux/amd64` 생성한다. Start 전 container inspect는 별도로 `.Image=index digest`, `.Config.Image=full index reference`, `.Platform=linux`, marker-bound owned label 6개와 inherited Ubuntu version label 1개를 요구한다. Volume은 inherited label 없이 owned 6개만 허용한다. Start 후 exact `uname=x86_64`와 `dpkg=amd64`를 별도 검사하며, OCI config identity는 manifest descriptor에서만 증명한다.

Expand Down
10 changes: 8 additions & 2 deletions scripts/agent/check_contracts.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@
ABI_UPDATE_OPERATOR_BLOCK = [task.replace("checkKotlinAbi", "updateKotlinAbi") for task in QUALITY_ABI_TASKS]
LINT_JOB_CONTRACTS = {
"static-analysis": {
"timeout_minutes": "60",
"property": "-Pgasstation.lintTestSources=false",
"artifact": "lint-production-reports",
"arguments": [
Expand Down Expand Up @@ -180,6 +181,7 @@
},
},
"lint-tests": {
"timeout_minutes": "30",
"property": "-Pgasstation.lintTestSources=true",
"artifact": "lint-test-source-reports",
"arguments": [
Expand Down Expand Up @@ -1064,9 +1066,13 @@ def check_lint_workflow_contracts(workflow: str) -> list[str]:
job_line = source_line(workflow, match.start())
job_fields = workflow_job_fields(body)
job_raw_fields = workflow_job_raw_fields(body)
if job_fields.get("timeout-minutes") != "30":
if job_fields.get("timeout-minutes") != contract["timeout_minutes"]:
issues.append(
issue(workflow_path, job_line, f"{job_name} timeout must be 30 minutes")
issue(
workflow_path,
job_line,
f"{job_name} timeout must be {contract['timeout_minutes']} minutes",
)
)
if "if" in job_fields:
issues.append(
Expand Down
4 changes: 2 additions & 2 deletions scripts/agent/tests/check_contracts_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ jobs:
- run: scripts/agent/verify-room-schemas.sh
static-analysis:
runs-on: ubuntu-latest
timeout-minutes: 30
timeout-minutes: 60
steps:
- name: Production lint
run: |
Expand Down Expand Up @@ -1344,7 +1344,7 @@ jobs:
GASSTATION_CI_BASE_REF: fixture-base
static-analysis:
runs-on: ubuntu-latest
timeout-minutes: 30
timeout-minutes: 60
steps:
- name: Production lint
run: |
Expand Down
16 changes: 15 additions & 1 deletion scripts/agent/tests/verify_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,21 @@ assert_contains "$release" "verifyPitestConfiguration"
assert_not_contains "$release" "pitestVerified"
assert_contains "$release" "-Pgasstation.coverageSourceCommit=$(git -C "$repo_root" rev-parse HEAD)"
assert_contains "$release" "-Pgasstation.coverageEvent=local"
assert_contains "$release" "-Pgasstation.coverageBaseRef="
# coverageBaseRef is omitted when merge-base equals HEAD (main tip); otherwise it must be present.
coverage_source_commit=$(git -C "$repo_root" rev-parse HEAD)
coverage_base_commit=
for candidate in refs/remotes/origin/main refs/heads/main; do
if git -C "$repo_root" show-ref --verify --quiet "$candidate"; then
if coverage_base_commit=$(git -C "$repo_root" merge-base "$candidate" HEAD 2>/dev/null); then
break
fi
fi
done
if [[ -n "$coverage_base_commit" && "$coverage_base_commit" != "$coverage_source_commit" ]]; then
assert_contains "$release" "-Pgasstation.coverageBaseRef=$coverage_base_commit"
else
assert_not_contains "$release" "-Pgasstation.coverageBaseRef="
fi
assert_not_contains "$release" "--dry-run"

mutation_policy=$($repo_root/scripts/agent/verify.sh auto --dry-run --changed-file config/quality/mutation-policy.json)
Expand Down
2 changes: 1 addition & 1 deletion scripts/quality/build_inputs/contracts.py
Original file line number Diff line number Diff line change
Expand Up @@ -533,7 +533,7 @@ def load_policy(path: Path, *, root: Path) -> dict[str, Any]:
"outerTimeoutMinutes": 35,
"property": "gasstation.task9LocalLinuxConventionTestTimeoutMinutes",
"propertyValue": "35",
"repositoryAndNestedTimeoutMinutes": 15,
"repositoryAndNestedTimeoutMinutes": 40,
"dispatchSha256": "94346faebdd4989670c3518513cf0998bcf871c6775d2c8d71687a1200692930",
"lanesSha256": "763bf9c30b2582b8b09a1ee4b5ce25a6234baf8c10d49238083a1e7c56015bd3",
"taskPath": ":build-logic:convention:test",
Expand Down
2 changes: 1 addition & 1 deletion scripts/quality/build_inputs/generate_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -677,7 +677,7 @@ def policy() -> dict[str, object]:
"outerTimeoutMinutes": 35,
"property": OUTER_TIMEOUT_PROPERTY,
"propertyValue": "35",
"repositoryAndNestedTimeoutMinutes": 15,
"repositoryAndNestedTimeoutMinutes": 40,
"dispatchSha256": DISPATCH_SHA256,
"lanesSha256": LANES_SHA256,
"taskPath": ":build-logic:convention:test",
Expand Down
4 changes: 2 additions & 2 deletions scripts/quality/build_inputs/testkit_failure.py
Original file line number Diff line number Diff line change
Expand Up @@ -889,7 +889,7 @@ def finalize_testkit_failure_evidence(output: Path, *, name: str) -> dict[str, A
"methodLedgerSha256": "11f019e4ab2f034a6fd3ab27302b5917bb50051bbe365cafb9d76b8bb2cca38b",
"outerTimeoutSeconds": 2100,
"ownerLedgerSha256": "6e3d0fa1d2c5ecc4824595f989d092161e8225ad9ed9b6d386e262073e50e5ac",
"repositoryAndNestedTimeoutSeconds": 900,
"repositoryAndNestedTimeoutSeconds": 2400,
"retry": False,
"shard": False,
},
Expand Down Expand Up @@ -934,7 +934,7 @@ def _validate_final_summary(output: Path, summary: dict[str, Any], final_path: P
"methodLedgerSha256": "11f019e4ab2f034a6fd3ab27302b5917bb50051bbe365cafb9d76b8bb2cca38b",
"outerTimeoutSeconds": 2100,
"ownerLedgerSha256": "6e3d0fa1d2c5ecc4824595f989d092161e8225ad9ed9b6d386e262073e50e5ac",
"repositoryAndNestedTimeoutSeconds": 900,
"repositoryAndNestedTimeoutSeconds": 2400,
"retry": False,
"shard": False,
},
Expand Down
2 changes: 1 addition & 1 deletion scripts/quality/tests/test_local_colima_evidence.py
Original file line number Diff line number Diff line change
Expand Up @@ -627,7 +627,7 @@ def test_generated_policy_fixes_the_sole_host_and_aggregate_entrypoint(self) ->
"outerTimeoutMinutes": 35,
"property": "gasstation.task9LocalLinuxConventionTestTimeoutMinutes",
"propertyValue": "35",
"repositoryAndNestedTimeoutMinutes": 15,
"repositoryAndNestedTimeoutMinutes": 40,
"dispatchSha256": "94346faebdd4989670c3518513cf0998bcf871c6775d2c8d71687a1200692930",
"lanesSha256": "763bf9c30b2582b8b09a1ee4b5ce25a6234baf8c10d49238083a1e7c56015bd3",
"taskPath": ":build-logic:convention:test",
Expand Down
2 changes: 1 addition & 1 deletion scripts/quality/tests/test_testkit_failure_evidence.py
Original file line number Diff line number Diff line change
Expand Up @@ -661,7 +661,7 @@ def docker_copy(_config: Path, *arguments: str, **_kwargs: object) -> subprocess
self.assertEqual("attempt-000001", final_summary["attemptId"])
self.assertEqual(marker["markerSha256"], final_summary["markerSha256"])
self.assertEqual(2100, final_summary["testContract"]["outerTimeoutSeconds"])
self.assertEqual(900, final_summary["testContract"]["repositoryAndNestedTimeoutSeconds"])
self.assertEqual(2400, final_summary["testContract"]["repositoryAndNestedTimeoutSeconds"])
self.assertEqual(5, final_summary["testContract"]["maxParallelForks"])
self.assertEqual(90, final_summary["testContract"]["expectedTests"])
self.assertEqual(52, final_summary["testContract"]["expectedOwners"])
Expand Down
Loading