From 9bd057e2f22d5b47ad1014334e36e2d14561f147 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 04:51:53 +0900 Subject: [PATCH 1/4] fix: preserve OSV base output across forks --- .github/workflows/security-scan.yml | 11 ++--- CHANGELOG.md | 2 + .../osv-cross-fork-result-isolation.md | 45 +++++++++++++++++++ .../organization_commercial_readiness_loop.py | 3 +- .../test_required_workflow_queue_contract.py | 15 ++++--- 5 files changed, 63 insertions(+), 13 deletions(-) create mode 100644 docs/doctoring/osv-cross-fork-result-isolation.md diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml index c3b8fa5dbb..61d7d7c54f 100644 --- a/.github/workflows/security-scan.yml +++ b/.github/workflows/security-scan.yml @@ -81,7 +81,7 @@ jobs: with: scan-args: | --format=json - --output=old-results.json + --output-file=/github/runner_temp/old-results.json --maven-registry=https://maven-central.storage-download.googleapis.com/maven2 --no-resolve --allow-no-lockfiles @@ -99,7 +99,7 @@ jobs: with: scan-args: | --format=json - --output=old-results.json + --output-file=/github/runner_temp/old-results.json --no-resolve --allow-no-lockfiles -r @@ -120,7 +120,7 @@ jobs: with: scan-args: | --format=json - --output=new-results.json + --output-file=new-results.json --maven-registry=https://maven-central.storage-download.googleapis.com/maven2 --no-resolve --allow-no-lockfiles @@ -138,7 +138,7 @@ jobs: with: scan-args: | --format=json - --output=new-results.json + --output-file=new-results.json --no-resolve --allow-no-lockfiles -r @@ -146,7 +146,8 @@ jobs: - name: Require OSV scan output run: | set -euo pipefail - test -s old-results.json + test -s "$RUNNER_TEMP/old-results.json" + cp "$RUNNER_TEMP/old-results.json" old-results.json test -s new-results.json - name: Print OSV findings being compared shell: python3 {0} diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ab82ccea8..cb5a0970a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,8 @@ Semantic Versioning where the repository publishes a release. ### Fixed +- Preserved the OSV base-scan JSON in the runner-temporary mount before a cross-fork head checkout replaces the workspace, then restored the exact artifact for the required base/head vulnerability comparison instead of failing every fork PR with a missing `old-results.json`. +- Restored 100% docstring coverage for the commercial-readiness GitHub transport constructor. - Refused PR Review Merge Scheduler head mutations, `update-branch` and the last-push approval head restamp, whenever the resolved mutation credential is the workflow `GITHUB_TOKEN`. GitHub starts no workflow run for events created with that credential, so the moved head collected no current-head required checks and the PR stayed permanently `BLOCKED` with a `github-actions[bot]` merge commit that no later scheduler run could repair, because the branch was no longer behind. The scheduler now waits with `head_mutation_credential_upgrade` guidance naming `PR_REVIEW_MERGE_TOKEN`, `OPENCODE_APPROVE_TOKEN`, and the OpenCode app token exchange. - Parsed `opencode.jsonc` as JSONC (stripping `//` and `/* */` comments outside string literals) in the reasoning-effort guard and its contract tests, instead of raw `json.loads`, which rejected the file the moment it carried its first explanatory comment (added for the `contextual-orchestrator` provider block) with `Expecting property name enclosed in double quotes`. Comment markers inside string values, such as the `$schema` URL, are left untouched. - Download the pinned `uv` 0.12.1 exporter from the official GitHub Releases URL instead of `releases.astral.sh`, which now returns HTTP 403 and blocks org-wide OpenCode `coverage-evidence`. The SHA-256 pin is unchanged. The opener may follow one hop onto `release-assets.githubusercontent.com` or `objects.githubusercontent.com` and still rejects every other host, userinfo, non-HTTPS scheme, and nondefault port (ContextualWisdomLab/.github#1109). diff --git a/docs/doctoring/osv-cross-fork-result-isolation.md b/docs/doctoring/osv-cross-fork-result-isolation.md new file mode 100644 index 0000000000..5af51331d7 --- /dev/null +++ b/docs/doctoring/osv-cross-fork-result-isolation.md @@ -0,0 +1,45 @@ +# OSV cross-fork result isolation + +## Failure and root cause + +The organization-required `osv-scan` passed both scanner invocations but failed +`Require OSV scan output` because `old-results.json` disappeared. The base +checkout used the upstream repository while the head belonged to a fork. +`actions/checkout` detected the different repository origin and replaced the +workspace before checking out the fork, including the untracked base result. +`clean: false` does not preserve files when checkout must replace a workspace +whose repository identity changed. + +## Decision + +Write the base result to `/github/runner_temp/old-results.json`, the container +mount for the job-scoped runner temporary directory. After the head checkout, +require that artifact and copy it back to `old-results.json` so the existing +OSV reporter, log, SARIF, and debug-artifact boundaries stay unchanged. Keep the +head result in the current workspace. Missing or empty output remains a hard +failure; this change does not weaken vulnerability comparison. + +The workflow also uses OSV Scanner's current `--output-file` spelling instead +of the deprecated `--output` alias. + +## Verification and rollback + +- The workflow contract proves both base attempts target the runner-temporary + mount and that the required-output step restores the artifact before use. +- `actionlint` validates the edited workflow. +- Rerun a fork PR's `Security Scan`; both `old-results.json` and + `new-results.json` must be non-empty before the reporter runs. +- Roll back only after the base and head are scanned in independently named + workspaces with normalized source paths, or another job-scoped store retains + the base artifact across repository replacement. + +## References + +GitHub. (2026). *Variables reference*. GitHub Docs. Retrieved August 22, 2026, +from https://docs.github.com/en/actions/reference/variables-reference + +GitHub Actions. (2026). *Checkout*. GitHub. Retrieved August 22, 2026, from +https://github.com/actions/checkout + +Google. (2026). *OSV-Scanner*. GitHub. Retrieved August 22, 2026, from +https://github.com/google/osv-scanner diff --git a/scripts/ci/organization_commercial_readiness_loop.py b/scripts/ci/organization_commercial_readiness_loop.py index c00cfa1e0a..71a0232ef8 100644 --- a/scripts/ci/organization_commercial_readiness_loop.py +++ b/scripts/ci/organization_commercial_readiness_loop.py @@ -239,6 +239,7 @@ class GitHubClient: """Use the GitHub CLI as an authenticated, bounded REST transport.""" def __init__(self, token: str, *, timeout_seconds: int = 60) -> None: + """Initialize one token-bound client with a bounded request timeout.""" if not token: raise GitHubError("GH_TOKEN is required for organization coordination") self._token = token @@ -853,4 +854,4 @@ def main( if __name__ == "__main__": # pragma: no cover - exercised through main() - raise SystemExit(main()) \ No newline at end of file + raise SystemExit(main()) diff --git a/tests/test_required_workflow_queue_contract.py b/tests/test_required_workflow_queue_contract.py index 9e6d14b333..172a095d4a 100644 --- a/tests/test_required_workflow_queue_contract.py +++ b/tests/test_required_workflow_queue_contract.py @@ -903,14 +903,15 @@ def test_security_scan_skips_dependency_review_when_dependency_graph_is_unavaila assert "steps.dependency_review_support.outputs.supported == 'true'" in workflow -def test_security_scan_allows_repositories_without_supported_lockfiles() -> None: - """Allow manifestless repositories while still requiring scan artifacts.""" +def test_security_scan_preserves_base_output_across_cross_fork_checkout() -> None: + """Keep the base artifact outside a fork checkout's replaced workspace.""" workflow = workflow_text("security-scan.yml") assert workflow.count("--allow-no-lockfiles") == 4 - assert "--output=old-results.json" in workflow - assert "--output=new-results.json" in workflow - assert "test -s old-results.json" in workflow + assert workflow.count("--output-file=/github/runner_temp/old-results.json") == 2 + assert workflow.count("--output-file=new-results.json") == 2 + assert 'test -s "$RUNNER_TEMP/old-results.json"' in workflow + assert 'cp "$RUNNER_TEMP/old-results.json" old-results.json' in workflow assert "test -s new-results.json" in workflow @@ -983,8 +984,8 @@ def test_osv_scan_logs_and_retries_without_transitive_resolution_on_resolver_fai "Retry head OSV without transitive resolution\n if: steps.osv_head.outcome == 'failure'\n continue-on-error: true" in workflow ) - assert "--output=old-results.json" in workflow - assert "--output=new-results.json" in workflow + assert "--output-file=/github/runner_temp/old-results.json" in workflow + assert "--output-file=new-results.json" in workflow assert "Print OSV findings being compared" in workflow assert "OSV {label} scan produced {len(findings)} finding(s)" in workflow From 53781cc16b79a03a36f9dc9f7e1d6b9886cd0622 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 05:06:04 +0900 Subject: [PATCH 2/4] fix: isolate cross-fork OSV checkouts --- .github/workflows/security-scan.yml | 18 ++++++++-------- CHANGELOG.md | 2 +- .../osv-cross-fork-result-isolation.md | 21 +++++++++---------- .../test_required_workflow_queue_contract.py | 12 ++++++----- 4 files changed, 27 insertions(+), 26 deletions(-) diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml index 61d7d7c54f..bbac70b48d 100644 --- a/.github/workflows/security-scan.yml +++ b/.github/workflows/security-scan.yml @@ -71,6 +71,7 @@ jobs: with: repository: ${{ github.event.pull_request.base.repo.full_name }} ref: ${{ github.event.pull_request.base.sha }} + path: source fetch-depth: 0 persist-credentials: false - name: Scan base with OSV @@ -81,12 +82,12 @@ jobs: with: scan-args: | --format=json - --output-file=/github/runner_temp/old-results.json + --output-file=old-results.json --maven-registry=https://maven-central.storage-download.googleapis.com/maven2 --no-resolve --allow-no-lockfiles -r - ./ + source/ - name: Explain base OSV resolver fallback if: steps.osv_base.outcome == 'failure' run: | @@ -99,18 +100,18 @@ jobs: with: scan-args: | --format=json - --output-file=/github/runner_temp/old-results.json + --output-file=old-results.json --no-resolve --allow-no-lockfiles -r - ./ + source/ - name: Checkout head uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: repository: ${{ github.event.pull_request.head.repo.full_name }} ref: ${{ github.event.pull_request.head.sha }} + path: source fetch-depth: 0 - clean: false persist-credentials: false - name: Scan head with OSV id: osv_head @@ -125,7 +126,7 @@ jobs: --no-resolve --allow-no-lockfiles -r - ./ + source/ - name: Explain head OSV resolver fallback if: steps.osv_head.outcome == 'failure' run: | @@ -142,12 +143,11 @@ jobs: --no-resolve --allow-no-lockfiles -r - ./ + source/ - name: Require OSV scan output run: | set -euo pipefail - test -s "$RUNNER_TEMP/old-results.json" - cp "$RUNNER_TEMP/old-results.json" old-results.json + test -s old-results.json test -s new-results.json - name: Print OSV findings being compared shell: python3 {0} diff --git a/CHANGELOG.md b/CHANGELOG.md index cb5a0970a0..8790f1c9e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,7 +36,7 @@ Semantic Versioning where the repository publishes a release. ### Fixed -- Preserved the OSV base-scan JSON in the runner-temporary mount before a cross-fork head checkout replaces the workspace, then restored the exact artifact for the required base/head vulnerability comparison instead of failing every fork PR with a missing `old-results.json`. +- Confined OSV base and head repository checkouts to the same `source/` child directory, so a cross-fork head checkout can replace that repository without deleting the base-scan JSON held at the workspace root. Both scans retain identical source paths and the required base/head vulnerability comparison remains fail-closed. - Restored 100% docstring coverage for the commercial-readiness GitHub transport constructor. - Refused PR Review Merge Scheduler head mutations, `update-branch` and the last-push approval head restamp, whenever the resolved mutation credential is the workflow `GITHUB_TOKEN`. GitHub starts no workflow run for events created with that credential, so the moved head collected no current-head required checks and the PR stayed permanently `BLOCKED` with a `github-actions[bot]` merge commit that no later scheduler run could repair, because the branch was no longer behind. The scheduler now waits with `head_mutation_credential_upgrade` guidance naming `PR_REVIEW_MERGE_TOKEN`, `OPENCODE_APPROVE_TOKEN`, and the OpenCode app token exchange. - Parsed `opencode.jsonc` as JSONC (stripping `//` and `/* */` comments outside string literals) in the reasoning-effort guard and its contract tests, instead of raw `json.loads`, which rejected the file the moment it carried its first explanatory comment (added for the `contextual-orchestrator` provider block) with `Expecting property name enclosed in double quotes`. Comment markers inside string values, such as the `$schema` URL, are left untouched. diff --git a/docs/doctoring/osv-cross-fork-result-isolation.md b/docs/doctoring/osv-cross-fork-result-isolation.md index 5af51331d7..5d72c58afd 100644 --- a/docs/doctoring/osv-cross-fork-result-isolation.md +++ b/docs/doctoring/osv-cross-fork-result-isolation.md @@ -12,26 +12,25 @@ whose repository identity changed. ## Decision -Write the base result to `/github/runner_temp/old-results.json`, the container -mount for the job-scoped runner temporary directory. After the head checkout, -require that artifact and copy it back to `old-results.json` so the existing -OSV reporter, log, SARIF, and debug-artifact boundaries stay unchanged. Keep the -head result in the current workspace. Missing or empty output remains a hard -failure; this change does not weaken vulnerability comparison. +Checkout both exact repositories into the same `source/` child directory and +scan that directory. The head checkout may replace `source/` when the repository +identity changes, but the base result remains at the workspace root. Reusing the +same checkout path also gives the base and head scan identical source paths, so +the existing OSV comparison retains its meaning. Missing or empty output remains +a hard failure; this change does not weaken vulnerability comparison. The workflow also uses OSV Scanner's current `--output-file` spelling instead of the deprecated `--output` alias. ## Verification and rollback -- The workflow contract proves both base attempts target the runner-temporary - mount and that the required-output step restores the artifact before use. +- The workflow contract proves both checkouts target `source/`, every scan reads + that same directory, and both result files remain at the workspace root. - `actionlint` validates the edited workflow. - Rerun a fork PR's `Security Scan`; both `old-results.json` and `new-results.json` must be non-empty before the reporter runs. -- Roll back only after the base and head are scanned in independently named - workspaces with normalized source paths, or another job-scoped store retains - the base artifact across repository replacement. +- Roll back only after another job-scoped store retains the base artifact across + repository replacement without changing the compared source paths. ## References diff --git a/tests/test_required_workflow_queue_contract.py b/tests/test_required_workflow_queue_contract.py index 172a095d4a..250df76aed 100644 --- a/tests/test_required_workflow_queue_contract.py +++ b/tests/test_required_workflow_queue_contract.py @@ -904,14 +904,16 @@ def test_security_scan_skips_dependency_review_when_dependency_graph_is_unavaila def test_security_scan_preserves_base_output_across_cross_fork_checkout() -> None: - """Keep the base artifact outside a fork checkout's replaced workspace.""" + """Limit cross-fork replacement to a child checkout directory.""" workflow = workflow_text("security-scan.yml") assert workflow.count("--allow-no-lockfiles") == 4 - assert workflow.count("--output-file=/github/runner_temp/old-results.json") == 2 + assert workflow.count("path: source") == 2 + assert workflow.count("--output-file=old-results.json") == 2 assert workflow.count("--output-file=new-results.json") == 2 - assert 'test -s "$RUNNER_TEMP/old-results.json"' in workflow - assert 'cp "$RUNNER_TEMP/old-results.json" old-results.json' in workflow + assert workflow.count("source/") == 4 + assert "clean: false" not in workflow + assert "test -s old-results.json" in workflow assert "test -s new-results.json" in workflow @@ -984,7 +986,7 @@ def test_osv_scan_logs_and_retries_without_transitive_resolution_on_resolver_fai "Retry head OSV without transitive resolution\n if: steps.osv_head.outcome == 'failure'\n continue-on-error: true" in workflow ) - assert "--output-file=/github/runner_temp/old-results.json" in workflow + assert "--output-file=old-results.json" in workflow assert "--output-file=new-results.json" in workflow assert "Print OSV findings being compared" in workflow assert "OSV {label} scan produced {len(findings)} finding(s)" in workflow From 225c415179180606f9a935304f61b09dc3e5c084 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 05:11:44 +0900 Subject: [PATCH 3/4] fix: retain proven OSV output flags --- .github/workflows/security-scan.yml | 8 ++++---- docs/doctoring/osv-cross-fork-result-isolation.md | 3 --- tests/test_required_workflow_queue_contract.py | 8 ++++---- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml index bbac70b48d..29376269d4 100644 --- a/.github/workflows/security-scan.yml +++ b/.github/workflows/security-scan.yml @@ -82,7 +82,7 @@ jobs: with: scan-args: | --format=json - --output-file=old-results.json + --output=old-results.json --maven-registry=https://maven-central.storage-download.googleapis.com/maven2 --no-resolve --allow-no-lockfiles @@ -100,7 +100,7 @@ jobs: with: scan-args: | --format=json - --output-file=old-results.json + --output=old-results.json --no-resolve --allow-no-lockfiles -r @@ -121,7 +121,7 @@ jobs: with: scan-args: | --format=json - --output-file=new-results.json + --output=new-results.json --maven-registry=https://maven-central.storage-download.googleapis.com/maven2 --no-resolve --allow-no-lockfiles @@ -139,7 +139,7 @@ jobs: with: scan-args: | --format=json - --output-file=new-results.json + --output=new-results.json --no-resolve --allow-no-lockfiles -r diff --git a/docs/doctoring/osv-cross-fork-result-isolation.md b/docs/doctoring/osv-cross-fork-result-isolation.md index 5d72c58afd..c9a6c0a98d 100644 --- a/docs/doctoring/osv-cross-fork-result-isolation.md +++ b/docs/doctoring/osv-cross-fork-result-isolation.md @@ -19,9 +19,6 @@ same checkout path also gives the base and head scan identical source paths, so the existing OSV comparison retains its meaning. Missing or empty output remains a hard failure; this change does not weaken vulnerability comparison. -The workflow also uses OSV Scanner's current `--output-file` spelling instead -of the deprecated `--output` alias. - ## Verification and rollback - The workflow contract proves both checkouts target `source/`, every scan reads diff --git a/tests/test_required_workflow_queue_contract.py b/tests/test_required_workflow_queue_contract.py index 250df76aed..b196c4ba62 100644 --- a/tests/test_required_workflow_queue_contract.py +++ b/tests/test_required_workflow_queue_contract.py @@ -909,8 +909,8 @@ def test_security_scan_preserves_base_output_across_cross_fork_checkout() -> Non assert workflow.count("--allow-no-lockfiles") == 4 assert workflow.count("path: source") == 2 - assert workflow.count("--output-file=old-results.json") == 2 - assert workflow.count("--output-file=new-results.json") == 2 + assert workflow.count("--output=old-results.json") == 2 + assert workflow.count("--output=new-results.json") == 2 assert workflow.count("source/") == 4 assert "clean: false" not in workflow assert "test -s old-results.json" in workflow @@ -986,8 +986,8 @@ def test_osv_scan_logs_and_retries_without_transitive_resolution_on_resolver_fai "Retry head OSV without transitive resolution\n if: steps.osv_head.outcome == 'failure'\n continue-on-error: true" in workflow ) - assert "--output-file=old-results.json" in workflow - assert "--output-file=new-results.json" in workflow + assert "--output=old-results.json" in workflow + assert "--output=new-results.json" in workflow assert "Print OSV findings being compared" in workflow assert "OSV {label} scan produced {len(findings)} finding(s)" in workflow From d3a3f4e6211a56d503b783d8784d1d79a262ca68 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 05:45:39 +0900 Subject: [PATCH 4/4] docs(fixtures): complete quality gate docstrings --- organization_commercial_readiness_fixtures.py | 1 + 1 file changed, 1 insertion(+) diff --git a/organization_commercial_readiness_fixtures.py b/organization_commercial_readiness_fixtures.py index d865961962..4275ea3dcb 100644 --- a/organization_commercial_readiness_fixtures.py +++ b/organization_commercial_readiness_fixtures.py @@ -90,6 +90,7 @@ def __init__( repositories: list[dict[str, Any]], snapshots: dict[str, list[RepositorySnapshot | Exception]], ) -> None: + """Initialize deterministic repository and dispatch fixtures.""" self.repositories = repositories self.snapshots = snapshots self.dispatched_repairs: list[tuple[str, str]] = []