diff --git a/.github/workflows/codecov-upload.yml b/.github/workflows/codecov-upload.yml index 57637838..54ed8483 100644 --- a/.github/workflows/codecov-upload.yml +++ b/.github/workflows/codecov-upload.yml @@ -50,11 +50,80 @@ jobs: go-version: ${{ env.GO_VERSION }} cache-dependency-path: backend/go.sum + # SECURITY: Keep pull_request (not pull_request_target) for secret-bearing backend tests. + # Untrusted code (fork PRs and Dependabot PRs) gets ephemeral workflow-only keys. + - name: Resolve encryption key for backend coverage + shell: bash + env: + EVENT_NAME: ${{ github.event_name }} + ACTOR: ${{ github.actor }} + REPO: ${{ github.repository }} + PR_HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }} + PR_HEAD_FORK: ${{ github.event.pull_request.head.repo.fork }} + WORKFLOW_SECRET_KEY: ${{ secrets.CHARON_ENCRYPTION_KEY_TEST }} + run: | + set -euo pipefail + + is_same_repo_pr=false + if [[ "$EVENT_NAME" == "pull_request" && -n "${PR_HEAD_REPO:-}" && "$PR_HEAD_REPO" == "$REPO" ]]; then + is_same_repo_pr=true + fi + + is_workflow_dispatch=false + if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then + is_workflow_dispatch=true + fi + + is_dependabot_pr=false + if [[ "$EVENT_NAME" == "pull_request" && "$ACTOR" == "dependabot[bot]" ]]; then + is_dependabot_pr=true + fi + + is_fork_pr=false + if [[ "$EVENT_NAME" == "pull_request" && "${PR_HEAD_FORK:-false}" == "true" ]]; then + is_fork_pr=true + fi + + is_untrusted=false + if [[ "$is_fork_pr" == "true" || "$is_dependabot_pr" == "true" ]]; then + is_untrusted=true + fi + + is_trusted=false + if [[ "$is_untrusted" == "false" && ( "$is_same_repo_pr" == "true" || "$is_workflow_dispatch" == "true" ) ]]; then + is_trusted=true + fi + + resolved_key="" + if [[ "$is_trusted" == "true" ]]; then + if [[ -z "${WORKFLOW_SECRET_KEY:-}" ]]; then + echo "::error title=Missing required secret::Trusted backend CI context requires CHARON_ENCRYPTION_KEY_TEST. Add repository secret CHARON_ENCRYPTION_KEY_TEST." + exit 1 + fi + resolved_key="$WORKFLOW_SECRET_KEY" + elif [[ "$is_untrusted" == "true" ]]; then + resolved_key="$(openssl rand -base64 32)" + else + echo "::error title=Unsupported event context::Unable to classify trust for backend key resolution (event=${EVENT_NAME})." + exit 1 + fi + + if [[ -z "$resolved_key" ]]; then + echo "::error title=Key resolution failure::Resolved encryption key is empty." + exit 1 + fi + + echo "::add-mask::$resolved_key" + { + echo "CHARON_ENCRYPTION_KEY<<__CHARON_EOF__" + echo "$resolved_key" + echo "__CHARON_EOF__" + } >> "$GITHUB_ENV" + - name: Run Go tests with coverage working-directory: ${{ github.workspace }} env: CGO_ENABLED: 1 - CHARON_ENCRYPTION_KEY: ${{ secrets.CHARON_ENCRYPTION_KEY_TEST }} run: | bash scripts/go-test-coverage.sh 2>&1 | tee backend/test-output.txt exit "${PIPESTATUS[0]}" diff --git a/.github/workflows/quality-checks.yml b/.github/workflows/quality-checks.yml index 97a29c1f..59507ef6 100644 --- a/.github/workflows/quality-checks.yml +++ b/.github/workflows/quality-checks.yml @@ -18,12 +18,92 @@ env: GOTOOLCHAIN: auto jobs: + codecov-trigger-parity-guard: + name: Codecov Trigger/Comment Parity Guard + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Enforce Codecov trigger and comment parity + run: | + bash scripts/ci/check-codecov-trigger-parity.sh + backend-quality: name: Backend (Go) runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + # SECURITY: Do not switch this workflow to pull_request_target for backend tests. + # Untrusted code paths (fork PRs and Dependabot PRs) must never receive repository secrets. + - name: Resolve encryption key for backend tests + shell: bash + env: + EVENT_NAME: ${{ github.event_name }} + ACTOR: ${{ github.actor }} + REPO: ${{ github.repository }} + PR_HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }} + PR_HEAD_FORK: ${{ github.event.pull_request.head.repo.fork }} + WORKFLOW_SECRET_KEY: ${{ secrets.CHARON_ENCRYPTION_KEY_TEST }} + run: | + set -euo pipefail + + is_same_repo_pr=false + if [[ "$EVENT_NAME" == "pull_request" && -n "${PR_HEAD_REPO:-}" && "$PR_HEAD_REPO" == "$REPO" ]]; then + is_same_repo_pr=true + fi + + is_workflow_dispatch=false + if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then + is_workflow_dispatch=true + fi + + is_dependabot_pr=false + if [[ "$EVENT_NAME" == "pull_request" && "$ACTOR" == "dependabot[bot]" ]]; then + is_dependabot_pr=true + fi + + is_fork_pr=false + if [[ "$EVENT_NAME" == "pull_request" && "${PR_HEAD_FORK:-false}" == "true" ]]; then + is_fork_pr=true + fi + + is_untrusted=false + if [[ "$is_fork_pr" == "true" || "$is_dependabot_pr" == "true" ]]; then + is_untrusted=true + fi + + is_trusted=false + if [[ "$is_untrusted" == "false" && ( "$is_same_repo_pr" == "true" || "$is_workflow_dispatch" == "true" ) ]]; then + is_trusted=true + fi + + resolved_key="" + if [[ "$is_trusted" == "true" ]]; then + if [[ -z "${WORKFLOW_SECRET_KEY:-}" ]]; then + echo "::error title=Missing required secret::Trusted backend CI context requires CHARON_ENCRYPTION_KEY_TEST. Add repository secret CHARON_ENCRYPTION_KEY_TEST." + exit 1 + fi + resolved_key="$WORKFLOW_SECRET_KEY" + elif [[ "$is_untrusted" == "true" ]]; then + resolved_key="$(openssl rand -base64 32)" + else + echo "::error title=Unsupported event context::Unable to classify trust for backend key resolution (event=${EVENT_NAME})." + exit 1 + fi + + if [[ -z "$resolved_key" ]]; then + echo "::error title=Key resolution failure::Resolved encryption key is empty." + exit 1 + fi + + echo "::add-mask::$resolved_key" + { + echo "CHARON_ENCRYPTION_KEY<<__CHARON_EOF__" + echo "$resolved_key" + echo "__CHARON_EOF__" + } >> "$GITHUB_ENV" + - name: Set up Go uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 with: @@ -39,7 +119,6 @@ jobs: working-directory: ${{ github.workspace }} env: CGO_ENABLED: 1 - CHARON_ENCRYPTION_KEY: ${{ secrets.CHARON_ENCRYPTION_KEY_TEST }} run: | bash "scripts/go-test-coverage.sh" 2>&1 | tee backend/test-output.txt exit "${PIPESTATUS[0]}" @@ -64,7 +143,7 @@ jobs: fi } >> "$GITHUB_STEP_SUMMARY" - # Codecov upload moved to `codecov-upload.yml` which is push-only. + # Codecov upload moved to `codecov-upload.yml` (pull_request + workflow_dispatch). - name: Run golangci-lint @@ -118,7 +197,6 @@ jobs: PERF_MAX_MS_GETSTATUS_P95: 500ms PERF_MAX_MS_GETSTATUS_P95_PARALLEL: 1500ms PERF_MAX_MS_LISTDECISIONS_P95: 2000ms - CHARON_ENCRYPTION_KEY: ${{ secrets.CHARON_ENCRYPTION_KEY_TEST }} run: | { echo "## 🔍 Running performance assertions (TestPerf)" @@ -212,7 +290,7 @@ jobs: fi } >> "$GITHUB_STEP_SUMMARY" - # Codecov upload moved to `codecov-upload.yml` which is push-only. + # Codecov upload moved to `codecov-upload.yml` (pull_request + workflow_dispatch). diff --git a/docs/plans/current_spec.md b/docs/plans/current_spec.md index 1f92b679..cebf8125 100644 --- a/docs/plans/current_spec.md +++ b/docs/plans/current_spec.md @@ -1,198 +1,145 @@ -# PR #666 Patch Coverage Recovery Spec (Approval-Ready) +# CI Encryption-Key Investigation and Remediation Plan -Date: 2026-02-16 -Owner: Planning Agent -Status: Draft for Supervisor approval (single coherent plan) +## Context +- Date: 2026-02-17 +- Scope: CI failures where backend jobs report encryption key not picked up. +- In-scope files: + - `.github/workflows/quality-checks.yml` + - `.github/workflows/codecov-upload.yml` + - `scripts/go-test-coverage.sh` + - `backend/internal/crypto/rotation_service.go` + - `backend/internal/services/dns_provider_service.go` + - `backend/internal/services/credential_service.go` -## 1) Scope Decision (Unified) +## Problem Statement +CI backend tests can fail late and ambiguously when `CHARON_ENCRYPTION_KEY` is missing or malformed. The root causes are context-dependent secret availability, missing preflight validation, and drift between workflow intent and implementation. -### In Scope -- Backend unit-test additions only, targeting changed patch lines in backend handlers/services/utils. -- Minimum-risk posture: prioritize test-only additions in files already touched by PR #666 before opening any new test surface. -- Coverage validation using current backend coverage task/script. +## Research Findings -### Out of Scope -- E2E/Playwright, integration, frontend, Docker, and security scan remediation. +### Workflow Surface and Risks +| Workflow | Job | Key-sensitive step | Current key source | Main risk | +|---|---|---|---|---| +| `.github/workflows/quality-checks.yml` | `backend-quality` | `Run Go tests`, `Run Perf Asserts` | `${{ secrets.CHARON_ENCRYPTION_KEY_TEST }}` | Empty/malformed input not preflighted | +| `.github/workflows/codecov-upload.yml` | `backend-codecov` | `Run Go tests with coverage` | `${{ secrets.CHARON_ENCRYPTION_KEY_TEST }}` | Same key-risk as above | -### E2E Decision for this task -- E2E is explicitly **out-of-scope** for this patch-coverage remediation. -- Rationale: target metric is Codecov patch lines on backend changes; E2E adds runtime risk/cycle time without direct line-level patch closure. +### Backend Failure Surface +- `backend/internal/crypto/rotation_service.go` + - `NewRotationService(db *gorm.DB)` hard-fails if `CHARON_ENCRYPTION_KEY` is empty. +- `backend/internal/services/dns_provider_service.go` + - `NewDNSProviderService(...)` depends on `NewRotationService(...)` and can degrade to warning-based behavior when key input is bad. +- `backend/internal/services/credential_service.go` + - `NewCredentialService(...)` has the same dependency pattern. -### Scope Reconciliation with Current Implementation (PR #666) -Confirmed already touched backend test files: -- `backend/internal/api/handlers/import_handler_test.go` -- `backend/internal/api/handlers/settings_handler_helpers_test.go` -- `backend/internal/api/handlers/emergency_handler_test.go` -- `backend/internal/services/backup_service_test.go` -- `backend/internal/api/handlers/backup_handler_test.go` -- `backend/internal/api/handlers/system_permissions_handler_test.go` -- `backend/internal/api/handlers/notification_provider_handler_validation_test.go` +### Script Failure Mode +- `scripts/go-test-coverage.sh` currently uses `set -euo pipefail` but does not pre-validate key shape before `go test`. +- Empty secret expressions become late runtime failures instead of deterministic preflight failures. -Optional/deferred (not yet touched in current remediation pass): -- `backend/internal/util/permissions_test.go` -- `backend/internal/services/notification_service_json_test.go` -- `backend/internal/services/backup_service_rehydrate_test.go` -- `backend/internal/api/handlers/security_handler_coverage_test.go` +## Supervisor-Required Constraints (Preserved) +1. `pull_request_target` SHALL NOT be used for secret-bearing backend test execution on untrusted code (fork PRs and Dependabot PRs). +2. Same-repo `pull_request` and `workflow_dispatch` SHALL require `CHARON_ENCRYPTION_KEY_TEST`; missing secret SHALL fail fast (no fallback). +3. Fork PRs and Dependabot PRs SHALL use workflow-only ephemeral key fallback for backend test execution. +4. Key material SHALL NEVER be logged. +5. Resolved key SHALL be masked before any potential output path. +6. `GITHUB_ENV` propagation SHALL use safe delimiter write pattern. +7. Workflow layer SHALL own key resolution/fallback. +8. Script layer SHALL only validate and fail fast; it SHALL NOT generate fallback keys. +9. Anti-drift guard SHALL be added so trigger comments and trigger blocks remain aligned. +10. Known drift SHALL be corrected: comment in `quality-checks.yml` about `codecov-upload.yml` trigger behavior must match actual triggers. -## 2) Single Source of Truth for Success +## EARS Requirements -Authoritative success metric: -- **Codecov PR patch status (`lines`)** is the source of truth for this task. +### Ubiquitous +- THE SYSTEM SHALL fail fast with explicit diagnostics when encryption-key input is required and unavailable or malformed. +- THE SYSTEM SHALL prevent secret-value exposure in logs, summaries, and artifacts. -Relationship to `codecov.yml`: -- `coverage.status.patch.default.target: 100%` and `required: false` means patch status is advisory in CI. -- For this plan, we set an internal quality gate: **patch lines >= 85%** (minimum), preferred **>= 87%** buffer. -- Local script output (`go tool cover`) remains diagnostic; pass/fail is decided by Codecov patch `lines` after upload. +### Event-driven +- WHEN workflow context is trusted (same-repo `pull_request` or `workflow_dispatch`), THE SYSTEM SHALL require `secrets.CHARON_ENCRYPTION_KEY_TEST`. +- WHEN workflow context is untrusted (fork PR or Dependabot PR), THE SYSTEM SHALL generate ephemeral key material in workflow preflight only. +- WHEN workflow context is untrusted, THE SYSTEM SHALL NOT use `pull_request_target` for secret-bearing backend tests. -## 3) Feasibility Math and Coverage-Line Budget +### Unwanted behavior +- IF `CHARON_ENCRYPTION_KEY` is empty, non-base64, or decoded length is not 32 bytes, THEN THE SYSTEM SHALL stop before running tests. +- IF trigger comments diverge from workflow triggers, THEN THE SYSTEM SHALL fail anti-drift validation. -Given baseline: -- Patch coverage = `60.84011%` -- Missing patch lines = `578` +## Technical Design -Derived totals: -- Let total patch lines = `T` -- `578 = T * (1 - 0.6084011)` => `T ≈ 1476` -- Currently covered lines `C0 = 1476 - 578 = 898` +### Workflow Contract +Both backend jobs (`backend-quality`, `backend-codecov`) implement the same preflight sequence: +1. `Resolve encryption key for backend tests` +2. `Fail fast when required encryption secret is missing` +3. `Validate encryption key format` -Required for >=85%: -- `C85 = ceil(0.85 * 1476) = 1255` -- Additional covered lines required: `1255 - 898 = 357` +### Preflight Resolution Algorithm +1. Detect fork PR context via `github.event.pull_request.head.repo.fork`. +2. Detect Dependabot PR context (actor/repo metadata check). +3. Trusted context: require `secrets.CHARON_ENCRYPTION_KEY_TEST`; fail immediately if empty. +4. Untrusted context: generate ephemeral key (`openssl rand -base64 32`) in workflow only. +5. Mask resolved key via `::add-mask::`. +6. Export via delimiter-based `GITHUB_ENV` write: + - `CHARON_ENCRYPTION_KEY<` + - `EOF` -Budget by phase (line-coverage gain target): +### Script Validation Contract +`scripts/go-test-coverage.sh` adds strict preflight validation: +- Present and non-empty. +- Base64 decodable. +- Decoded length exactly 32 bytes. -| Phase | Target line gain | Cumulative gain target | Stop/Go threshold | -|---|---:|---:|---| -| Phase 1 | +220 | +220 | Stop if <+170; re-scope before Phase 2 | -| Phase 2 | +100 | +320 | Stop if <+70; activate residual plan | -| Phase 3 (residual closure) | +45 | +365 | Must reach >=+357 total | +Script constraints: +- SHALL NOT generate keys. +- SHALL NOT select key source. +- SHALL only validate and fail fast with deterministic error messages. -Notes: -- Planned total gain `+365` gives `+8` lines safety over minimum `+357`. -- If patch denominator changes due to rebase/new touched lines, recompute budget before continuing. +### Error Handling Matrix +| Condition | Detection layer | Outcome | +|---|---|---| +| Trusted context + missing secret | Workflow preflight | Immediate failure with explicit message | +| Untrusted context + no secret access | Workflow preflight | Ephemeral key path (masked) | +| Malformed key | Script preflight | Immediate failure before `go test` | +| Trigger/comment drift | Workflow consistency guard | CI failure until synchronized | -## 4) Target Files/Functions (Concise, Specific) +## Implementation Plan -Primary hotspots (Phase 1 focus, aligned to touched tests first): -- `backend/internal/api/handlers/system_permissions_handler.go` - - `RepairPermissions`, `repairPath`, `normalizePath`, `pathHasSymlink`, `isWithinAllowlist`, `mapRepairErrorCode` -- `backend/internal/services/backup_service.go` - - `RestoreBackup`, `extractDatabaseFromBackup`, `unzipWithSkip`, `RehydrateLiveDatabase`, `GetAvailableSpace` -- `backend/internal/api/handlers/settings_handler.go` - - `UpdateSetting`, `PatchConfig`, `validateAdminWhitelist`, `syncAdminWhitelistWithDB` -- `backend/internal/api/handlers/import_handler.go` - - `GetStatus`, `Upload`, `Commit`, `Cancel`, `safeJoin` -- `backend/internal/api/handlers/backup_handler.go` - - `Restore`, `isSQLiteTransientRehydrateError` -- `backend/internal/api/handlers/emergency_handler.go` - - `SecurityReset`, `disableAllSecurityModules`, `upsertSettingWithRetry` -- `backend/internal/api/handlers/notification_provider_handler.go` - - `isProviderValidationError`, provider validation branches +### Phase 1: Workflow Hardening +- Update `.github/workflows/quality-checks.yml` and `.github/workflows/codecov-upload.yml` with identical key-resolution and key-validation steps. +- Enforce trusted-context fail-fast and untrusted-context fallback boundaries. +- Add explicit prohibition notes and controls preventing `pull_request_target` migration for secret-bearing tests. -Secondary hotspots (Phase 2 focus, optional/deferred expansion): -- `backend/internal/api/handlers/security_handler.go` (`GetStatus`, `latestConfigApplyState`) -- `backend/internal/util/permissions.go` (`CheckPathPermissions`, `MapSaveErrorCode`, `MapDiagnosticErrorCode`) -- `backend/internal/services/notification_service.go` (`sendJSONPayload`, `TestProvider`, `RenderTemplate`) +### Phase 2: Script Preflight Hardening +- Update `scripts/go-test-coverage.sh` to validate key presence/format/length before tests. +- Preserve existing coverage behavior; only harden pre-test guard path. -## 5) Execution Phases with Strict Stop/Go and De-Scoping Rules +### Phase 3: Anti-Drift Enforcement +- Define one canonical backend-key-bootstrap contract path. +- Add consistency check that enforces trigger/comment parity between `quality-checks.yml` and `codecov-upload.yml`. +- Fix known push-only comment mismatch in `quality-checks.yml`. -### Phase 0 - Baseline Lock -Actions: -- Run `Test: Backend with Coverage` task (`.github/skills/scripts/skill-runner.sh test-backend-coverage`). -- Record baseline patch lines from Codecov PR view and local artifact `backend/coverage.txt`. +## Validation Plan +Run these scenarios: +1. Same-repo PR with valid secret. +2. Same-repo PR with missing secret (must fail fast). +3. Same-repo PR with malformed secret (must fail fast before tests). +4. Fork PR with no secret access (must use ephemeral fallback). +5. Dependabot PR with no secret access (must use ephemeral fallback, no `pull_request_target`). +6. `workflow_dispatch` with valid secret. -Go gate: -- Baseline captured and denominator confirmed. +Expected results: +- No late ambiguous key-init failures. +- No secret material logged. +- Deterministic and attributable failure messages. +- Trigger docs and trigger config remain synchronized. -Stop gate: -- If patch denominator changed by >5% from 1476, pause and recompute budgets before coding. +## Acceptance Criteria +- Backend jobs in `quality-checks.yml` and `codecov-upload.yml` no longer fail ambiguously on encryption-key pickup. +- Trusted contexts fail fast if `CHARON_ENCRYPTION_KEY_TEST` is missing. +- Untrusted contexts use workflow-only ephemeral fallback. +- `scripts/go-test-coverage.sh` enforces deterministic key preflight checks. +- `pull_request_target` is explicitly prohibited for secret-bearing backend tests on untrusted code. +- Never-log-key-material and safe `GITHUB_ENV` propagation are implemented. +- Workflow/script responsibility boundary is enforced. +- Anti-drift guard is present and known trigger-comment mismatch is resolved. -### Phase 1 - High-yield branch closure -Actions: -- Extend existing tests only in: - - `backend/internal/api/handlers/system_permissions_handler_test.go` - - `backend/internal/services/backup_service_test.go` - - `backend/internal/api/handlers/backup_handler_test.go` - - `backend/internal/api/handlers/emergency_handler_test.go` - - `backend/internal/api/handlers/settings_handler_helpers_test.go` - - `backend/internal/api/handlers/import_handler_test.go` - - `backend/internal/api/handlers/notification_provider_handler_validation_test.go` - -Go gate: -- Achieve >= `+170` covered patch lines and no failing backend tests. - -Stop gate: -- If < `+170`, do not proceed; re-scope to only highest delta-per-test functions. - -### Phase 2 - Secondary branch fill -Actions: -- Extend tests in: - - `backend/internal/api/handlers/security_handler_coverage_test.go` - - `backend/internal/util/permissions_test.go` - - `backend/internal/services/backup_service_rehydrate_test.go` - - `backend/internal/services/notification_service_json_test.go` - -Go gate: -- Additional >= `+70` covered patch lines in this phase. - -Stop gate: -- If < `+70`, skip low-yield areas and move directly to residual-line closure. - -### Phase 3 - Residual-line closure (minimum-risk) -Actions: -- Work only uncovered/partial lines still shown in Codecov patch details. -- Add narrow table-driven tests to existing files; no new harness/framework. - -Go gate: -- Reach total >= `+357` covered lines and patch >=85%. - -Stop gate: -- If a residual branch requires production refactor, de-scope it and log as follow-up. - -### Global de-scope rules (all phases) -- No production code changes unless a test proves a correctness bug. -- No new test framework, no integration/E2E expansion, no unrelated cleanup. -- No edits outside targeted backend test and directly related helper files. - -## 6) Current Tasks/Scripts (Deprecated references removed) - -Use these current commands/tasks only: -- Backend coverage (preferred): `Test: Backend with Coverage` - - command: `.github/skills/scripts/skill-runner.sh test-backend-coverage` -- Equivalent direct script: `bash scripts/go-test-coverage.sh` -- Optional backend unit quick check: `Test: Backend Unit Tests` - - command: `.github/skills/scripts/skill-runner.sh test-backend-unit` - -Deprecated tasks are explicitly out-of-plan (for this work): -- `Security: CodeQL Go Scan (DEPRECATED)` -- `Security: CodeQL JS Scan (DEPRECATED)` - -## 7) Residual Uncovered Lines Handling (Beyond hotspot table) - -After each phase, run a residual triage loop: -1. Export remaining uncovered/partial patch lines from Codecov patch detail. -2. Classify each residual line into one of: - - `validation/error mapping` - - `permission/role guard` - - `fallback/retry` - - `low-value defensive log/telemetry` -3. Apply closure rule: - - First three classes: add targeted tests in existing suite. - - Last class: close only if deterministic and cheap; otherwise de-scope with rationale. -4. Maintain a residual ledger in the PR description: - - line(s), owning function, planned test, status (`closed`/`de-scoped`), reason. - -Exit condition: -- No unclassified residual lines remain. -- Any de-scoped residual lines have explicit follow-up items. - -## 8) Acceptance Criteria (Unified) - -1. One coherent plan only (this document), no conflicting statuses. -2. E2E explicitly out-of-scope for this patch-coverage task. -3. Success is measured by Codecov patch `lines`; local statement output is diagnostic only. -4. Feasibility math and phase budgets remain explicit and tracked against actual deltas. -5. All phase stop/go gates enforced; de-scope rules followed. -6. Only current tasks/scripts are referenced. -7. Residual uncovered lines are either closed with tests or formally de-scoped with follow-up. -8. Scope remains reconciled with touched files first; deferred files are only pulled in if phase gates require expansion. +## Handoff to Supervisor +- This document is intentionally single-scope and restricted to CI encryption-key investigation/remediation. +- Legacy multi-topic coverage planning content has been removed from this file to maintain coherence. diff --git a/scripts/ci/check-codecov-trigger-parity.sh b/scripts/ci/check-codecov-trigger-parity.sh new file mode 100644 index 00000000..ef59280f --- /dev/null +++ b/scripts/ci/check-codecov-trigger-parity.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +set -euo pipefail + +QUALITY_WORKFLOW=".github/workflows/quality-checks.yml" +CODECOV_WORKFLOW=".github/workflows/codecov-upload.yml" +EXPECTED_COMMENT='Codecov upload moved to `codecov-upload.yml` (pull_request + workflow_dispatch).' + +fail() { + local message="$1" + echo "::error title=Codecov trigger/comment drift::${message}" + exit 1 +} + +[[ -f "$QUALITY_WORKFLOW" ]] || fail "Missing workflow file: $QUALITY_WORKFLOW" +[[ -f "$CODECOV_WORKFLOW" ]] || fail "Missing workflow file: $CODECOV_WORKFLOW" + +grep -qE '^on:' "$QUALITY_WORKFLOW" || fail "quality-checks workflow is missing an 'on:' block" +grep -qE '^on:' "$CODECOV_WORKFLOW" || fail "codecov-upload workflow is missing an 'on:' block" + +grep -qE '^ pull_request:' "$QUALITY_WORKFLOW" || fail "quality-checks must run on pull_request" +if grep -qE '^ workflow_dispatch:' "$QUALITY_WORKFLOW"; then + fail "quality-checks unexpectedly includes workflow_dispatch; keep Codecov manual trigger scoped to codecov-upload workflow" +fi + +grep -qE '^ pull_request:' "$CODECOV_WORKFLOW" || fail "codecov-upload must run on pull_request" +grep -qE '^ workflow_dispatch:' "$CODECOV_WORKFLOW" || fail "codecov-upload must run on workflow_dispatch" +if grep -qE '^ pull_request_target:' "$CODECOV_WORKFLOW"; then + fail "codecov-upload must not use pull_request_target" +fi + +if ! grep -Fq "$EXPECTED_COMMENT" "$QUALITY_WORKFLOW"; then + fail "quality-checks Codecov handoff comment is missing or changed; expected: $EXPECTED_COMMENT" +fi + +echo "Codecov trigger/comment parity check passed" diff --git a/scripts/go-test-coverage.sh b/scripts/go-test-coverage.sh index a72c1179..3f7ff3c8 100755 --- a/scripts/go-test-coverage.sh +++ b/scripts/go-test-coverage.sh @@ -13,6 +13,26 @@ BACKEND_DIR="$ROOT_DIR/backend" COVERAGE_FILE="$BACKEND_DIR/coverage.txt" MIN_COVERAGE="${CHARON_MIN_COVERAGE:-${CPM_MIN_COVERAGE:-85}}" +if [[ -z "${CHARON_ENCRYPTION_KEY:-}" ]]; then + echo "Error: CHARON_ENCRYPTION_KEY is required for backend tests." + echo "Set CHARON_ENCRYPTION_KEY to a base64-encoded 32-byte key before running this script." + exit 1 +fi + +DECODED_KEY_HEX="" +if ! DECODED_KEY_HEX=$(printf '%s' "$CHARON_ENCRYPTION_KEY" | base64 --decode 2>/dev/null | od -An -tx1 -v | tr -d ' \n'); then + echo "Error: CHARON_ENCRYPTION_KEY is not valid base64." + echo "Provide a base64-encoded key whose decoded length is exactly 32 bytes." + exit 1 +fi + +DECODED_KEY_BYTES=$(( ${#DECODED_KEY_HEX} / 2 )) +if [[ "$DECODED_KEY_BYTES" -ne 32 ]]; then + echo "Error: CHARON_ENCRYPTION_KEY decoded length is ${DECODED_KEY_BYTES} bytes; expected 32 bytes." + echo "Regenerate key (example): openssl rand -base64 32" + exit 1 +fi + # Perf asserts are sensitive to -race overhead; loosen defaults for hook runs export PERF_MAX_MS_GETSTATUS_P95="${PERF_MAX_MS_GETSTATUS_P95:-25ms}" export PERF_MAX_MS_GETSTATUS_P95_PARALLEL="${PERF_MAX_MS_GETSTATUS_P95_PARALLEL:-50ms}"