From 61e6d8805d9f54b350ccce59f811e0362cd77865 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 13 Aug 2026 09:05:48 +0900 Subject: [PATCH] fix(operations): restack scheduler activation evidence after release integration --- .../workflows/hourly-commercial-readiness.yml | 103 +++++++++++- ...hourly-scheduler-activation-feasibility.md | 147 ++++++++++++++++++ docs/hourly-commercial-readiness-loop.md | 54 +++++-- ...ly-commercial-readiness-activation.test.ts | 76 +++++++++ test/workflow-readiness.test.ts | 6 +- 5 files changed, 370 insertions(+), 16 deletions(-) create mode 100644 docs/doctoring/hourly-scheduler-activation-feasibility.md create mode 100644 test/hourly-commercial-readiness-activation.test.ts 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/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 339b0a72a..5202f2af4 100644 --- a/test/workflow-readiness.test.ts +++ b/test/workflow-readiness.test.ts @@ -12,7 +12,7 @@ describe("deployment workflow readiness gates", () => { ]) { const workflow = readFileSync(path, "utf8"); - expect(workflow).toMatch(/node-version:\s*"24(?:\.\d+\.\d+)?"/); + expect(workflow).toMatch(/node-version: "24(?:\.\d+\.\d+)?"/); expect(workflow).not.toContain('node-version: "20"'); } }); @@ -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");