diff --git a/.github/workflows/hourly-commercial-readiness.yml b/.github/workflows/hourly-commercial-readiness.yml index fd86cf78a..294debefc 100644 --- a/.github/workflows/hourly-commercial-readiness.yml +++ b/.github/workflows/hourly-commercial-readiness.yml @@ -13,15 +13,110 @@ concurrency: group: noema-hourly-commercial-readiness cancel-in-progress: true -# The workflow token only checks out trusted default-branch code. All PR writes -# use the dedicated maintainer App token so merge events can trigger downstream -# push workflows instead of being suppressed by GITHUB_TOKEN recursion rules. +# The workflow token only reads trusted default-branch state. All PR writes use +# the dedicated Maintainer App token so merge events can trigger downstream push +# workflows instead of being suppressed by GITHUB_TOKEN recursion rules. permissions: contents: read jobs: + activation_preflight: + name: scheduler-activation-preflight + runs-on: ubuntu-latest + timeout-minutes: 5 + outputs: + write_ready: ${{ steps.activation.outputs.write_ready }} + terminal_classification: ${{ steps.activation.outputs.terminal_classification }} + env: + MAINTENANCE_ENABLED: ${{ vars.NOEMA_MAINTENANCE_ENABLED == 'true' }} + MAINTAINER_APP_CLIENT_ID_CONFIGURED: ${{ vars.NOEMA_MAINTAINER_APP_CLIENT_ID != '' }} + MAINTAINER_APP_PRIVATE_KEY_CONFIGURED: ${{ secrets.NOEMA_MAINTAINER_APP_PRIVATE_KEY != '' }} + REVIEWER_LOGIN_CONFIGURED: ${{ vars.NOEMA_REVIEWER_LOGIN != '' }} + REPOSITORY_FULL_NAME: ${{ github.repository }} + WORKFLOW_SOURCE_SHA: ${{ github.sha }} + EVENT_NAME: ${{ github.event_name }} + WORKFLOW_RUN_ID: ${{ github.run_id }} + WORKFLOW_RUN_ATTEMPT: ${{ github.run_attempt }} + steps: + - name: classify activation feasibility without repository writes + id: activation + shell: bash + run: | + set -euo pipefail + write_ready=false + terminal_classification=EXTERNAL_GATE_REMAINS + reason_code=activation_prerequisite_unavailable + + if [ "$REPOSITORY_FULL_NAME" != "ContextualWisdomLab/noema" ]; then + terminal_classification=SAFETY_OR_POLICY_BLOCKER + reason_code=unexpected_repository + elif ! [[ "$WORKFLOW_SOURCE_SHA" =~ ^[0-9a-f]{40}$ ]]; then + terminal_classification=SAFETY_OR_POLICY_BLOCKER + reason_code=invalid_workflow_source_sha + elif [ "$MAINTENANCE_ENABLED" != "true" ] \ + || [ "$MAINTAINER_APP_CLIENT_ID_CONFIGURED" != "true" ] \ + || [ "$MAINTAINER_APP_PRIVATE_KEY_CONFIGURED" != "true" ] \ + || [ "$REVIEWER_LOGIN_CONFIGURED" != "true" ]; then + terminal_classification=EXTERNAL_GATE_REMAINS + reason_code=activation_prerequisite_unavailable + else + write_ready=true + terminal_classification=NO_ACTION_NEEDED + reason_code=write_lane_ready + fi + + generated_at="$(date -u +'%Y-%m-%dT%H:%M:%SZ')" + evidence_path="artifacts/operations/hourly-scheduler-activation.json" + mkdir -p "$(dirname "$evidence_path")" + jq -n \ + --argjson schema_version 1 \ + --arg repository_full_name "$REPOSITORY_FULL_NAME" \ + --arg workflow_source_sha "$WORKFLOW_SOURCE_SHA" \ + --arg event_name "$EVENT_NAME" \ + --arg workflow_run_id "$WORKFLOW_RUN_ID" \ + --arg workflow_run_attempt "$WORKFLOW_RUN_ATTEMPT" \ + --arg generated_at "$generated_at" \ + --arg terminal_classification "$terminal_classification" \ + --arg reason_code "$reason_code" \ + --argjson write_ready "$write_ready" \ + '{ + schema_version: $schema_version, + repository_full_name: $repository_full_name, + workflow_source_sha: $workflow_source_sha, + event_name: $event_name, + workflow_run_id: $workflow_run_id, + workflow_run_attempt: $workflow_run_attempt, + generated_at: $generated_at, + terminal_classification: $terminal_classification, + reason_code: $reason_code, + write_ready: $write_ready + }' >"$evidence_path" + chmod 0600 "$evidence_path" + + { + echo "write_ready=$write_ready" + echo "terminal_classification=$terminal_classification" + } >>"$GITHUB_OUTPUT" + { + echo "## Hourly scheduler activation" + echo + echo "- Classification: \`$terminal_classification\`" + echo "- Reason: \`$reason_code\`" + echo "- Credential-bearing write lane: \`$write_ready\`" + } >>"$GITHUB_STEP_SUMMARY" + + - name: upload scheduler activation evidence + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: scheduler-activation-evidence + path: artifacts/operations/hourly-scheduler-activation.json + if-no-files-found: error + retention-days: 90 + maintain: - if: vars.NOEMA_MAINTENANCE_ENABLED == 'true' + needs: activation_preflight + if: needs.activation_preflight.outputs.write_ready == 'true' name: commercial-readiness-maintenance runs-on: ubuntu-latest timeout-minutes: 45 diff --git a/CHANGELOG.md b/CHANGELOG.md index 94a79797d..d3f8163d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog ## Unreleased +- 매시간 commercial-readiness scheduler에 항상 실행되는 read-only activation preflight를 추가해 whole-run skipped 상태를 없앤다. Preflight는 activation prerequisite를 내부에서만 평가하고, public-repository artifact에는 exact workflow source SHA·run identity·`write_ready`·configuration-opaque terminal classification/reason만 90일 보존한다. Credential-bearing Maintainer App write lane은 모든 activation prerequisite가 확인될 때만 열리며, 개별 variable/secret 존재 여부·기본 `GITHUB_TOKEN` 권한·reviewer key contract·branch protection·merge/release authority는 노출하거나 변경하지 않는다. - 개발 의존성 체인의 transitive `nanoid` lockfile resolution을 `3.3.16`에서 `3.3.17`로 최소 갱신하여 GHSA-2v37-7h3g-55p8 / CVE-2026-67213 보안 게이트를 복구한다. PostCSS의 선언 범위 `^3.3.16`과 다른 package metadata는 변경하지 않으며 audit waiver·ignore·severity 완화 없이 `npm ci`/`npm audit --audit-level=high`가 exact head에서 재검증되도록 유지한다. - `hourly-product-development`가 `NVIDIA_NIM_API_KEY`뿐 아니라 `NOEMA_MAINTAINER_APP_CLIENT_ID`와 `NOEMA_MAINTAINER_APP_PRIVATE_KEY` 존재를 checkout·OpenCode 설치·NVIDIA 호출 전에 검증한다. 게시 경로가 준비되지 않았으면 `maintainer_app_unavailable`로 실패 폐쇄하여 알려진 실패에 추론 비용을 쓰지 않으며, `dry_run`은 credential 없이 queue와 task contract를 검토하는 경로로 유지한다. 기존 reviewer App 및 `NOEMA_LLM_API_KEY`·`contextual-orchestrator` reviewer credential 경계는 변경하지 않는다. - zero open pull requests일 때만 `NVIDIA_NIM_API_KEY` 전용 OpenCode 1.17.13 세션을 실행하는 proposal-only `hourly-product-development` 루프를 추가. minute-47 schedule·non-cancelling single flight·OpenCode binary SHA-256 pin·NVIDIA NIM model fallback·후보 실패 시 clean reset·GitHub/OIDC credential 제거·reviewer key 비참조·full release verification·40-file/500,000-byte proposal budget·trusted one-PR packaging을 강제한다. 각 후보 실행은 900초와 30초 kill grace로 제한하고, 실패 후 `npm ci --ignore-scripts` 재설치는 별도 60초와 10초 kill grace로 제한한다. 재설치가 실패하거나 시간 초과되면 불완전한 dependency tree로 다음 후보를 실행하지 않고 실패 폐쇄한다. 세 후보의 실행·종료 2,790초, 두 번의 후보 간 재설치 140초, 300초 setup/diagnostic reserve를 합친 3,230초가 55분(3,300초) job budget에 들어가며 70초 여유를 남긴다. 마지막 후보가 실패하면 불필요한 reset·clean·재설치를 생략하고 안정적인 전체 후보 실패 진단으로 곧바로 종료한다. 모델 실행, 제안 코드 검증, publication credential을 각각 별도의 GitHub-hosted runner로 분리하고, immutable artifact의 exact ID·workflow-run ID·archive digest와 patch SHA-256·base SHA·file/byte count를 교차 검증하며 symlink(`120000`)와 gitlink(`160000`)를 세 경계 모두에서 차단한다. 제안 코드를 실행한 runner에는 Maintainer App secret/token을 절대 제공하지 않고, 세 번째 non-executing publisher에서만 late-bound repository-scoped App token을 발급한다. merge/release/deploy authority는 기존 `hourly-commercial-readiness` exact-head governance에 유지하며, 운영 Runbook과 OpenCode/NVIDIA/GitHub Actions/NIST SP 800-218 근거를 APA 7th doctoring에 기록했다. package version은 release·deployment·production KPI evidence를 발행하지 않으므로 유지한다. diff --git a/docs/doctoring/hourly-scheduler-activation-feasibility.md b/docs/doctoring/hourly-scheduler-activation-feasibility.md new file mode 100644 index 000000000..f6ac16363 --- /dev/null +++ b/docs/doctoring/hourly-scheduler-activation-feasibility.md @@ -0,0 +1,147 @@ +# Hourly Scheduler Activation RCA and Feasibility + +## Status + +- Decision state: Proposed implementation under exact-head review +- Repository: `ContextualWisdomLab/noema` +- Initially observed protected base: `8273b350b633eae245f5cab8da4cb1d43799c3a2` +- First integrated protected base: `db4f444c1b1849ec615364a233469870c23407e6` +- Observation date: 2026-08-12 UTC +- Canonical operational contract: `docs/hourly-commercial-readiness-loop.md` +- External scheduler evidence owner: issue #96 + +This doctoring record distinguishes observed GitHub evidence, source-supported platform behavior, project decisions, and inference. It does not claim that an external ChatGPT task was modified, that the Maintainer App is provisioned, or that a pull request is merge-authorized. + +## Observed failure + +Scheduled workflow run `31587463951` completed with one job named `commercial-readiness-maintenance`. The job conclusion was `skipped`, its step list was empty, and no runner was assigned. At the observed default-branch source, the sole job had the condition: + +```yaml +if: vars.NOEMA_MAINTENANCE_ENABLED == 'true' +``` + +The connector available to this review could not read the repository variable value because the variables endpoint returned `403 Resource not accessible by integration`. Therefore the evidence supports only this bounded conclusion: the job-level expression evaluated to a non-running state. It does not identify whether the variable was absent, false, inaccessible to the integration, or intentionally disabled. + +## Root-cause analysis + +### Immediate cause + +The activation condition was attached to the only job. When the condition did not permit execution, GitHub had no step in which Noema could retain a reason code, inspect the remaining configuration prerequisites, or upload bounded activation evidence. + +### Systemic cause + +The design combined two different decisions: + +1. whether the scheduler should produce read-only operational evidence; and +2. whether the credential-bearing Maintainer App write lane may run. + +The first decision is safe and useful on every schedule. The second must remain fail closed. Binding both to one job-level condition converted an expected external gate into an opaque whole-run skip. + +### Secondary confidentiality finding + +The first implementation retained one boolean per activation prerequisite and a specific missing-configuration reason. GitHub documents that artifact metadata for public resources can be requested without authentication, and repository readers can retrieve workflow artifacts. Because `ContextualWisdomLab/noema` is public, individual App-private-key, App-client-ID, reviewer-login, or maintenance-variable presence is not suitable for artifact or step-summary retention even when no secret value is printed. + +The source configuration names are already visible in the workflow, but whether each credential exists is additional operational information. The public evidence contract was therefore narrowed to repository/run/source identity, `write_ready`, and a configuration-opaque terminal classification/reason. + +### What is not established + +The observation does not prove a GitHub Actions outage, a malformed secret, an invalid App installation, a reviewer identity mismatch, or a hidden scheduler-provider error code. Those hypotheses require separate access-controlled evidence and must not be invented from the skipped result. + +## Remedies considered + +| Remedy | Feasibility decision | Reason | +| --- | --- | --- | +| Re-run the unchanged workflow | Rejected | It recreates the same job-level decision and adds no diagnostic boundary. | +| Set `NOEMA_MAINTENANCE_ENABLED=true` immediately | Rejected | It could open a credential-bearing lane before App, reviewer, and governance prerequisites are evidenced. | +| Remove the activation gate | Rejected | It weakens the fail-closed authorization boundary and can turn missing credentials into recurring failures. | +| Add another hourly workflow | Rejected | It creates a duplicate writer/schedule, increases queue pressure, and divides operational authority. | +| Grant the default `GITHUB_TOKEN` write access | Rejected | It expands authority and changes downstream workflow-trigger behavior instead of repairing diagnosis. | +| Publish one boolean or reason per missing credential | Rejected after security review | Public-repository artifacts can expose configuration-presence metadata. | +| Add an always-running read-only activation preflight, then gate the existing write job on its configuration-opaque output | Selected | It preserves one schedule and least privilege while proving whether the write lane was evaluated without publishing which credential or variable is absent. | + +## Selected design + +The workflow is split into two jobs. + +### `activation_preflight` + +The preflight has workflow-level `contents: read` authority only and does not checkout repository code, mint an App token, call repository write APIs, use OIDC, or expose secret values. It evaluates internally: + +- exact repository identity; +- canonical 40-character workflow source SHA shape; +- explicit maintenance activation; +- presence of Maintainer App client ID, private key, and reviewer login. + +It emits `write_ready` and `terminal_classification` as job outputs. The public artifact contains no individual variable/secret presence boolean and no reason that identifies the missing prerequisite. When any ordinary activation prerequisite is unavailable, the public result is the fixed pair: + +```text +terminal_classification=EXTERNAL_GATE_REMAINS +reason_code=activation_prerequisite_unavailable +``` + +Repository/source identity violations remain separately classified because they do not disclose credential state. + +### `maintain` + +The existing maintenance job declares `needs: activation_preflight` and runs only when: + +```yaml +if: needs.activation_preflight.outputs.write_ready == 'true' +``` + +The preflight output is not merge authority. The maintenance job must still mint the repository-scoped Maintainer App token, run live `main` governance audit, collect full exact-head evidence, and satisfy all existing review and merge gates. + +## Classification semantics + +| Classification | Meaning | +| --- | --- | +| `EXTERNAL_GATE_REMAINS` | At least one activation prerequisite is unavailable; public evidence does not identify which one. | +| `SAFETY_OR_POLICY_BLOCKER` | Repository or workflow-source identity is invalid. | +| `NO_ACTION_NEEDED` | The write lane may evaluate its existing governance controls. | + +`NO_ACTION_NEEDED` deliberately does not mean that any pull request is green, approved, protected, mergeable, releasable, deployed, or acquisition ready. + +## Evidence minimization + +The activation artifact excludes: + +- secret values and private keys; +- existence booleans for individual variables or secrets; +- reason codes naming a missing client ID, private key, or reviewer login; +- GitHub tokens or OIDC material; +- reviewer credential values; +- vulnerability details; +- hidden model reasoning; +- pull-request approval or release claims. + +The public artifact retains only what is necessary to prove that the read-only preflight ran, which exact trusted workflow source it evaluated, whether the credential-bearing lane opened, and whether a non-secret safety identity check failed. Exact configuration diagnosis remains in repository administrator controls and access-controlled Maintainer App readiness evidence. + +## Test-first evidence + +The first regression-only head `4dffd09ddac273a8f8756c0db5f6c9289cd9b861` added `test/hourly-commercial-readiness-activation.test.ts` before the workflow implementation. A focused local contract execution against the fetched predecessor workflow failed at `missing activation_preflight`, which is the intended RED condition. Exact-head GitHub application CI for that predecessor remained queued during the initial implementation window and is not treated as completed RED evidence. + +After the public-artifact access review, regression-only head `7bfecff9cc39705fae1e144496ed23298452384f` added a second failing contract: no `AUTH_OR_TOOLING_BLOCKER`, no credential-specific reason, and no per-prerequisite boolean may be serialized into the public workflow evidence. The predecessor workflow contained all prohibited strings, so the contract was RED before implementation commit `3eef1a41a70a35c8ef27a931e1f5ca1aace87b76` replaced them with a generic closed-lane reason and removed individual booleans from the JSON artifact. + +The integrated head must still pass the repository-owned application CI, reviewer CI, central Security Scan, coverage gates, current review, and branch-protection requirements. Source-level RED demonstrations do not substitute for those acceptance gates. + +## Standards and primary documentation rationale + +GitHub job outputs are designed to be consumed through the downstream `needs` context. This supports a narrow preflight-to-write-lane decision without sharing a credential. GitHub also documents that dependent jobs normally do not run when a prerequisite fails or is skipped, which is why the preflight itself must complete successfully for classified external gates. + +Workflow artifacts are the platform mechanism for retaining run-generated evidence after a job completes, but GitHub's artifact REST documentation states that public resources may be queried without authentication. This makes a public artifact an unsuitable place for individual credential-presence metadata. GitHub's secrets documentation also recommends minimum credential permissions and explicitly warns against intentional secret exposure. These source-supported platform behaviors inform the configuration-opaque evidence decision; they do not independently prove Noema's configuration is correct. + +GitHub recommends granting `GITHUB_TOKEN` only the minimum permissions required and using a GitHub App installation token when different permissions are needed. The workflow therefore retains top-level `contents: read` and mints the repository-scoped Maintainer App token only after the preflight opens the write lane. + +## References — APA 7th + +GitHub, Inc. (n.d.). *GITHUB_TOKEN*. GitHub Docs. Retrieved August 12, 2026, from https://docs.github.com/en/actions/concepts/security/github_token + +GitHub, Inc. (n.d.). *REST API endpoints for GitHub Actions artifacts*. GitHub Docs. Retrieved August 12, 2026, from https://docs.github.com/en/rest/actions/artifacts + +GitHub, Inc. (n.d.). *Secrets*. GitHub Docs. Retrieved August 12, 2026, from https://docs.github.com/en/actions/concepts/security/secrets + +GitHub, Inc. (n.d.). *Use GITHUB_TOKEN for authentication in workflows*. GitHub Docs. Retrieved August 12, 2026, from https://docs.github.com/en/actions/tutorials/authenticate-with-github_token + +GitHub, Inc. (n.d.). *Using jobs in a workflow*. GitHub Docs. Retrieved August 12, 2026, from https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/use-jobs + +GitHub, Inc. (n.d.). *Workflow syntax for GitHub Actions*. GitHub Docs. Retrieved August 12, 2026, from https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax diff --git a/docs/hourly-commercial-readiness-loop.md b/docs/hourly-commercial-readiness-loop.md index 8970f9fa6..b0301aa54 100644 --- a/docs/hourly-commercial-readiness-loop.md +++ b/docs/hourly-commercial-readiness-loop.md @@ -26,7 +26,11 @@ Repository variable과 secret을 다음 이름으로 등록합니다. - `NOEMA_REVIEWER_LOGIN`: 신뢰할 Noema reviewer GitHub App의 정확한 bot login(예: `noema-reviewer[bot]` 형식) - `NOEMA_MAINTENANCE_ENABLED`: App 설치·권한·secret·reviewer login 검증이 끝난 뒤에만 문자열 `true`로 설정 -`NOEMA_MAINTENANCE_ENABLED`가 정확히 `true`가 아니면 hourly job은 skipped 상태로 유지됩니다. 자격 증명이 없는 상태에서 매시간 실패하거나 기본 `GITHUB_TOKEN`으로 강등되지 않도록 하는 명시적 activation gate입니다. 활성화 후 `NOEMA_REVIEWER_LOGIN`이 비어 있거나 `[bot]` 형식이 아니면 스크립트는 쓰기 전에 실패합니다. +매시간 `scheduler-activation-preflight`는 쓰기 권한이나 App token 없이 먼저 실행됩니다. 이 preflight는 필수 설정의 존재 여부를 job 내부에서만 평가하고, 공개 저장소에서 다운로드 가능한 `scheduler-activation-evidence`에는 개별 설정 이름·존재 여부·누락 항목을 남기지 않습니다. Artifact에는 workflow source SHA, 실행 identity, `write_ready`, 그리고 구성 상태를 역추론할 수 없는 고정 terminal classification/reason만 남깁니다. + +`NOEMA_MAINTENANCE_ENABLED`가 정확히 `true`가 아니거나 필수 App/reviewer 설정이 없으면 credential-bearing `maintain` job만 skipped 상태로 유지됩니다. 따라서 scheduler 전체가 무설명 `skipped`로 사라지지 않으면서도 기본 `GITHUB_TOKEN` fallback이나 권한 확대 없이 실패 폐쇄됩니다. 공개 artifact의 `activation_prerequisite_unavailable`은 어느 설정이 없거나 비활성인지 밝히지 않습니다. 정확한 구성 진단은 repository 관리자 제어면과 별도의 access-controlled App readiness evidence에서 수행해야 합니다. + +활성화 후 `NOEMA_REVIEWER_LOGIN`이 비어 있거나 `[bot]` 형식이 아니면 기존 governance script가 repository write 전에 다시 실패합니다. Activation preflight는 write-lane 진입 가능성만 분류하며 App 설치 범위, 실제 permission, reviewer identity 또는 branch protection을 승인하지 않습니다. App은 `ContextualWisdomLab/noema`에만 설치하고 다음 repository permission만 부여합니다. @@ -39,6 +43,29 @@ App은 `ContextualWisdomLab/noema`에만 설치하고 다음 repository permissi 이 App은 Noema review verdict를 작성하는 reviewer App과 분리합니다. reviewer App의 contents permission을 write로 확대하지 않습니다. +## 활성화 RCA와 현실성 분류 + +과거 구조는 sole maintenance job에 `if: vars.NOEMA_MAINTENANCE_ENABLED == 'true'`를 직접 배치했습니다. 조건이 false이거나 integration에서 값을 평가할 수 없으면 job에 step이 하나도 생성되지 않아, 운영자는 의도적인 비활성화와 구성 누락을 구분할 수 없었습니다. 동일 workflow를 재실행하는 것은 원인 증거를 추가하지 않으므로 현실적인 복구가 아닙니다. + +현재 구조는 다음 순서를 강제합니다. + +```text +read-only activation preflight +→ bounded configuration-opaque evidence +→ feasibility classification +→ only when write_ready=true: Maintainer App token mint +→ live main governance audit +→ exact-head PR loop +``` + +| terminal classification | 의미 | write lane | +| --- | --- | --- | +| `EXTERNAL_GATE_REMAINS` | 하나 이상의 activation prerequisite가 충족되지 않았으나 공개 evidence로 개별 상태를 노출하지 않음 | 닫힘 | +| `SAFETY_OR_POLICY_BLOCKER` | repository identity나 workflow source SHA가 비정상 | 닫힘 | +| `NO_ACTION_NEEDED` | activation prerequisite가 모두 존재해 기존 governance lane을 평가할 수 있음 | 열림 | + +`NO_ACTION_NEEDED`는 병합 가능, review 승인, security 통과, release 또는 acquisition readiness를 뜻하지 않습니다. 오직 credential-bearing governance lane을 시작할 수 있다는 activation 판단입니다. 실제 병합 권한은 이후 exact-head Checks, review, ruleset 및 SHA-bound merge 검증에 남습니다. + ## 결정 상태 | 상태 | 의미 | 자동 쓰기 | @@ -80,13 +107,15 @@ Check Runs API는 `filter=all`과 전체 pagination으로 수집합니다. 재 ## 권한 경계 -워크플로 자체 `GITHUB_TOKEN`은 trusted default branch checkout을 위한 `contents: read`만 갖습니다. PR 조회·dispatch·merge는 `actions/create-github-app-token`이 발급한 짧은 수명의 Maintainer App token으로 수행하며, action 입력에서 `actions: read`, `checks: read`, `contents: write`, `metadata: read`, `pull-requests: write`, `statuses: read`를 명시합니다. +Workflow-level `GITHUB_TOKEN`은 `contents: read`만 갖습니다. Activation preflight는 checkout, App token mint, repository API write, OIDC 또는 secret 출력 없이 `write_ready` 결정을 계산합니다. 개별 variable/secret 존재 여부는 downstream output, artifact, step summary 또는 로그에 직렬화하지 않습니다. PR 조회·dispatch·merge는 preflight가 `write_ready=true`를 증명한 뒤 `actions/create-github-app-token`이 발급한 짧은 수명의 Maintainer App token으로 수행하며, action 입력에서 `actions: read`, `checks: read`, `contents: write`, `metadata: read`, `pull-requests: write`, `statuses: read`를 명시합니다. 워크플로와 Maintainer App은 `issues: write`, `id-token: write`, secret write, administration 권한을 갖지 않습니다. App token 발급 실패나 permission 부족은 `operational_error`로 실패-폐쇄 처리합니다. ## 감사 산출물 -모든 실행은 `commercial-readiness-loop-report` artifact에 `artifacts/commercial-readiness/hourly-loop-report.json`을 90일 보존합니다. 주요 필드는 다음과 같습니다. +모든 schedule/dispatch 실행은 `scheduler-activation-evidence` artifact에 `artifacts/operations/hourly-scheduler-activation.json`을 90일 보존합니다. GitHub의 public-repository artifact API는 public resource 조회를 허용하므로 이 JSON은 공개 가능 자료로 취급합니다. Repository, exact workflow source SHA, run identity, `write_ready`, configuration-opaque reason code와 terminal classification만 포함하며, variable/secret 이름별 존재 여부, secret, private key, token, reviewer credential 또는 vulnerability detail은 포함하지 않습니다. + +Credential-bearing loop가 실행되면 `commercial-readiness-loop-report` artifact에 `artifacts/commercial-readiness/hourly-loop-report.json`도 90일 보존합니다. 주요 필드는 다음과 같습니다. ```json { @@ -124,10 +153,15 @@ PR 처리 후 남은 열린 PR이 0개이면 기존 `readiness:audit`, `acquisit ## 운영 점검 -1. `commercial-readiness-loop-report`에서 각 PR의 reason code를 확인합니다. -2. `required_check_missing`이 있으면 workflow trigger, `app.slug=github-actions`, ruleset context 이름을 점검합니다. -3. `review_in_progress`가 장시간 유지되면 `central-review.yml` run과 contextual-orchestrator 상태를 점검합니다. -4. `merge_state_not_clean`이면 충돌·behind 상태·repository policy를 해소합니다. -5. Maintainer App token mint가 실패하면 App 설치 대상과 정확한 permissions를 확인합니다. `GITHUB_TOKEN` fallback을 추가하지 않습니다. -6. Noema 승인 marker가 존재하는데 `noema_current_head_approval_missing`이 남으면 `NOEMA_REVIEWER_LOGIN`이 실제 App bot login과 정확히 일치하는지 확인합니다. -7. `operational_error`이면 artifact의 bounded detail과 GitHub Actions 로그를 확인하고, 권한을 넓히기 전에 실제 API 실패 원인을 수정합니다. +1. 모든 실행에서 먼저 `scheduler-activation-evidence`의 terminal classification과 reason code를 확인합니다. +2. `EXTERNAL_GATE_REMAINS`이면 public artifact만으로 누락된 prerequisite를 추정하지 않습니다. Repository 관리자 제어면과 access-controlled `maintainer-app-readiness` 증거에서 maintenance activation, App configuration, reviewer identity를 각각 확인합니다. +3. Configuration 복구 시 `GITHUB_TOKEN` fallback이나 permission 확대를 추가하지 않습니다. +4. `commercial-readiness-loop-report`에서 각 PR의 reason code를 확인합니다. +5. `required_check_missing`이 있으면 workflow trigger, `app.slug=github-actions`, ruleset context 이름을 점검합니다. +6. `review_in_progress`가 장시간 유지되면 `central-review.yml` run과 contextual-orchestrator 상태를 점검합니다. +7. `merge_state_not_clean`이면 충돌·behind 상태·repository policy를 해소합니다. +8. Maintainer App token mint가 실패하면 App 설치 대상과 정확한 permissions를 확인합니다. `GITHUB_TOKEN` fallback을 추가하지 않습니다. +9. Noema 승인 marker가 존재하는데 `noema_current_head_approval_missing`이 남으면 `NOEMA_REVIEWER_LOGIN`이 실제 App bot login과 정확히 일치하는지 확인합니다. +10. `operational_error`이면 artifact의 bounded detail과 GitHub Actions 로그를 확인하고, 권한을 넓히기 전에 실제 API 실패 원인을 수정합니다. + +상세 RCA와 설계 근거는 `docs/doctoring/hourly-scheduler-activation-feasibility.md`에 기록합니다. diff --git a/test/hourly-commercial-readiness-activation.test.ts b/test/hourly-commercial-readiness-activation.test.ts new file mode 100644 index 000000000..4acc8ed9e --- /dev/null +++ b/test/hourly-commercial-readiness-activation.test.ts @@ -0,0 +1,76 @@ +import { readFileSync } from "node:fs"; +import { describe, expect, it } from "vitest"; + +describe("hourly commercial-readiness activation evidence", () => { + it("always runs a read-only activation preflight instead of skipping the whole schedule", () => { + const workflow = readFileSync( + ".github/workflows/hourly-commercial-readiness.yml", + "utf8", + ); + + expect(workflow).toContain("activation_preflight:"); + expect(workflow).toContain("name: scheduler-activation-preflight"); + expect(workflow).toContain("write_ready: ${{ steps.activation.outputs.write_ready }}"); + expect(workflow).toContain( + "terminal_classification: ${{ steps.activation.outputs.terminal_classification }}", + ); + expect(workflow).toContain("name: scheduler-activation-evidence"); + expect(workflow).toContain( + "path: artifacts/operations/hourly-scheduler-activation.json", + ); + expect(workflow).not.toContain( + "if: vars.NOEMA_MAINTENANCE_ENABLED == 'true'", + ); + }); + + it("opens the write lane only after the activation preflight proves it is ready", () => { + const workflow = readFileSync( + ".github/workflows/hourly-commercial-readiness.yml", + "utf8", + ); + + expect(workflow).toContain("needs: activation_preflight"); + expect(workflow).toContain( + "if: needs.activation_preflight.outputs.write_ready == 'true'", + ); + expect(workflow).toContain( + "MAINTENANCE_ENABLED: ${{ vars.NOEMA_MAINTENANCE_ENABLED == 'true' }}", + ); + expect(workflow).toContain( + "MAINTAINER_APP_CLIENT_ID_CONFIGURED: ${{ vars.NOEMA_MAINTAINER_APP_CLIENT_ID != '' }}", + ); + expect(workflow).toContain( + "MAINTAINER_APP_PRIVATE_KEY_CONFIGURED: ${{ secrets.NOEMA_MAINTAINER_APP_PRIVATE_KEY != '' }}", + ); + expect(workflow).toContain( + "REVIEWER_LOGIN_CONFIGURED: ${{ vars.NOEMA_REVIEWER_LOGIN != '' }}", + ); + expect(workflow).toContain("terminal_classification=EXTERNAL_GATE_REMAINS"); + expect(workflow).toContain("terminal_classification=NO_ACTION_NEEDED"); + }); + + it("keeps publicly downloadable activation evidence opaque to individual credential state", () => { + const workflow = readFileSync( + ".github/workflows/hourly-commercial-readiness.yml", + "utf8", + ); + + expect(workflow).toContain("reason_code=activation_prerequisite_unavailable"); + expect(workflow).not.toContain("terminal_classification=AUTH_OR_TOOLING_BLOCKER"); + for (const specificReason of [ + "maintainer_app_client_id_unavailable", + "maintainer_app_private_key_unavailable", + "reviewer_login_unavailable", + ]) { + expect(workflow).not.toContain(specificReason); + } + for (const retainedField of [ + "maintenance_enabled: $maintenance_enabled", + "maintainer_app_client_id_configured: $maintainer_app_client_id_configured", + "maintainer_app_private_key_configured: $maintainer_app_private_key_configured", + "reviewer_login_configured: $reviewer_login_configured", + ]) { + expect(workflow).not.toContain(retainedField); + } + }); +}); diff --git a/test/workflow-readiness.test.ts b/test/workflow-readiness.test.ts index 0d79a1c45..5202f2af4 100644 --- a/test/workflow-readiness.test.ts +++ b/test/workflow-readiness.test.ts @@ -74,7 +74,9 @@ describe("deployment workflow readiness gates", () => { it("uses a dedicated maintainer App token so merges trigger downstream workflows", () => { const workflow = readFileSync(".github/workflows/hourly-commercial-readiness.yml", "utf8"); - expect(workflow).toContain("if: vars.NOEMA_MAINTENANCE_ENABLED == 'true'"); + expect(workflow).toContain( + "if: needs.activation_preflight.outputs.write_ready == 'true'", + ); expect(workflow).toContain("actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1"); expect(workflow).toContain("NOEMA_MAINTAINER_APP_CLIENT_ID"); expect(workflow).toContain("NOEMA_MAINTAINER_APP_PRIVATE_KEY");