diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 397c518..3a1d7e8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -186,19 +186,6 @@ jobs: python-version: "3.12" - run: pip install build twine - run: sh -n scripts/ci/check-docs-release-audit.sh - - name: Verify release recovery source contracts - env: - CONFORMANCE_BASE_REF: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before }} - run: | - arguments=() - if [ -n "$CONFORMANCE_BASE_REF" ]; then - arguments+=(--previous-ref "$CONFORMANCE_BASE_REF") - fi - python scripts/ci/release_recovery_consumer_conformance.py \ - --contract scripts/ci/release-recovery-consumer-contract.json \ - --adapter scripts/ci/release-recovery-consumer-adapter.json \ - "${arguments[@]}" - python scripts/ci/test-component-release-recovery.py ConsumerContractIdentityRegressionTest - run: python -m build - name: Compare built release metadata with the exact source commit run: python scripts/check_release_metadata.py --source-ref "$(git rev-parse HEAD)" --dist dist diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 04a44a9..0d450be 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -3,7 +3,6 @@ name: Publish to PyPI run-name: >- ${{ (github.event_name == 'push' || inputs.publish) && 'Publish' || 'Build' }} ${{ inputs.release_tag || github.ref_name }}@${{ inputs.release_commit || github.sha }} - from ${{ inputs.release_plan || 'direct' }} on: push: @@ -17,15 +16,10 @@ on: type: string default: '' release_commit: - description: 'Expected commit for a release-plan publication' + description: 'Expected commit for publication' required: false type: string default: '' - release_plan: - description: 'Immutable release-plan tag initiating this recovery run' - required: false - type: string - default: 'direct' publish: description: 'Publish the exact release tag to PyPI' required: false diff --git a/.github/workflows/release-plan-recovery.yml b/.github/workflows/release-plan-recovery.yml deleted file mode 100644 index a60eb15..0000000 --- a/.github/workflows/release-plan-recovery.yml +++ /dev/null @@ -1,143 +0,0 @@ -name: Release plan recovery - -run-name: Recover Python SDK from ${{ inputs.plan_tag || 'latest public release plan' }} - -on: - schedule: - - cron: '43 * * * *' - workflow_dispatch: - inputs: - plan_tag: - description: Immutable release-plan tag; empty selects the newest public plan - required: false - type: string - default: '' - -permissions: - attestations: read - contents: read - -concurrency: - group: release-plan-recovery-sdk-python-${{ inputs.plan_tag || 'latest' }} - cancel-in-progress: false - -jobs: - discover: - name: Discover exact Python SDK release - if: github.ref == 'refs/heads/main' - runs-on: ubuntu-latest - timeout-minutes: 10 - permissions: - attestations: read - contents: read - outputs: - action: ${{ steps.recovery.outputs.action }} - plan: ${{ steps.recovery.outputs.plan }} - plan_tag: ${{ steps.recovery.outputs.plan_tag }} - version: ${{ steps.recovery.outputs.version }} - commit: ${{ steps.recovery.outputs.commit }} - steps: - - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - - - name: Discover plan and verify upstream public artifacts - id: recovery - env: - GITHUB_TOKEN: ${{ github.token }} - REQUESTED_PLAN_TAG: ${{ inputs.plan_tag }} - run: | - arguments=( - resolve - --component sdk-python - --plan-output release-plan.json - --preparation-output release-preparation.json - --evidence release-recovery-evidence.json - --github-output "$GITHUB_OUTPUT" - ) - if [ "$GITHUB_EVENT_NAME" = schedule ]; then - arguments+=(--allow-empty) - elif [ -n "$REQUESTED_PLAN_TAG" ]; then - arguments+=(--plan-tag "$REQUESTED_PLAN_TAG") - fi - python scripts/ci/component-release-recovery.py "${arguments[@]}" - - - name: Retain recovery evidence - if: always() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 - with: - name: sdk-python-release-recovery-${{ steps.recovery.outputs.plan || github.run_id }} - path: | - release-plan.json - release-preparation.json - release-recovery-evidence.json - if-no-files-found: warn - - publish: - name: Publish exact Python SDK release - needs: discover - if: >- - github.repository == 'durable-workflow/sdk-python' && - github.ref == 'refs/heads/main' && - needs.discover.outputs.action == 'publish' - runs-on: ubuntu-latest - timeout-minutes: 10 - permissions: - actions: write - attestations: read - contents: write - steps: - - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - - - name: Create the exact source tag - env: - GH_TOKEN: ${{ github.token }} - RELEASE_TAG: ${{ needs.discover.outputs.version }} - RELEASE_COMMIT: ${{ needs.discover.outputs.commit }} - run: | - set -euo pipefail - if ! gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$RELEASE_TAG" >/dev/null 2>&1; then - gh api --method POST "repos/$GITHUB_REPOSITORY/git/refs" \ - -f ref="refs/tags/$RELEASE_TAG" -f sha="$RELEASE_COMMIT" >/dev/null - fi - - - name: Start or resume repository-owned publication - env: - GH_TOKEN: ${{ github.token }} - PLAN_TAG: ${{ needs.discover.outputs.plan_tag }} - RELEASE_TAG: ${{ needs.discover.outputs.version }} - RELEASE_COMMIT: ${{ needs.discover.outputs.commit }} - run: | - set -euo pipefail - decision= - for attempt in 1 2 3 4 5 6; do - gh run list --workflow publish.yml --event workflow_dispatch --branch main --limit 100 \ - --json databaseId,displayTitle,headBranch,headSha,status,conclusion \ - > publication-runs.json - decision="$(python scripts/ci/component-release-recovery.py select-publication-run \ - --release-tag "$RELEASE_TAG" --release-commit "$RELEASE_COMMIT" \ - --release-plan "$PLAN_TAG" \ - --runs publication-runs.json)" - IFS=$'\t' read -r publication_action run_id status conclusion <<< "$decision" - if [ "$publication_action" != dispatch ]; then - break - fi - if [ "$attempt" -lt 6 ]; then - sleep 5 - fi - done - if [ "$publication_action" = dispatch ]; then - gh workflow run publish.yml --ref main \ - -f release_tag="$RELEASE_TAG" -f release_commit="$RELEASE_COMMIT" \ - -f release_plan="$PLAN_TAG" -f publish=true - else - printf 'Durable publication run %s is %s/%s; no duplicate dispatch is needed.\n' \ - "$run_id" "$status" "${conclusion:-pending}" - fi - - - name: Retain publication evidence - if: always() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 - with: - name: sdk-python-release-publication-${{ needs.discover.outputs.plan }} - path: | - publication-runs.json - if-no-files-found: warn diff --git a/scripts/ci/cli-release-plan-recovery.fixture.yml b/scripts/ci/cli-release-plan-recovery.fixture.yml deleted file mode 100644 index 67993e1..0000000 --- a/scripts/ci/cli-release-plan-recovery.fixture.yml +++ /dev/null @@ -1,333 +0,0 @@ -name: Release plan recovery - -run-name: Recover CLI from ${{ inputs.plan_tag || 'latest public release plan' }} - -on: - schedule: - - cron: '41 * * * *' - workflow_dispatch: - inputs: - plan_tag: - description: Immutable release-plan tag; empty selects the newest public plan - required: false - type: string - default: '' - -permissions: - contents: read - -concurrency: - group: release-plan-recovery-cli-${{ inputs.plan_tag || 'latest' }} - cancel-in-progress: false - -jobs: - discover: - name: Discover exact CLI release - if: >- - github.event_name != 'workflow_dispatch' || - github.ref == 'refs/heads/main' - runs-on: ubuntu-latest - timeout-minutes: 10 - permissions: - attestations: read - contents: read - outputs: - action: ${{ steps.recovery.outputs.action }} - plan: ${{ steps.recovery.outputs.plan }} - plan_tag: ${{ steps.recovery.outputs.plan_tag }} - version: ${{ steps.recovery.outputs.version }} - commit: ${{ steps.recovery.outputs.commit }} - steps: - - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - with: - persist-credentials: false - - - name: Discover plan and verify upstream public artifacts - id: recovery - env: - GITHUB_TOKEN: ${{ github.token }} - REQUESTED_PLAN_TAG: ${{ inputs.plan_tag }} - run: | - arguments=( - resolve - --component cli - --plan-output release-plan.json - --preparation-output release-preparation.json - --evidence release-recovery-evidence.json - --github-output "$GITHUB_OUTPUT" - ) - if [ "$GITHUB_EVENT_NAME" = schedule ]; then - arguments+=(--allow-empty) - elif [ -n "$REQUESTED_PLAN_TAG" ]; then - arguments+=(--plan-tag "$REQUESTED_PLAN_TAG") - fi - python scripts/ci/component-release-recovery.py "${arguments[@]}" - - - name: Retain recovery evidence - if: always() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 - with: - name: cli-release-recovery-${{ steps.recovery.outputs.plan || github.run_id }} - path: | - release-plan.json - release-preparation.json - release-recovery-evidence.json - if-no-files-found: warn - - publish: - name: Publish exact CLI release - needs: discover - if: >- - github.ref == 'refs/heads/main' && - needs.discover.outputs.action == 'publish' - runs-on: ubuntu-latest - timeout-minutes: 45 - environment: release-plan-publication - permissions: - actions: write - attestations: read - contents: read - steps: - - name: Require repository publication credential - env: - CLI_RELEASE_DEPLOY_KEY: ${{ secrets.CLI_RELEASE_DEPLOY_KEY }} - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - if [ -z "$CLI_RELEASE_DEPLOY_KEY" ]; then - printf 'The release-plan-publication environment has no repository write deploy key.\n' >&2 - exit 1 - fi - if [ -z "$GH_TOKEN" ]; then - printf 'The protected publication job has no GitHub Actions credential.\n' >&2 - exit 1 - fi - - - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - with: - fetch-depth: 0 - ssh-key: ${{ secrets.CLI_RELEASE_DEPLOY_KEY }} - - - name: Restore the immutable release plan - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 - with: - name: cli-release-recovery-${{ needs.discover.outputs.plan }} - path: recovery-input - - - name: Create or verify the exact planned source tag - env: - PLAN_TAG: ${{ needs.discover.outputs.plan_tag }} - RELEASE_TAG: ${{ needs.discover.outputs.version }} - RELEASE_COMMIT: ${{ needs.discover.outputs.commit }} - run: | - python scripts/ci/publish-planned-tag.py \ - --tag "$RELEASE_TAG" --commit "$RELEASE_COMMIT" --plan-tag "$PLAN_TAG" \ - --plan recovery-input/release-plan.json \ - --evidence release-tag-publication-evidence.json - - - name: Verify the protected source tag through GitHub - env: - GH_TOKEN: ${{ github.token }} - RELEASE_TAG: ${{ needs.discover.outputs.version }} - RELEASE_COMMIT: ${{ needs.discover.outputs.commit }} - run: scripts/ci/verify-release-tag-source.sh - - - name: Quarantine the exact tag-triggered publication run - env: - GH_TOKEN: ${{ github.token }} - RELEASE_TAG: ${{ needs.discover.outputs.version }} - RELEASE_COMMIT: ${{ needs.discover.outputs.commit }} - run: | - set -euo pipefail - cancel_requested=false - quarantined=false - retained_run_id= - for attempt in {1..12}; do - gh run list --workflow release.yml --event push --branch "$RELEASE_TAG" --limit 100 \ - --json databaseId,event,displayTitle,headBranch,headSha,status,conclusion,url,workflowName \ - > tag-push-runs.json - decision="$(python scripts/ci/component-release-recovery.py select-tag-push-run \ - --release-tag "$RELEASE_TAG" --release-commit "$RELEASE_COMMIT" \ - --runs tag-push-runs.json)" - IFS=$'\t' read -r quarantine_action run_id status conclusion <<< "$decision" - if [ -n "$run_id" ]; then - if [ -n "$retained_run_id" ] && [ "$retained_run_id" != "$run_id" ]; then - printf 'Tag-push quarantine changed from run %s to run %s.\n' \ - "$retained_run_id" "$run_id" >&2 - exit 1 - fi - retained_run_id="$run_id" - fi - if [ "$quarantine_action" = cancel ] && [ "$cancel_requested" != true ]; then - gh run cancel "$run_id" - cancel_requested=true - elif [ "$quarantine_action" = complete ]; then - quarantined=true - break - fi - [ "$attempt" -eq 12 ] || sleep 5 - done - if [ "$quarantined" != true ] || [ -z "$retained_run_id" ]; then - printf 'The exact tag-push publication run was not observed and cancelled.\n' >&2 - exit 1 - fi - gh run view "$retained_run_id" \ - --json databaseId,event,displayTitle,headBranch,headSha,status,conclusion,url,workflowName \ - > tag-push-run.json - python scripts/ci/component-release-recovery.py retain-tag-push-run \ - --repository "$GITHUB_REPOSITORY" \ - --release-tag "$RELEASE_TAG" --release-commit "$RELEASE_COMMIT" \ - --run-id "$retained_run_id" --run tag-push-run.json \ - --evidence release-tag-push-quarantine-evidence.json - - - - name: Start or resume the exact repository-owned publication run - env: - GH_TOKEN: ${{ github.token }} - PLAN_TAG: ${{ needs.discover.outputs.plan_tag }} - RELEASE_TAG: ${{ needs.discover.outputs.version }} - RELEASE_COMMIT: ${{ needs.discover.outputs.commit }} - run: | - set -euo pipefail - if [ -z "$GH_TOKEN" ]; then - printf 'The protected publication job has no GitHub Actions credential.\n' >&2 - exit 1 - fi - publication_title="Release ${RELEASE_TAG} for ${PLAN_TAG}" - - select_publication_run() { - gh run list --workflow release.yml --event workflow_dispatch --branch main --limit 100 \ - --json databaseId,event,displayTitle,headBranch,headSha,status,conclusion,url \ - > publication-runs.json - python scripts/ci/component-release-recovery.py select-publication-run \ - --release-tag "$RELEASE_TAG" --release-commit "$RELEASE_COMMIT" \ - --required-event workflow_dispatch --required-head-branch main \ - --required-display-title "$publication_title" --runs publication-runs.json - } - - retain_publication_run() { - gh run view "$run_id" \ - --json databaseId,event,displayTitle,headBranch,headSha,status,conclusion,url,workflowName \ - > publication-run.json - arguments=( - retain-publication-run - --repository "$GITHUB_REPOSITORY" - --release-tag "$RELEASE_TAG" - --release-commit "$RELEASE_COMMIT" - --control-ref main - --display-title "$publication_title" - --run-id "$run_id" - --run publication-run.json - --evidence release-publication-run-evidence.json - ) - if [ "${1:-}" = resumed ]; then - arguments+=(--reject-completed-failure) - elif [ "${1:-}" = success ]; then - arguments+=(--require-success) - fi - python scripts/ci/component-release-recovery.py "${arguments[@]}" - } - - decision="$(select_publication_run)" - IFS=$'\t' read -r publication_action run_id status conclusion <<< "$decision" - if [ "$publication_action" = dispatch ]; then - python - "$RELEASE_TAG" "$RELEASE_COMMIT" "$PLAN_TAG" > publication-dispatch-request.json <<'PY' - import json - import sys - - tag, commit, plan = sys.argv[1:] - json.dump( - { - "ref": "main", - "inputs": {"tag": tag, "release_commit": commit, "release_plan": plan}, - }, - sys.stdout, - separators=(",", ":"), - ) - PY - gh api --method POST \ - -H 'X-GitHub-Api-Version: 2026-03-10' \ - "repos/$GITHUB_REPOSITORY/actions/workflows/release.yml/dispatches" \ - --input publication-dispatch-request.json > publication-dispatch.json - run_id="$(python scripts/ci/component-release-recovery.py validate-dispatch-response \ - --repository "$GITHUB_REPOSITORY" --response publication-dispatch.json)" - publication_action=wait - elif [ "$publication_action" = rerun ]; then - retain_publication_run - gh run rerun "$run_id" - publication_action=wait - fi - - retained=false - for attempt in {1..12}; do - if retain_publication_run resumed; then - retained=true - break - fi - [ "$attempt" -eq 12 ] || sleep 5 - done - if [ "$retained" != true ]; then - printf 'Exact publication run %s did not become observable with the planned identity.\n' \ - "$run_id" >&2 - exit 1 - fi - - if [ "$publication_action" = wait ]; then - gh run watch "$run_id" --exit-status --interval 10 - else - printf 'Exact publication run %s is already completed successfully.\n' "$run_id" - fi - retain_publication_run success - - - name: Retain publication evidence - if: always() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 - with: - name: cli-release-publication-${{ needs.discover.outputs.plan }} - path: | - release-tag-publication-evidence.json - tag-push-runs.json - tag-push-run.json - release-tag-push-quarantine-evidence.json - publication-runs.json - publication-dispatch-request.json - publication-dispatch.json - publication-run.json - release-publication-run-evidence.json - if-no-files-found: warn - - verify-publication: - name: Verify installable public CLI artifacts - needs: [discover, publish] - if: needs.discover.outputs.action == 'publish' && needs.publish.result == 'success' - runs-on: ubuntu-latest - timeout-minutes: 10 - permissions: - attestations: read - contents: read - steps: - - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - with: - persist-credentials: false - - - name: Restore the immutable release plan - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 - with: - name: cli-release-recovery-${{ needs.discover.outputs.plan }} - path: recovery-input - - - name: Verify installable public CLI artifacts - env: - GITHUB_TOKEN: ${{ github.token }} - run: | - python scripts/ci/component-release-recovery.py verify \ - --component cli --plan recovery-input/release-plan.json \ - --attempts 6 --sleep 10 --evidence release-completion-evidence.json - - - name: Retain installability evidence - if: always() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 - with: - name: cli-release-verification-${{ needs.discover.outputs.plan }} - path: release-completion-evidence.json - if-no-files-found: warn diff --git a/scripts/ci/cli_release_verifier_contract.py b/scripts/ci/cli_release_verifier_contract.py deleted file mode 100644 index ec5f690..0000000 --- a/scripts/ci/cli_release_verifier_contract.py +++ /dev/null @@ -1,398 +0,0 @@ -#!/usr/bin/env python3 -"""Shared focused contracts for CLI recovery provenance verification.""" - -from __future__ import annotations - -import hashlib -import importlib.util -import sys -import unittest -from pathlib import Path -from unittest import mock - -RECOVERY_SCRIPT = Path(__file__).with_name("component-release-recovery.py") -CLI_WORKFLOW_FIXTURE = Path(__file__).with_name("cli-release-plan-recovery.fixture.yml") -CURRENT_CLI_RECOVERY_WORKFLOW = CLI_WORKFLOW_FIXTURE.read_text() - - -def load_recovery_module(): - spec = importlib.util.spec_from_file_location("component_release_recovery_cli_contract", RECOVERY_SCRIPT) - assert spec is not None and spec.loader is not None - module = importlib.util.module_from_spec(spec) - sys.modules[spec.name] = module - spec.loader.exec_module(module) - return module - - -class CliRecoveryWorkflowSourceTest(unittest.TestCase): - @classmethod - def setUpClass(cls) -> None: - cls.recovery = load_recovery_module() - - def test_cli_workflow_fixture_matches_the_pinned_protected_authority(self) -> None: - digest = hashlib.sha256(CURRENT_CLI_RECOVERY_WORKFLOW.encode("utf-8")).hexdigest() - self.recovery.verify_recovery_workflow_source("cli", CURRENT_CLI_RECOVERY_WORKFLOW, digest) - self.recovery.verify_recovery_workflow_source( - "cli", - CURRENT_CLI_RECOVERY_WORKFLOW.replace("\n", "\r\n"), - digest, - ) - - def test_cli_workflow_pin_rejects_any_source_mutation(self) -> None: - mutated = CURRENT_CLI_RECOVERY_WORKFLOW.replace("timeout-minutes: 45", "timeout-minutes: 44", 1) - self.assertNotEqual(CURRENT_CLI_RECOVERY_WORKFLOW, mutated) - with self.assertRaises(self.recovery.RecoveryError) as caught: - self.recovery.verify_recovery_workflow_source( - "cli", - mutated, - hashlib.sha256(CURRENT_CLI_RECOVERY_WORKFLOW.encode("utf-8")).hexdigest(), - ) - self.assertEqual("default-branch-preflight", caught.exception.phase) - - def test_artifact_execution_jobs_cannot_retain_checkout_credentials(self) -> None: - def job_source(name: str) -> str: - lines = CURRENT_CLI_RECOVERY_WORKFLOW.splitlines() - start = lines.index(f" {name}:") + 1 - end = next( - ( - index - for index, line in enumerate(lines[start:], start=start) - if line.startswith(" ") and not line.startswith(" ") - ), - len(lines), - ) - return "\n".join(lines[start:end]) - - non_persistent_checkout = """ - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - with: - persist-credentials: false""" - discover_job = job_source("discover") - verification_job = job_source("verify-publication") - self.assertIn(non_persistent_checkout, discover_job) - self.assertIn("resolve\n --component cli", discover_job) - self.assertIn(non_persistent_checkout, verification_job) - self.assertIn("component-release-recovery.py verify", verification_job) - - -class CliReleaseAuthorityTest(unittest.TestCase): - @classmethod - def setUpClass(cls) -> None: - cls.recovery = load_recovery_module() - - def release_client(self, version: str) -> mock.Mock: - client = mock.Mock() - assets = [ - { - "id": index, - "name": name, - "browser_download_url": f"https://example.invalid/{name}", - } - for index, name in enumerate(sorted(self.recovery.CLI_ASSETS), start=1) - ] - client.json.return_value = { - "id": 94, - "html_url": f"https://github.com/durable-workflow/cli/releases/tag/{version}", - "tag_name": version, - "draft": False, - "assets": assets, - } - client.bytes.return_value = "".join( - f"{'a' * 64} {name}\n" for name in sorted(self.recovery.CLI_ASSETS - {"SHA256SUMS"}) - ).encode() - - def download(_url: str, path: Path, *, expected_sha256: str | None = None) -> dict[str, object]: - path.write_bytes(b"artifact") - return {"url": str(path), "size": 8, "sha256": expected_sha256} - - client.download.side_effect = download - return client - - def test_qualified_main_authority_is_observed_without_a_version_gate(self) -> None: - version = "0.1.93" - commit = "3fcc580000000000000000000000000000000000" - calls: list[list[str]] = [] - - def run(arguments: list[str], **_kwargs: object) -> object: - calls.append(arguments) - if arguments[0] == "php": - return mock.Mock( - returncode=0, - stdout=f"dw {version} (commit {commit[:12]}, built 2026-07-20)", - stderr="", - ) - if "--source-digest" in arguments: - return mock.Mock(returncode=1, stdout="", stderr="no exact-tag attestation") - return mock.Mock(returncode=0, stdout="verified", stderr="") - - with ( - mock.patch.object(self.recovery.shutil, "which", return_value="/usr/bin/tool"), - mock.patch.object(self.recovery.subprocess, "run", side_effect=run), - ): - evidence = self.recovery.verify_cli( - self.release_client(version), - self.recovery.COMPONENTS["cli"], - version, - commit, - ) - - attestations = [arguments for arguments in calls if arguments[:3] == ["gh", "attestation", "verify"]] - main_attestations = [arguments for arguments in attestations if "--signer-workflow" in arguments] - self.assertEqual(len(self.recovery.CLI_ASSETS), len(main_attestations)) - self.assertEqual(len(self.recovery.CLI_ASSETS) + 1, len(attestations)) - self.assertEqual("php", calls[-1][0]) - self.assertTrue(all(arguments[0] == "gh" for arguments in calls[:-1])) - self.assertEqual("qualified-main-workflow", evidence["build_attestation_authority"]["mode"]) - self.assertEqual("refs/heads/main", evidence["build_attestation_authority"]["ref"]) - self.assertEqual(commit, evidence["package_source"]["commit"]) - for arguments in main_attestations: - self.assertIn("durable-workflow/cli/.github/workflows/release.yml", arguments) - self.assertNotIn("--source-digest", arguments) - - def test_future_ordinary_release_uses_observed_exact_tag_authority(self) -> None: - version = "0.1.95" - commit = "4" * 40 - calls: list[list[str]] = [] - - def run(arguments: list[str], **_kwargs: object) -> object: - calls.append(arguments) - if arguments[0] == "php": - return mock.Mock( - returncode=0, - stdout=f"dw {version} (commit {commit[:12]}, built 2026-07-21)", - stderr="", - ) - return mock.Mock(returncode=0, stdout="verified", stderr="") - - with ( - mock.patch.object(self.recovery.shutil, "which", return_value="/usr/bin/tool"), - mock.patch.object(self.recovery.subprocess, "run", side_effect=run), - ): - evidence = self.recovery.verify_cli( - self.release_client(version), - self.recovery.COMPONENTS["cli"], - version, - commit, - ) - - attestations = [arguments for arguments in calls if arguments[:3] == ["gh", "attestation", "verify"]] - self.assertEqual(len(self.recovery.CLI_ASSETS), len(attestations)) - self.assertEqual("php", calls[-1][0]) - self.assertEqual("exact-tag", evidence["build_attestation_authority"]["mode"]) - self.assertEqual(f"refs/tags/{version}", evidence["build_attestation_authority"]["ref"]) - self.assertEqual(commit, evidence["build_attestation_authority"]["commit"]) - for arguments in attestations: - self.assertIn("--source-digest", arguments) - self.assertIn(commit, arguments) - self.assertNotIn("--signer-workflow", arguments) - - def test_mixed_asset_authorities_are_rejected_before_phar_execution(self) -> None: - version = "0.1.94" - commit = "36bde75882980e834854a145c9ad0f61ceec4659" - attestation_count = 0 - calls: list[list[str]] = [] - - def run(arguments: list[str], **_kwargs: object) -> object: - nonlocal attestation_count - calls.append(arguments) - if arguments[0] == "php": - self.fail("the PHAR executed before the complete asset set was authenticated") - if "--signer-workflow" in arguments: - return mock.Mock(returncode=0, stdout="verified under main", stderr="") - attestation_count += 1 - return mock.Mock( - returncode=0 if attestation_count == 1 else 1, - stdout="verified" if attestation_count == 1 else "", - stderr="authority differs" if attestation_count > 1 else "", - ) - - with ( - mock.patch.object(self.recovery.shutil, "which", return_value="/usr/bin/tool"), - mock.patch.object(self.recovery.subprocess, "run", side_effect=run), - self.assertRaisesRegex(self.recovery.RecoveryError, "exact-tag: authority differs"), - ): - self.recovery.verify_cli( - self.release_client(version), - self.recovery.COMPONENTS["cli"], - version, - commit, - ) - - self.assertEqual(2, attestation_count) - self.assertFalse(any(arguments[0] == "php" for arguments in calls)) - self.assertFalse(any("--signer-workflow" in arguments for arguments in calls)) - - def test_missing_attestations_fail_before_phar_execution(self) -> None: - version = "0.1.94" - commit = "36bde75882980e834854a145c9ad0f61ceec4659" - calls: list[list[str]] = [] - - def run(arguments: list[str], **_kwargs: object) -> object: - calls.append(arguments) - if arguments[0] == "php": - self.fail("the PHAR executed without a build attestation") - return mock.Mock(returncode=1, stdout="", stderr="attestation missing") - - with ( - mock.patch.object(self.recovery.shutil, "which", return_value="/usr/bin/tool"), - mock.patch.object(self.recovery.subprocess, "run", side_effect=run), - self.assertRaisesRegex(self.recovery.RecoveryError, "attestation missing"), - ): - self.recovery.verify_cli( - self.release_client(version), - self.recovery.COMPONENTS["cli"], - version, - commit, - ) - - self.assertEqual(2, len(calls)) - self.assertTrue(all(arguments[0] == "gh" for arguments in calls)) - - def test_embedded_planned_commit_is_enforced_after_provenance(self) -> None: - version = "0.1.94" - commit = "36bde75882980e834854a145c9ad0f61ceec4659" - calls: list[list[str]] = [] - - def run(arguments: list[str], **_kwargs: object) -> object: - calls.append(arguments) - if arguments[0] == "php": - return mock.Mock( - returncode=0, - stdout=f"dw {version} (commit {'f' * 12}, built 2026-07-20)", - stderr="", - ) - return mock.Mock(returncode=0, stdout="verified", stderr="") - - with ( - mock.patch.object(self.recovery.shutil, "which", return_value="/usr/bin/tool"), - mock.patch.object(self.recovery.subprocess, "run", side_effect=run), - self.assertRaisesRegex(self.recovery.RecoveryError, "does not embed planned source commit"), - ): - self.recovery.verify_cli( - self.release_client(version), - self.recovery.COMPONENTS["cli"], - version, - commit, - ) - - attestations = [arguments for arguments in calls if arguments[0] == "gh"] - self.assertEqual(len(self.recovery.CLI_ASSETS), len(attestations)) - self.assertEqual("php", calls[-1][0]) - - def test_phar_execution_receives_no_workflow_credentials_or_other_secrets(self) -> None: - version = "0.1.94" - commit = "36bde75882980e834854a145c9ad0f61ceec4659" - phar_environment: dict[object, object] | None = None - - def run(arguments: list[str], **kwargs: object) -> object: - nonlocal phar_environment - if Path(arguments[0]).name == "php": - environment = kwargs.get("env") - if isinstance(environment, dict): - phar_environment = environment - return mock.Mock( - returncode=0, - stdout=f"dw {version} (commit {commit[:12]}, built 2026-07-20)", - stderr="", - ) - return mock.Mock(returncode=0, stdout="verified", stderr="") - - inherited_secrets = { - "GITHUB_TOKEN": "github-token", - "GH_TOKEN": "gh-token", - "AWS_SECRET_ACCESS_KEY": "cloud-secret", - "DATABASE_URL": "postgres://user:password@example.invalid/database", - } - allowed_path = "/opt/php/bin:/usr/bin" - with ( - mock.patch.dict(self.recovery.os.environ, {**inherited_secrets, "PATH": allowed_path}, clear=False), - mock.patch.object(self.recovery.shutil, "which", return_value="/usr/bin/tool"), - mock.patch.object(self.recovery.subprocess, "run", side_effect=run), - ): - self.recovery.verify_cli( - self.release_client(version), - self.recovery.COMPONENTS["cli"], - version, - commit, - ) - - self.assertEqual({"PATH": allowed_path}, phar_environment) - self.assertTrue(inherited_secrets.keys().isdisjoint(phar_environment or {})) - - def test_completed_plan_skip_executes_phar_with_the_isolated_environment(self) -> None: - version = "0.1.94" - commit = "36bde75882980e834854a145c9ad0f61ceec4659" - plan = { - "plan": "completed-cli-recovery", - "channel": "alpha", - "components": { - "server": {"version": "1.0.0", "commit": "a" * 40}, - "cli": {"version": version, "commit": commit}, - }, - } - phar_environment: dict[object, object] | None = None - - def run(arguments: list[str], **kwargs: object) -> object: - nonlocal phar_environment - if Path(arguments[0]).name == "php": - environment = kwargs.get("env") - if isinstance(environment, dict): - phar_environment = environment - return mock.Mock( - returncode=0, - stdout=f"dw {version} (commit {commit[:12]}, built 2026-07-20)", - stderr="", - ) - return mock.Mock(returncode=0, stdout="verified", stderr="") - - client = self.release_client(version) - - def verify_completed_component( - verifier_client: mock.Mock, - component_name: str, - identity: dict[str, str], - ) -> dict[str, object]: - if component_name == "server": - return {"version": identity["version"], "commit": identity["commit"]} - self.assertEqual("cli", component_name) - distribution = self.recovery.verify_cli( - verifier_client, - self.recovery.COMPONENTS[component_name], - identity["version"], - identity["commit"], - ) - return { - "version": identity["version"], - "commit": identity["commit"], - "distribution": distribution, - "github_release": distribution, - } - - allowed_path = "/opt/php/bin:/usr/bin" - with ( - mock.patch.dict( - self.recovery.os.environ, - { - "GITHUB_TOKEN": "checkout-token", - "RUNNER_TEMP": "/runner/temp", - "PATH": allowed_path, - }, - clear=False, - ), - mock.patch.object(self.recovery, "verify_plan_authority", return_value=({}, {})), - mock.patch.object(self.recovery, "resolve_tag", return_value=commit), - mock.patch.object(self.recovery, "verify_component", side_effect=verify_completed_component), - mock.patch.object(self.recovery.shutil, "which", return_value="/usr/bin/tool"), - mock.patch.object(self.recovery.subprocess, "run", side_effect=run), - ): - state, outputs = self.recovery.resolve_component( - client, - "cli", - "release-plan/completed-cli-recovery", - "b" * 40, - plan, - None, - ) - - self.assertEqual("skip", outputs["action"]) - self.assertEqual("complete", state["phase"]) - self.assertEqual({"PATH": allowed_path}, phar_environment) diff --git a/scripts/ci/component-release-recovery.py b/scripts/ci/component-release-recovery.py deleted file mode 100644 index 79a570c..0000000 --- a/scripts/ci/component-release-recovery.py +++ /dev/null @@ -1,3616 +0,0 @@ -#!/usr/bin/env python3 -"""Discover and classify one repository's work for an immutable release plan.""" - -from __future__ import annotations - -import argparse -import contextlib -import datetime as dt -import email.utils -import errno -import hashlib -import http.client -import io -import json -import os -import re -import shutil -import ssl -import subprocess -import sys -import tarfile -import tempfile -import time -import urllib.error -import urllib.parse -import urllib.request -import zipfile -from collections.abc import Callable, Mapping -from dataclasses import dataclass -from pathlib import Path -from typing import Any - -import tomllib -from recovery_workflow_authority import ( - RecoveryWorkflowAuthorityError, - load_qualified_authority, - verify_workflow_source, -) - -SCHEMA = "durable-workflow.release-plan/v2" -LEGACY_SCHEMA = "durable-workflow.release-plan/v1" -LEGACY_PLAN_DIGESTS = frozenset( - { - "0be354d5ea603170b6aef8ae0d9861886c4ccc0f75e6acb763239b30dd5d8ba3", - "295a3f654716ea8cd8dc693c1cd15a4b487737e5f01184bad7363fbde6717c40", - "486d9ef7c5a7f4443a89566cab33d7f2bccc518254ab6698d918a431d6a1c9ce", - "498804a2c7fd5b0e34f93ef080bea3073bc98e420e8bf84a98ca4cdb94729973", - "7bd737c92f139eec33026bc88a6491dc635d819a87a61c985e14e06aca645582", - "80e88698fa37b6d738d111dd2be3e3c145607973f8147c54cc25e5d91d415b17", - "9c0a5879652a2d5f4806a9167399687328c1764fa10dbc8d76215b43ac83b9d6", - "db90616c98f305c61d7eb2fb9ed03cc28f06963e9ca020c8ef6d7c6a8557f7bc", - "e1fc6e20c9d2ded0b5e7ac4d6be75ba861d31fc4b2db651dc0272dca623f2c7f", - } -) -PREPARATION_SCHEMA = "durable-workflow.release-preparation/v1" -STATE_SCHEMA = "durable-workflow.component-release-recovery/v1" -CONTROL_REPOSITORY = "durable-workflow/.github" -PLAN_TAG_PREFIX = "release-plan/" -COMPLETION_TAG_PREFIX = "release-candidate/" -FAILURE_TAG_PREFIX = "release-plan-failure/" -CONTINUITY_TAG_PREFIX = "beta-continuity/" -CONTINUITY_EVIDENCE_SCHEMA = "durable-workflow.beta-continuity.evidence/v1" -CONTINUITY_SUPERSESSION_REASON = "missing-post-acceptance-publication-trigger" -CONTINUITY_RESOLUTION_TAG_PREFIX = "release-plan-continuity-resolution/" -CONTINUITY_RESOLUTION_SCHEMA = "durable-workflow.release-plan-continuity-resolution/v2" -CONTINUITY_RESOLUTION_QUALIFICATION_WORKFLOW = ".github/workflows/beta-candidate.yml" -CONTINUITY_RESOLUTION_QUALIFICATION_EVENT = "push" -CONTINUITY_RESOLUTION_QUALIFICATION_BRANCH = "main" -SUPERSESSION_ENVIRONMENT = "release-plan-supersession" -SUPERSESSION_WORKFLOW = ".github/workflows/release-plan-supersession.yml" -SUPERSESSION_API_VERSION = "2026-03-10" -SUPERSESSION_REASON = "published-version-source-conflict" -SOURCE_MANIFEST_REASON = "source-manifest-version-conflict" -OCCUPIED_SOURCE_MANIFEST_REASON = "occupied-source-manifest-version-conflict" -FOUNDATION_TAG = "beta-candidate/beta-continuity-foundation" -FOUNDATION_COMMIT = "4995052410bd4301c5796ffba54e0b6d2f490ed1" -COMMIT_PATTERN = re.compile(r"^[0-9a-f]{40}$") -SHA256_PATTERN = re.compile(r"^[0-9a-f]{64}$") -OCI_DIGEST_PATTERN = re.compile(r"^sha256:[0-9a-f]{64}$") -PLAN_PATTERN = re.compile(r"^[a-z0-9][a-z0-9._-]{0,55}$") -VERSION_PATTERN = re.compile( - r"^(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)" - r"(?:-(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)" - r"(?:\.(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*)?" - r"(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$" -) -ALPHA_VERSION_PATTERN = re.compile(r"^2\.0\.0-alpha\.[1-9][0-9]*$") -BETA_VERSION_PATTERN = re.compile(r"^2\.0\.0-beta\.[1-9][0-9]*$") -RC_VERSION_PATTERN = re.compile(r"^2\.0\.0-rc\.[1-9][0-9]*$") -MARKDOWN_MEDIA_TYPE = "text/markdown" -GITHUB_READ_MAX_ATTEMPTS = 5 -GITHUB_READ_RETRY_BASE_SECONDS = 2.0 -GITHUB_READ_RETRY_MAX_SECONDS = 120.0 -GITHUB_READ_REQUEST_TIMEOUT_SECONDS = 30.0 -GITHUB_READ_DEADLINE_SECONDS = 600.0 -IMPLICIT_AUTHORITY_MAX_ATTEMPTS = 3 -INFRASTRUCTURE_EXIT_CODE = 75 - -SOURCE_CHANGELOGS = {"workflow", "waterline", "sdk-php", "sdk-python"} -SOURCE_MANIFESTS = { - "sdk-python": {"path": "pyproject.toml", "package": "durable-workflow"}, - "sdk-rust": {"path": "Cargo.toml", "package": "durable-workflow"}, -} -SUPERSESSION_ENVIRONMENT_URL = ( - f"https://github.com/{CONTROL_REPOSITORY}/deployments/activity_log?environments_filter={SUPERSESSION_ENVIRONMENT}" -) -SUPERSESSION_ENVIRONMENT_API_URL = ( - f"https://api.github.com/repos/{CONTROL_REPOSITORY}/environments/{SUPERSESSION_ENVIRONMENT}" -) -SOURCE_MANIFEST_VERSION_CONFLICT = "source-manifest-version-conflict" -PYTHON_SOURCE_MANIFEST = {"path": "pyproject.toml", "package": "durable-workflow"} - - -@dataclass(frozen=True) -class Component: - repository: str - default_branch: str - distribution: str - package: str - dependencies: tuple[str, ...] - release_workflow: str | None - release_tag_input: str | None - - -COMPONENTS = { - "workflow": Component("durable-workflow/workflow", "v2", "composer", "durable-workflow/workflow", (), None, None), - "sdk-php": Component("durable-workflow/sdk-php", "main", "composer", "durable-workflow/sdk", (), None, None), - "waterline": Component( - "durable-workflow/waterline", - "v2", - "composer", - "durable-workflow/waterline", - ("workflow", "sdk-php"), - None, - None, - ), - "server": Component( - "durable-workflow/server", - "main", - "oci", - "docker.io/durableworkflow/server", - ("workflow",), - "release.yml", - "tag", - ), - "cli": Component( - "durable-workflow/cli", "main", "github-release", "durable-workflow/cli", ("server",), "release.yml", "tag" - ), - "sdk-python": Component( - "durable-workflow/sdk-python", - "main", - "pypi", - "durable-workflow", - ("server",), - "publish.yml", - "release_tag", - ), - "sdk-rust": Component( - "durable-workflow/sdk-rust", - "main", - "crates.io", - "durable-workflow", - ("server",), - "release.yml", - "release_tag", - ), -} - -CLI_ASSETS = { - "dw.phar", - "dw-linux-x86_64", - "dw-linux-aarch64", - "dw-macos-aarch64", - "dw-windows-x86_64.exe", - "dw.rb", - "install.sh", - "install.ps1", - "verify-release.sh", - "SHA256SUMS", -} - - -class RecoveryError(RuntimeError): - """A release plan cannot safely advance.""" - - def __init__( - self, - message: str, - phase: str = "preflight", - *, - evidence: dict[str, Any] | None = None, - resume_action: str | None = None, - ) -> None: - super().__init__(message) - self.phase = phase - self.evidence = evidence - self.resume_action = resume_action - - -class NotFound(RecoveryError): - """A public API resource is absent.""" - - -class PublicInfrastructureError(RuntimeError): - """A bounded set of transient GitHub public-read attempts was exhausted.""" - - def __init__( - self, - endpoint_class: str, - attempts: int, - *, - reason: str, - failure: str | None = None, - ) -> None: - self.evidence: dict[str, str | int] = { - "classification": "github-read-transient", - "endpoint_class": endpoint_class, - "attempts": attempts, - "reason": reason, - } - if failure is not None: - self.evidence["failure"] = failure - evidence = [ - f"{key}={value}" - for key, value in self.evidence.items() - if key != "failure" - ] - if failure is not None: - evidence.append(failure) - super().__init__(f"GitHub public read transient failure exhausted ({', '.join(evidence)})") - - -class _TransientGitHubRead(RuntimeError): - """One GitHub public-read attempt encountered retryable infrastructure.""" - - def __init__(self, evidence: str, headers: Mapping[str, str] | None = None) -> None: - self.evidence = evidence - self.headers = headers or {} - super().__init__(evidence) - - -class _GitHubCliResponse(io.BytesIO): - """A response-shaped wrapper around one GitHub CLI API result.""" - - def __init__(self, body: bytes, headers: Mapping[str, str]) -> None: - super().__init__(body) - self.headers = headers - - -def canonical_json(value: Any) -> bytes: - return (json.dumps(value, indent=2, sort_keys=True, ensure_ascii=True) + "\n").encode() - - -class PublicClient: - def __init__( - self, - token: str | None = None, - *, - max_attempts: int = GITHUB_READ_MAX_ATTEMPTS, - retry_base_seconds: float = GITHUB_READ_RETRY_BASE_SECONDS, - retry_max_seconds: float = GITHUB_READ_RETRY_MAX_SECONDS, - request_timeout_seconds: float = GITHUB_READ_REQUEST_TIMEOUT_SECONDS, - deadline_seconds: float = GITHUB_READ_DEADLINE_SECONDS, - sleep: Callable[[float], None] = time.sleep, - now: Callable[[], float] = time.time, - monotonic: Callable[[], float] = time.monotonic, - ) -> None: - if ( - max_attempts < 1 - or retry_base_seconds < 0 - or retry_max_seconds < retry_base_seconds - or request_timeout_seconds <= 0 - or deadline_seconds <= 0 - ): - raise ValueError("invalid GitHub public-read retry configuration") - self.token = token - self.max_attempts = max_attempts - self.retry_base_seconds = retry_base_seconds - self.retry_max_seconds = retry_max_seconds - self.request_timeout_seconds = request_timeout_seconds - self.sleep = sleep - self.now = now - self.monotonic = monotonic - self.deadline = monotonic() + deadline_seconds - - @staticmethod - def _github_endpoint_class(url: str) -> str | None: - parsed = urllib.parse.urlsplit(url) - host = (parsed.hostname or "").lower() - if host == "api.github.com": - path = parsed.path - endpoint_classes = ( - ("/releases", "releases-api"), - ("/git/", "git-api"), - ("/contents/", "contents-api"), - ("/commits/", "commits-api"), - ("/actions/", "actions-api"), - ("/environments/", "environments-api"), - ) - for marker, endpoint_class in endpoint_classes: - if marker in path: - return endpoint_class - if path.startswith("/users/"): - return "users-api" - return "repositories-api" - if host == "github.com" or host.endswith(".github.com") or host.endswith(".githubusercontent.com"): - return "github-download" - return None - - @staticmethod - def _error_detail(error: urllib.error.HTTPError) -> str: - try: - return error.read(1024).decode(errors="replace") - except OSError: - return "response body unavailable" - - @staticmethod - def _header_value(headers: Mapping[str, str], name: str) -> str | None: - normalized_name = name.casefold() - return next( - (value for header_name, value in headers.items() if header_name.casefold() == normalized_name), - None, - ) - - @classmethod - def _is_rate_limited(cls, error: urllib.error.HTTPError, detail: str) -> bool: - headers = error.headers or {} - return error.code == 429 or ( - error.code == 403 - and ( - cls._header_value(headers, "Retry-After") is not None - or cls._header_value(headers, "X-RateLimit-Remaining") == "0" - or "rate limit" in detail.lower() - ) - ) - - @staticmethod - def _transport_name(error: BaseException) -> str | None: - reason = error.reason if isinstance(error, urllib.error.URLError) else error - if isinstance(reason, ssl.SSLCertVerificationError): - return "tls-certificate-verification" - if isinstance( - reason, - ConnectionError | TimeoutError | http.client.IncompleteRead | http.client.RemoteDisconnected, - ): - return type(reason).__name__ - if isinstance(reason, OSError) and reason.errno in { - errno.ECONNABORTED, - errno.ECONNRESET, - errno.EPIPE, - errno.ETIMEDOUT, - }: - return type(reason).__name__ - return None - - def _server_retry_delay(self, headers: Mapping[str, str]) -> float | None: - delays: list[float] = [] - retry_after = self._header_value(headers, "Retry-After") - if retry_after: - try: - delays.append(float(retry_after)) - except ValueError: - try: - retry_at = email.utils.parsedate_to_datetime(retry_after) - except (TypeError, ValueError): - pass - else: - if retry_at.tzinfo is None: - retry_at = retry_at.replace(tzinfo=dt.UTC) - delays.append(retry_at.timestamp() - self.now()) - rate_limit_reset = self._header_value(headers, "X-RateLimit-Reset") - if rate_limit_reset: - with contextlib.suppress(ValueError): - delays.append(float(rate_limit_reset) - self.now()) - return max((delay for delay in delays if delay > 0), default=None) - - def _retry_delay(self, attempt: int, failure: _TransientGitHubRead) -> float: - backoff = min(self.retry_base_seconds * (2 ** (attempt - 1)), self.retry_max_seconds) - return max(backoff, self._server_retry_delay(failure.headers) or 0) - - def _remaining_time(self) -> float: - return self.deadline - self.monotonic() - - @staticmethod - def _parse_github_cli_response(output: bytes) -> tuple[int | None, dict[str, str], bytes]: - separator = b"\r\n\r\n" if b"\r\n\r\n" in output else b"\n\n" - head, found, body = output.partition(separator) - lines = head.replace(b"\r\n", b"\n").splitlines() - status_match = re.fullmatch(rb"HTTP/\S+ ([0-9]{3})(?: .*)?", lines[0]) if lines else None - if not found or status_match is None: - return None, {}, output - headers: dict[str, str] = {} - for line in lines[1:]: - name, present, value = line.partition(b":") - if present: - normalized = name.decode(errors="replace").strip().casefold() - headers[normalized] = value.decode(errors="replace").strip() - return int(status_match.group(1)), headers, body - - @staticmethod - def _github_cli_transport_failure(stderr: bytes) -> str | None: - detail = stderr.decode(errors="replace").lower() - if any(marker in detail for marker in ("certificate", "x509:", "tls: failed to verify")): - return "tls-certificate-verification" - transport_markers = ( - "connection refused", - "connection reset", - "connection was reset", - "i/o timeout", - "network is unreachable", - "no such host", - "temporary failure in name resolution", - "tls handshake timeout", - "unexpected eof", - ) - if any(marker in detail for marker in transport_markers): - return "github-cli-network" - return None - - def _github_cli_request( - self, - url: str, - headers: Mapping[str, str], - timeout: float, - ) -> _GitHubCliResponse: - if not self.token: - raise RecoveryError("GitHub CLI API transport requires GITHUB_TOKEN or GH_TOKEN") - parsed = urllib.parse.urlsplit(url) - if parsed.scheme != "https" or parsed.hostname != "api.github.com": - raise RecoveryError(f"GitHub CLI API transport rejected non-API URL: {url}") - endpoint = parsed.path.lstrip("/") - if parsed.query: - endpoint = f"{endpoint}?{parsed.query}" - command = ["gh", "api", "--hostname", "github.com", "--include", "--method", "GET"] - for name, value in headers.items(): - if name.lower() != "authorization": - command.extend(("--header", f"{name}: {value}")) - command.append(endpoint) - environment = os.environ.copy() - environment.update( - { - "GH_PROMPT_DISABLED": "1", - "GH_TOKEN": self.token, - "NO_COLOR": "1", - } - ) - try: - process = subprocess.run( - command, - check=False, - capture_output=True, - env=environment, - timeout=timeout, - ) - except FileNotFoundError as error: - raise RecoveryError("GitHub Actions-supported gh API transport is unavailable") from error - except subprocess.TimeoutExpired as error: - raise _TransientGitHubRead("transport=github-cli-timeout") from error - - status, response_headers, body = self._parse_github_cli_response(process.stdout) - if process.returncode != 0: - if status is None: - status_match = re.search(rb"\(HTTP ([0-9]{3})\)", process.stderr) - status = int(status_match.group(1)) if status_match else None - if status is not None: - raise urllib.error.HTTPError( - url, - status, - process.stderr.decode(errors="replace").strip(), - response_headers, - io.BytesIO(body), - ) - if transport := self._github_cli_transport_failure(process.stderr): - raise _TransientGitHubRead(f"transport={transport}") - detail = process.stderr.decode(errors="replace").strip() or "unknown GitHub CLI failure" - raise RecoveryError(f"GitHub CLI API request failed for {url}: {detail[:512]}") - if status is None or not 200 <= status <= 299: - raise RecoveryError(f"GitHub CLI API response was malformed for {url}") - return _GitHubCliResponse(body, response_headers) - - def _run( - self, - url: str, - operation: Callable[[urllib.response.addinfourl], Any], - *, - headers: dict[str, str] | None, - accept: str | None, - ) -> Any: - endpoint_class = self._github_endpoint_class(url) - attempt_limit = self.max_attempts if endpoint_class is not None else 1 - request_headers = {"User-Agent": "durable-workflow-release-recovery/1", **(headers or {})} - if accept: - request_headers["Accept"] = accept - if self.token and urllib.parse.urlsplit(url).hostname == "api.github.com": - request_headers["Authorization"] = f"Bearer {self.token}" - request_headers.setdefault("X-GitHub-Api-Version", "2022-11-28") - - for attempt in range(1, attempt_limit + 1): - if endpoint_class is not None and self._remaining_time() <= 0: - raise PublicInfrastructureError(endpoint_class, attempt - 1, reason="workflow-deadline") - timeout = min(self.request_timeout_seconds, self._remaining_time()) if endpoint_class is not None else 60 - failure: _TransientGitHubRead | None = None - try: - if ( - urllib.parse.urlsplit(url).hostname == "api.github.com" - and self.token - and os.environ.get("GITHUB_ACTIONS") == "true" - ): - response = self._github_cli_request(url, request_headers, timeout) - else: - request = urllib.request.Request(url, headers=request_headers) - response = urllib.request.urlopen(request, timeout=timeout) - result = operation(response) - if endpoint_class is not None and self._remaining_time() <= 0: - raise PublicInfrastructureError(endpoint_class, attempt, reason="workflow-deadline") - return result - except urllib.error.HTTPError as error: - detail = self._error_detail(error) - if endpoint_class is not None and (500 <= error.code <= 599 or self._is_rate_limited(error, detail)): - failure = _TransientGitHubRead(f"status={error.code}", error.headers) - elif error.code == 404: - raise NotFound(f"public resource is absent: {url}") from error - else: - raise RecoveryError(f"public request failed ({error.code}) for {url}: {detail}") from error - except (urllib.error.URLError, ConnectionError, TimeoutError, http.client.IncompleteRead) as error: - transport = self._transport_name(error) - if endpoint_class is not None and transport is not None: - failure = _TransientGitHubRead(f"transport={transport}") - else: - reason = error.reason if isinstance(error, urllib.error.URLError) else error - raise RecoveryError(f"public request failed for {url}: {reason}") from error - - except _TransientGitHubRead as error: - if endpoint_class is None: - raise RecoveryError(f"public request failed for {url}: {error}") from error - failure = error - - assert endpoint_class is not None and failure is not None - if attempt == attempt_limit: - raise PublicInfrastructureError( - endpoint_class, - attempt, - reason="retry-exhausted", - failure=failure.evidence, - ) - delay = self._retry_delay(attempt, failure) - if delay >= self._remaining_time(): - raise PublicInfrastructureError( - endpoint_class, - attempt, - reason="workflow-deadline", - failure=failure.evidence, - ) - print( - f"GitHub public read retry: endpoint_class={endpoint_class} " - f"attempt={attempt}/{attempt_limit} {failure.evidence} delay={delay:g}s", - file=sys.stderr, - ) - self.sleep(delay) - raise AssertionError("GitHub public-read retry loop ended unexpectedly") - - def request( - self, - url: str, - *, - headers: dict[str, str] | None = None, - accept: str | None = None, - ) -> urllib.response.addinfourl: - return self._run(url, lambda response: response, headers=headers, accept=accept) - - def json(self, url: str, *, headers: dict[str, str] | None = None, accept: str | None = None) -> Any: - def read_json(response: urllib.response.addinfourl) -> Any: - with response: - try: - return json.load(response) - except (json.JSONDecodeError, UnicodeDecodeError) as error: - raise RecoveryError(f"public endpoint did not return valid JSON: {url}") from error - - return self._run(url, read_json, headers=headers, accept=accept) - - def bytes(self, url: str, *, headers: dict[str, str] | None = None, accept: str | None = None) -> bytes: - def read_bytes(response: urllib.response.addinfourl) -> bytes: - with response: - return response.read() - - return self._run(url, read_bytes, headers=headers, accept=accept) - - def download(self, url: str, path: Path, *, expected_sha256: str | None = None) -> dict[str, Any]: - if expected_sha256 is not None and ( - not isinstance(expected_sha256, str) - or not re.fullmatch(r"[0-9a-fA-F]{64}", expected_sha256) - ): - raise RecoveryError(f"download for {url} has an invalid expected SHA-256") - - def download_once(response: urllib.response.addinfourl) -> tuple[str, int]: - digest = hashlib.sha256() - size = 0 - with response, path.open("wb") as destination: - while chunk := response.read(1024 * 1024): - digest.update(chunk) - destination.write(chunk) - size += len(chunk) - return digest.hexdigest(), size - - actual, size = self._run(url, download_once, headers=None, accept=None) - if expected_sha256 and actual != expected_sha256.lower(): - raise RecoveryError(f"download digest mismatch for {url}: expected {expected_sha256}, got {actual}") - return {"url": url, "size": size, "sha256": actual} - - -def validate_plan(plan: Any) -> None: - if not isinstance(plan, dict): - raise RecoveryError("release plan must be a JSON object") - expected = {"schema", "plan", "channel", "foundation", "components", "beta_authorization"} - if set(plan) != expected or plan.get("schema") not in {LEGACY_SCHEMA, SCHEMA}: - raise RecoveryError("release plan does not satisfy a supported channel-aware contract") - if not isinstance(plan["plan"], str) or not PLAN_PATTERN.fullmatch(plan["plan"]): - raise RecoveryError("release plan has an invalid identity") - if plan["channel"] not in {"alpha", "beta", "rc"}: - raise RecoveryError("release plan channel must be alpha, beta, or rc") - foundation = plan["foundation"] - legacy_foundation = {"tag": FOUNDATION_TAG, "commit": FOUNDATION_COMMIT} - aggregate_rc_foundation = ( - plan["channel"] == "rc" - and isinstance(foundation, dict) - and set(foundation) == {"tag", "commit"} - and foundation.get("tag") == f"beta-candidate/rc-{plan['plan']}" - and COMMIT_PATTERN.fullmatch(str(foundation.get("commit", ""))) is not None - ) - if foundation != legacy_foundation and not aggregate_rc_foundation: - raise RecoveryError("release plan does not name its proven immutable candidate foundation") - components = plan["components"] - if not isinstance(components, dict) or set(components) != set(COMPONENTS): - raise RecoveryError("release plan must contain the exact seven-component tuple") - for name, identity in components.items(): - if not isinstance(identity, dict) or set(identity) != {"version", "commit"}: - raise RecoveryError(f"components.{name} must contain only version and commit") - if not isinstance(identity["version"], str) or parse_semver(identity["version"]) is None: - raise RecoveryError(f"components.{name}.version is not exact SemVer") - if not isinstance(identity["commit"], str) or not COMMIT_PATTERN.fullmatch(identity["commit"]): - raise RecoveryError(f"components.{name}.commit is not a full source identity") - channel_pattern = { - "alpha": ALPHA_VERSION_PATTERN, - "beta": BETA_VERSION_PATTERN, - "rc": RC_VERSION_PATTERN, - }[plan["channel"]] - channel_components = COMPONENTS if plan["channel"] == "rc" else ("workflow", "waterline") - for name in channel_components: - if not channel_pattern.fullmatch(components[name]["version"]): - raise RecoveryError(f"{name} does not have an exact 2.0.0-{plan['channel']}.N identity") - authorization = plan["beta_authorization"] - if plan["channel"] == "alpha" and authorization is not None: - raise RecoveryError("alpha plans cannot claim beta authorization") - if aggregate_rc_foundation and authorization is not None: - raise RecoveryError("aggregate release-candidate plans cannot claim beta qualification") - if ( - plan["channel"] in {"beta", "rc"} - and not aggregate_rc_foundation - and ( - not isinstance(authorization, dict) - or set(authorization) != {"tag", "commit"} - or not isinstance(authorization.get("tag"), str) - or not re.fullmatch(r"beta-authorization/[a-z0-9][a-z0-9._-]{0,55}", authorization["tag"]) - or not isinstance(authorization.get("commit"), str) - or not COMMIT_PATTERN.fullmatch(authorization["commit"]) - ) - ): - raise RecoveryError("beta and release-candidate plans require immutable beta qualification") - if plan["schema"] == LEGACY_SCHEMA and manifest_digest(plan) not in LEGACY_PLAN_DIGESTS: - raise RecoveryError("legacy release plan is not an exact recorded historical contract") - - -def beta_authorization_matches_plan( - plan: dict[str, Any], - authorization: dict[str, str], - record: Any, -) -> bool: - if plan["channel"] == "beta": - return record == { - "schema": "durable-workflow.beta-authorization/v1", - "channel": "beta", - "candidate": plan["plan"], - "components": plan["components"], - } - if plan["channel"] != "rc" or not isinstance(record, dict): - return False - components = record.get("components") - candidate = record.get("candidate") - if ( - set(record) != {"schema", "channel", "candidate", "components"} - or record.get("schema") != "durable-workflow.beta-authorization/v1" - or record.get("channel") != "beta" - or not isinstance(candidate, str) - or authorization["tag"] != f"beta-authorization/{candidate}" - or not isinstance(components, dict) - or set(components) != set(COMPONENTS) - ): - return False - versions: set[str] = set() - for identity in components.values(): - if ( - not isinstance(identity, dict) - or set(identity) != {"version", "commit"} - or not BETA_VERSION_PATTERN.fullmatch(str(identity.get("version", ""))) - or not COMMIT_PATTERN.fullmatch(str(identity.get("commit", ""))) - ): - return False - versions.add(identity["version"]) - return len(versions) == 1 - - -def manifest_digest(value: Any) -> str: - return hashlib.sha256(canonical_json(value)).hexdigest() - - -def numeric_identifier_precedence(identifier: str) -> tuple[int, str]: - return len(identifier), identifier - - -def increment_numeric_identifier(identifier: str) -> str: - digits = list(identifier) - index = len(digits) - 1 - while index >= 0 and digits[index] == "9": - digits[index] = "0" - index -= 1 - if index < 0: - return "1" + "".join(digits) - digits[index] = chr(ord(digits[index]) + 1) - return "".join(digits) - - -@dataclass(frozen=True) -class SemVer: - value: str - core: tuple[str, str, str] - prerelease: tuple[str, ...] - build: tuple[str, ...] - - @property - def precedence(self) -> tuple[Any, ...]: - major, minor, patch = (numeric_identifier_precedence(part) for part in self.core) - identifiers = tuple( - (0, numeric_identifier_precedence(part)) if part.isdigit() else (1, part) for part in self.prerelease - ) - return major, minor, patch, 0 if self.prerelease else 1, identifiers - - def immediately_precedes(self, successor: SemVer) -> bool: - if not self.prerelease: - return ( - not successor.prerelease - and successor.core[:2] == self.core[:2] - and successor.core[2] == increment_numeric_identifier(self.core[2]) - ) - if self.prerelease[-1].isdigit(): - expected_prerelease = self.prerelease[:-1] + ( - increment_numeric_identifier(self.prerelease[-1]), - ) - else: - expected_prerelease = self.prerelease + ("1",) - return successor.core == self.core and successor.prerelease == expected_prerelease - - -def parse_semver(version: str) -> SemVer | None: - if VERSION_PATTERN.fullmatch(version) is None: - return None - without_build, build_separator, build = version.partition("+") - core, prerelease_separator, prerelease = without_build.partition("-") - major, minor, patch = core.split(".") - return SemVer( - value=version, - core=(major, minor, patch), - prerelease=tuple(prerelease.split(".")) if prerelease_separator else (), - build=tuple(build.split(".")) if build_separator else (), - ) - - -def is_immediate_version_successor(previous: str, successor: str) -> bool: - previous_semver = parse_semver(previous) - successor_semver = parse_semver(successor) - return ( - previous_semver is not None - and successor_semver is not None - and previous_semver.immediately_precedes(successor_semver) - ) - - -def conflict_component_names(conflicts: Any) -> list[str]: - if not isinstance(conflicts, list): - raise RecoveryError("release plan failure conflicts must be a non-empty list", "plan-discovery") - names = [conflict.get("component") if isinstance(conflict, dict) else None for conflict in conflicts] - if ( - not names - or any(not isinstance(name, str) or name not in COMPONENTS for name in names) - or len(names) != len(set(names)) - ): - raise RecoveryError( - f"conflicting components must be unique names from {sorted(COMPONENTS)}", - "plan-discovery", - ) - expected_order = [name for name in COMPONENTS if name in names] - if names != expected_order: - raise RecoveryError("conflicting components must follow release-plan component order", "plan-discovery") - return names - - -def validate_successor_transition( - failed_plan: dict[str, Any], - successor_plan: dict[str, Any], - conflicts: list[Any], -) -> None: - validate_plan(failed_plan) - validate_plan(successor_plan) - conflict_names = conflict_component_names(conflicts) - if successor_plan["plan"] == failed_plan["plan"]: - raise RecoveryError("a superseding release plan must use a new plan identity", "plan-discovery") - if successor_plan["channel"] != failed_plan["channel"]: - raise RecoveryError("a superseding release plan cannot change the release channel", "plan-discovery") - if successor_plan["foundation"] != failed_plan["foundation"]: - raise RecoveryError("a superseding release plan cannot change the candidate foundation", "plan-discovery") - for name, identity in failed_plan["components"].items(): - successor_identity = successor_plan["components"][name] - if name not in conflict_names and successor_identity != identity: - raise RecoveryError( - f"superseding release plan changes unaffected component {name}", - "plan-discovery", - ) - for conflict in conflicts: - name = conflict["component"] - failed_identity = failed_plan["components"][name] - successor_identity = successor_plan["components"][name] - if successor_identity == failed_identity: - raise RecoveryError( - f"superseding release plan leaves conflict unresolved for {name}", - "plan-discovery", - ) - if conflict["reason"] == SUPERSESSION_REASON: - if successor_identity["commit"] != failed_identity["commit"]: - raise RecoveryError( - f"superseding release plan must retain {name}'s conflicting planned commit", - "plan-discovery", - ) - if not is_immediate_version_successor( - failed_identity["version"], - successor_identity["version"], - ): - raise RecoveryError( - f"superseding release plan must allocate {name}'s immediate next version", - "plan-discovery", - ) - elif conflict["reason"] == SOURCE_MANIFEST_REASON: - if successor_identity["version"] != failed_identity["version"]: - raise RecoveryError( - f"superseding release plan must retain {name}'s intended version", - "plan-discovery", - ) - if successor_identity["commit"] == failed_identity["commit"]: - raise RecoveryError( - f"superseding release plan must replace {name}'s incompatible source commit", - "plan-discovery", - ) - elif conflict["reason"] == OCCUPIED_SOURCE_MANIFEST_REASON: - if not is_immediate_version_successor( - failed_identity["version"], - successor_identity["version"], - ): - raise RecoveryError( - f"superseding release plan must allocate {name}'s immediate next version", - "plan-discovery", - ) - if successor_identity["commit"] == failed_identity["commit"]: - raise RecoveryError( - f"superseding release plan must replace {name}'s incompatible tagged source commit", - "plan-discovery", - ) - else: - raise RecoveryError( - f"release plan failure has an unsupported conflict reason for {name}", - "plan-discovery", - ) - - -def validate_environment_protection_evidence(protection: Any) -> None: - expected_keys = { - "custom_branch_policies", - "deployment_branch_policy", - "environment_id", - "environment_url", - "required_reviewer_rule_ids", - } - if not isinstance(protection, dict) or set(protection) != expected_keys: - raise RecoveryError( - "release plan failure environment protection evidence has an invalid shape", - "plan-discovery", - ) - reviewer_rule_ids = protection["required_reviewer_rule_ids"] - branch_policy = protection["deployment_branch_policy"] - custom_policies = protection["custom_branch_policies"] - if ( - type(protection["environment_id"]) is not int - or protection["environment_id"] < 1 - or protection["environment_url"] != SUPERSESSION_ENVIRONMENT_URL - or not isinstance(reviewer_rule_ids, list) - or not reviewer_rule_ids - or any(type(rule_id) is not int or rule_id < 1 for rule_id in reviewer_rule_ids) - or reviewer_rule_ids != sorted(set(reviewer_rule_ids)) - ): - raise RecoveryError( - "release plan failure lacks protected-environment reviewer evidence", - "plan-discovery", - ) - if ( - not isinstance(branch_policy, dict) - or set(branch_policy) != {"custom_branch_policies", "protected_branches"} - or branch_policy["custom_branch_policies"] is not True - or branch_policy["protected_branches"] is not False - ): - raise RecoveryError( - "release plan failure lacks the protected environment custom-branch policy", - "plan-discovery", - ) - if ( - not isinstance(custom_policies, list) - or len(custom_policies) != 1 - or not isinstance(custom_policies[0], dict) - or set(custom_policies[0]) != {"id", "name"} - or type(custom_policies[0]["id"]) is not int - or custom_policies[0]["id"] < 1 - or custom_policies[0]["name"] != "main" - ): - raise RecoveryError( - "release plan failure lacks the protected environment custom main-branch policy", - "plan-discovery", - ) - - -def validate_environment_approval_evidence(approval: Any, authorization: dict[str, Any]) -> None: - expected_keys = {"comment", "environments", "run_attempt", "run_id", "state", "user"} - if not isinstance(approval, dict) or set(approval) != expected_keys: - raise RecoveryError( - "release plan failure environment approval evidence has an invalid shape", - "plan-discovery", - ) - environments = approval["environments"] - user = approval["user"] - protection = authorization["environment_protection"] - if ( - approval["state"] != "approved" - or not isinstance(approval["comment"], str) - or type(approval["run_id"]) is not int - or approval["run_id"] < 1 - or type(approval["run_attempt"]) is not int - or approval["run_attempt"] < 1 - or approval["run_id"] != authorization["run_id"] - or approval["run_attempt"] != authorization["run_attempt"] - or not isinstance(environments, list) - or len(environments) != 1 - or not isinstance(environments[0], dict) - or set(environments[0]) != {"html_url", "id", "name", "node_id", "url"} - ): - raise RecoveryError( - "release plan failure lacks an approved deployment bound to its workflow run", - "plan-discovery", - ) - environment = environments[0] - if ( - environment["id"] != protection["environment_id"] - or type(environment["id"]) is not int - or environment["name"] != SUPERSESSION_ENVIRONMENT - or environment["url"] != SUPERSESSION_ENVIRONMENT_API_URL - or environment["html_url"] != SUPERSESSION_ENVIRONMENT_URL - or not isinstance(environment["node_id"], str) - or not environment["node_id"] - ): - raise RecoveryError( - "release plan failure approval names the wrong protected environment", - "plan-discovery", - ) - if not isinstance(user, dict) or set(user) != {"html_url", "id", "login", "node_id", "url"}: - raise RecoveryError( - "release plan failure approving user evidence has an invalid shape", - "plan-discovery", - ) - login = user["login"] - if ( - type(user["id"]) is not int - or user["id"] < 1 - or not isinstance(user["node_id"], str) - or not user["node_id"] - or not isinstance(login, str) - or not re.fullmatch(r"[A-Za-z0-9-]{1,39}", login) - or user["url"] != f"https://api.github.com/users/{login}" - or user["html_url"] != f"https://github.com/{login}" - ): - raise RecoveryError( - "release plan failure lacks a durable approving user identity", - "plan-discovery", - ) - - -def validate_source_manifest_evidence( - evidence: Any, - component_name: str, - identity: dict[str, str], - *, - must_match_version: bool, -) -> None: - expected_keys = {"declared_version", "package", "path", "sha256", "source_commit", "url"} - specification = SOURCE_MANIFESTS.get(component_name) - if ( - specification is None - or not isinstance(evidence, dict) - or set(evidence) != expected_keys - or evidence["path"] != specification["path"] - or evidence["package"] != specification["package"] - or evidence["source_commit"] != identity["commit"] - or not isinstance(evidence["sha256"], str) - or not SHA256_PATTERN.fullmatch(evidence["sha256"]) - or not isinstance(evidence["declared_version"], str) - or not VERSION_PATTERN.fullmatch(evidence["declared_version"]) - or evidence["url"] - != ( - f"https://github.com/{COMPONENTS[component_name].repository}/blob/" - f"{identity['commit']}/{specification['path']}" - ) - ): - raise RecoveryError( - f"release plan failure has invalid source-manifest evidence for {component_name}", - "plan-discovery", - ) - version_matches = evidence["declared_version"] == identity["version"] - if version_matches is not must_match_version: - state = "match" if must_match_version else "conflict with" - raise RecoveryError( - f"release plan failure source manifest does not {state} {component_name} version allocation", - "plan-discovery", - ) - - -def publication_absence_locations( - component_name: str, - version: str, -) -> tuple[dict[str, str], dict[str, str]]: - component = COMPONENTS[component_name] - encoded_version = urllib.parse.quote(version, safe="") - release = { - "api_url": f"https://api.github.com/repos/{component.repository}/releases/tags/{encoded_version}", - "status": "absent", - "url": f"https://github.com/{component.repository}/releases/tag/{encoded_version}", - } - encoded_package = urllib.parse.quote(component.package, safe="") - if component.distribution == "pypi": - distribution = { - "api_url": f"https://pypi.org/pypi/{encoded_package}/{encoded_version}/json", - "kind": "pypi", - "status": "absent", - "url": f"https://pypi.org/project/{encoded_package}/{encoded_version}/", - } - elif component.distribution == "crates.io": - distribution = { - "api_url": f"https://crates.io/api/v1/crates/{encoded_package}/{encoded_version}", - "kind": "crates.io", - "status": "absent", - "url": f"https://crates.io/crates/{encoded_package}/{encoded_version}", - } - else: - raise RecoveryError( - f"{component_name} has no supported source-manifest distribution absence proof", - "plan-discovery", - ) - return release, distribution - - -def validate_occupied_source_manifest_evidence( - conflict: dict[str, Any], - component_name: str, - identity: dict[str, str], -) -> None: - component = COMPONENTS[component_name] - source_tag = conflict["source_tag"] - if ( - not isinstance(source_tag, dict) - or set(source_tag) != {"commit", "repository", "tag", "tag_object", "url"} - or source_tag["repository"] != component.repository - or source_tag["tag"] != identity["version"] - or source_tag["commit"] != identity["commit"] - or not isinstance(source_tag["tag_object"], str) - or not COMMIT_PATTERN.fullmatch(source_tag["tag_object"]) - or source_tag["url"] != f"https://github.com/{component.repository}/tree/{identity['version']}" - ): - raise RecoveryError( - f"release plan failure does not prove {component_name}'s occupied planned source tag", - "plan-discovery", - ) - expected_release, expected_distribution = publication_absence_locations(component_name, identity["version"]) - if conflict["github_release"] != expected_release: - raise RecoveryError( - f"release plan failure lacks {component_name} GitHub Release absence evidence", - "plan-discovery", - ) - if conflict["distribution"] != expected_distribution: - raise RecoveryError( - f"release plan failure lacks {component_name} distribution absence evidence", - "plan-discovery", - ) - - -def canonical_cli_embedded_identity(version: str, commit: str) -> str: - return f"dw {version.lstrip('v')} (commit {commit[:12]})" - - -def require_distribution_identity( - distribution: dict[str, Any], - component_name: str, - version: str, - observed_commit: str, -) -> None: - component = COMPONENTS[component_name] - if distribution.get("kind") != component.distribution: - raise RecoveryError("public distribution evidence has the wrong kind", "plan-discovery") - if component.distribution == "composer": - source_reference = distribution.get("source_reference") - dist_reference = distribution.get("dist_reference") - matches = ( - isinstance(source_reference, str) - and COMMIT_PATTERN.fullmatch(source_reference) - and source_reference == observed_commit - and isinstance(dist_reference, str) - and COMMIT_PATTERN.fullmatch(dist_reference) - and dist_reference == observed_commit - ) - elif component.distribution == "github-release": - package_source = distribution.get("package_source") - matches = ( - isinstance(package_source, dict) - and set(package_source) == {"commit", "embedded_phar_identity"} - and package_source.get("commit") == observed_commit - and package_source.get("embedded_phar_identity") - == canonical_cli_embedded_identity(version, observed_commit) - ) - authority = distribution.get("build_attestation_authority") - exact_tag_authority = { - "mode": "exact-tag", - "ref": f"refs/tags/{version}", - "commit": observed_commit, - } - qualified_main_authority = { - "mode": "qualified-main-workflow", - "ref": "refs/heads/main", - "workflow": f"{component.repository}/.github/workflows/release.yml", - } - if distribution.get("build_attestations_verified") is not True or authority not in ( - exact_tag_authority, - qualified_main_authority, - ): - raise RecoveryError( - "public distribution evidence has an untrusted build attestation authority", - "plan-discovery", - ) - elif component.distribution == "pypi": - source = distribution.get("source_identity") - matches = isinstance(source, dict) and source.get("source_commit") == observed_commit - elif component.distribution == "crates.io": - matches = distribution.get("archive_vcs_commit") == observed_commit - else: - configs = distribution.get("configs") - matches = ( - isinstance(configs, list) - and bool(configs) - and all( - isinstance(config, dict) - and isinstance(config.get("labels"), dict) - and config["labels"].get("org.opencontainers.image.revision") == observed_commit - for config in configs - ) - ) - if not matches: - raise RecoveryError( - "public distribution evidence does not bind the observed source commit", - "plan-discovery", - ) - - -def validate_conflict_record( - conflict: Any, - failed_plan: dict[str, Any], - successor_plan: dict[str, Any], -) -> None: - if not isinstance(conflict, dict): - raise RecoveryError( - "release plan failure conflict evidence has an invalid shape", - "plan-discovery", - ) - component_name = conflict.get("component") - if component_name not in COMPONENTS: - raise RecoveryError( - "release plan failure names an unknown conflicting component", - "plan-discovery", - ) - identity = failed_plan["components"][component_name] - successor_identity = successor_plan["components"][component_name] - common_identity_matches = ( - conflict.get("version") == identity["version"] and conflict.get("planned_commit") == identity["commit"] - ) - reason = conflict.get("reason") - if reason == SUPERSESSION_REASON: - expected_keys = { - "component", - "version", - "planned_commit", - "observed_commit", - "reason", - "github_release", - "distribution", - } - if ( - set(conflict) != expected_keys - or not common_identity_matches - or not isinstance(conflict.get("observed_commit"), str) - or not COMMIT_PATTERN.fullmatch(conflict["observed_commit"]) - or conflict["observed_commit"] == identity["commit"] - ): - raise RecoveryError( - "release plan failure conflict does not prove a different public source identity", - "plan-discovery", - ) - release = conflict["github_release"] - if ( - not isinstance(release, dict) - or set(release) != {"id", "url"} - or type(release["id"]) is not int - or release["id"] < 1 - or not isinstance(release["url"], str) - or not release["url"].startswith(f"https://github.com/{COMPONENTS[component_name].repository}/releases/") - ): - raise RecoveryError( - "release plan failure lacks durable GitHub Release evidence", - "plan-discovery", - ) - distribution = conflict["distribution"] - if not isinstance(distribution, dict): - raise RecoveryError( - "release plan failure lacks matching distribution evidence", - "plan-discovery", - ) - require_distribution_identity( - distribution, - component_name, - conflict["version"], - conflict["observed_commit"], - ) - elif reason == SOURCE_MANIFEST_REASON: - expected_keys = { - "component", - "version", - "planned_commit", - "reason", - "source_manifest", - "successor_source_manifest", - } - if set(conflict) != expected_keys or not common_identity_matches: - raise RecoveryError( - "release plan failure manifest conflict evidence has an invalid shape", - "plan-discovery", - ) - validate_source_manifest_evidence( - conflict["source_manifest"], - component_name, - identity, - must_match_version=False, - ) - validate_source_manifest_evidence( - conflict["successor_source_manifest"], - component_name, - successor_identity, - must_match_version=True, - ) - elif reason == OCCUPIED_SOURCE_MANIFEST_REASON: - expected_keys = { - "component", - "version", - "planned_commit", - "reason", - "source_manifest", - "source_tag", - "github_release", - "distribution", - "successor_source_manifest", - } - if set(conflict) != expected_keys or not common_identity_matches: - raise RecoveryError( - "release plan failure occupied manifest conflict evidence has an invalid shape", - "plan-discovery", - ) - validate_source_manifest_evidence( - conflict["source_manifest"], - component_name, - identity, - must_match_version=False, - ) - validate_source_manifest_evidence( - conflict["successor_source_manifest"], - component_name, - successor_identity, - must_match_version=True, - ) - validate_occupied_source_manifest_evidence(conflict, component_name, identity) - else: - raise RecoveryError( - f"release plan failure has an unsupported conflict reason for {component_name}", - "plan-discovery", - ) - - -def validate_supersession_record( - record: Any, - failed_plan: dict[str, Any], - failed_plan_commit: str, - successor_plan: dict[str, Any], -) -> None: - expected = { - "schema", - "outcome", - "failed_plan", - "conflicts", - "successor_plan", - "authorization", - } - if not isinstance(record, dict) or set(record) != expected: - raise RecoveryError( - f"release plan failure record keys must be exactly {sorted(expected)}", - "plan-discovery", - ) - expected_failed = { - "tag": f"{PLAN_TAG_PREFIX}{failed_plan['plan']}", - "commit": failed_plan_commit, - "sha256": manifest_digest(failed_plan), - } - if record["schema"] != "durable-workflow.release-plan-failure/v1": - raise RecoveryError( - "release plan failure record has an unsupported schema", - "plan-discovery", - ) - if record["outcome"] != "terminal-failure" or record["failed_plan"] != expected_failed: - raise RecoveryError( - "release plan failure record does not terminate this exact immutable plan", - "plan-discovery", - ) - expected_successor = { - "tag": f"{PLAN_TAG_PREFIX}{successor_plan['plan']}", - "sha256": manifest_digest(successor_plan), - } - if record["successor_plan"] != expected_successor: - raise RecoveryError( - "release plan failure record names a different successor plan", - "plan-discovery", - ) - conflicts = record["conflicts"] - conflict_component_names(conflicts) - for conflict in conflicts: - validate_conflict_record(conflict, failed_plan, successor_plan) - validate_successor_transition(failed_plan, successor_plan, conflicts) - - authorization = record["authorization"] - authorization_keys = { - "actor", - "environment", - "environment_approval", - "environment_protection", - "repository", - "run_attempt", - "run_id", - "run_url", - "workflow_commit", - "workflow_ref", - } - if not isinstance(authorization, dict) or set(authorization) != authorization_keys: - raise RecoveryError( - "release plan failure authorization evidence has an invalid shape", - "plan-discovery", - ) - protection = authorization["environment_protection"] - validate_environment_protection_evidence(protection) - workflow_ref = f"{CONTROL_REPOSITORY}/{SUPERSESSION_WORKFLOW}@refs/heads/main" - workflow_commit = authorization["workflow_commit"] - actor = authorization["actor"] - if ( - authorization.get("repository") != CONTROL_REPOSITORY - or authorization.get("environment") != SUPERSESSION_ENVIRONMENT - or authorization.get("workflow_ref") != workflow_ref - or not isinstance(workflow_commit, str) - or not COMMIT_PATTERN.fullmatch(workflow_commit) - or not isinstance(actor, str) - or not re.fullmatch(r"[A-Za-z0-9-]{1,39}", actor) - or type(authorization.get("run_id")) is not int - or authorization["run_id"] < 1 - or type(authorization.get("run_attempt")) is not int - or authorization["run_attempt"] < 1 - or authorization.get("run_url") - != f"https://github.com/{CONTROL_REPOSITORY}/actions/runs/{authorization.get('run_id')}" - ): - raise RecoveryError( - "release plan failure was not authorized by the protected supersession workflow", - "plan-discovery", - ) - validate_environment_approval_evidence(authorization["environment_approval"], authorization) - - -def protected_environment_evidence( - client: PublicClient, -) -> tuple[dict[str, Any], set[tuple[int, str]]]: - encoded = urllib.parse.quote(SUPERSESSION_ENVIRONMENT, safe="") - environment = client.json( - f"https://api.github.com/repos/{CONTROL_REPOSITORY}/environments/{encoded}", - headers={"X-GitHub-Api-Version": SUPERSESSION_API_VERSION}, - accept="application/vnd.github+json", - ) - rules = environment.get("protection_rules") if isinstance(environment, dict) else None - if not isinstance(rules, list): - raise RecoveryError( - f"GitHub environment {SUPERSESSION_ENVIRONMENT} has no protection rules", - "plan-discovery", - ) - reviewer_rule_ids = sorted( - rule["id"] - for rule in rules - if ( - isinstance(rule, dict) - and rule.get("type") == "required_reviewers" - and rule.get("reviewers") - and type(rule.get("id")) is int - and rule["id"] > 0 - ) - ) - required_reviewers = { - (reviewer["reviewer"]["id"], reviewer["reviewer"]["login"]) - for rule in rules - if isinstance(rule, dict) and rule.get("type") == "required_reviewers" - for reviewer in ( - rule.get("reviewers") if isinstance(rule.get("reviewers"), list) else [] - ) - if ( - isinstance(reviewer, dict) - and reviewer.get("type") == "User" - and isinstance(reviewer.get("reviewer"), dict) - and type(reviewer["reviewer"].get("id")) is int - and reviewer["reviewer"]["id"] > 0 - and isinstance(reviewer["reviewer"].get("login"), str) - and re.fullmatch(r"[A-Za-z0-9-]{1,39}", reviewer["reviewer"]["login"]) - ) - } - environment_id = environment.get("id") - branch_policy = environment.get("deployment_branch_policy") - if ( - not reviewer_rule_ids - or type(environment_id) is not int - or environment_id < 1 - or environment.get("html_url") != SUPERSESSION_ENVIRONMENT_URL - or branch_policy != {"custom_branch_policies": True, "protected_branches": False} - ): - raise RecoveryError( - f"GitHub environment {SUPERSESSION_ENVIRONMENT} lacks the required reviewer and branch policy", - "plan-discovery", - ) - policies = client.json( - f"https://api.github.com/repos/{CONTROL_REPOSITORY}/environments/{encoded}/" - "deployment-branch-policies?per_page=100", - headers={"X-GitHub-Api-Version": SUPERSESSION_API_VERSION}, - accept="application/vnd.github+json", - ) - branch_policies = policies.get("branch_policies") if isinstance(policies, dict) else None - if ( - not isinstance(branch_policies, list) - or type(policies.get("total_count")) is not int - or policies["total_count"] != 1 - or len(branch_policies) != 1 - or not isinstance(branch_policies[0], dict) - or type(branch_policies[0].get("id")) is not int - or branch_policies[0]["id"] < 1 - or branch_policies[0].get("name") != "main" - or branch_policies[0].get("type", "branch") != "branch" - ): - raise RecoveryError( - f"GitHub environment {SUPERSESSION_ENVIRONMENT} must allow only the main branch", - "plan-discovery", - ) - evidence = { - "custom_branch_policies": [{"id": branch_policies[0]["id"], "name": "main"}], - "deployment_branch_policy": branch_policy, - "environment_id": environment_id, - "environment_url": SUPERSESSION_ENVIRONMENT_URL, - "required_reviewer_rule_ids": reviewer_rule_ids, - } - validate_environment_protection_evidence(evidence) - return evidence, required_reviewers - - -def protected_run_approval_evidence( - client: PublicClient, - authorization: dict[str, Any], - environment_protection: dict[str, Any], - required_reviewers: set[tuple[int, str]], -) -> dict[str, Any]: - run_id = authorization["run_id"] - run_attempt = authorization["run_attempt"] - run = client.json( - f"https://api.github.com/repos/{CONTROL_REPOSITORY}/actions/runs/{run_id}", - headers={"X-GitHub-Api-Version": SUPERSESSION_API_VERSION}, - accept="application/vnd.github+json", - ) - actor = run.get("actor") if isinstance(run, dict) else None - repository = run.get("repository") if isinstance(run, dict) else None - if ( - not isinstance(actor, dict) - or actor.get("login") != authorization["actor"] - or not isinstance(repository, dict) - or repository.get("full_name") != CONTROL_REPOSITORY - or type(run.get("id")) is not int - or run["id"] != run_id - or type(run.get("run_attempt")) is not int - or run["run_attempt"] != run_attempt - or run.get("event") != "workflow_dispatch" - or run.get("path") not in {SUPERSESSION_WORKFLOW, f"{SUPERSESSION_WORKFLOW}@main"} - or run.get("head_branch") != "main" - or run.get("head_sha") != authorization["workflow_commit"] - or run.get("status") != "completed" - or run.get("conclusion") != "success" - or run.get("html_url") != authorization["run_url"] - ): - raise RecoveryError( - "protected supersession workflow run evidence does not match GitHub", - "plan-discovery", - ) - if run_attempt != 1: - # GitHub's approval-history response has no attempt identity. Attempt 1 - # is the only attempt for which an approval cannot be stale. - raise RecoveryError( - "GitHub approval history cannot bind protected approval to a rerun attempt", - "plan-discovery", - ) - history = client.json( - f"https://api.github.com/repos/{CONTROL_REPOSITORY}/actions/runs/{run_id}/approvals", - headers={"X-GitHub-Api-Version": SUPERSESSION_API_VERSION}, - accept="application/vnd.github+json", - ) - if ( - not isinstance(history, list) - or len(history) != 1 - or not isinstance(history[0], dict) - or history[0].get("state") != "approved" - ): - raise RecoveryError( - "protected supersession run must contain exactly one approved review", - "plan-discovery", - ) - review = history[0] - environments = review.get("environments") - user = review.get("user") - if ( - not isinstance(review.get("comment"), str) - or not isinstance(environments, list) - or len(environments) != 1 - or not isinstance(environments[0], dict) - or not isinstance(user, dict) - ): - raise RecoveryError( - "protected supersession approval history is malformed", - "plan-discovery", - ) - environment = environments[0] - evidence = { - "comment": review["comment"], - "environments": [ - { - "html_url": environment.get("html_url"), - "id": environment.get("id"), - "name": environment.get("name"), - "node_id": environment.get("node_id"), - "url": environment.get("url"), - } - ], - "run_attempt": run_attempt, - "run_id": run_id, - "state": review["state"], - "user": { - "html_url": user.get("html_url"), - "id": user.get("id"), - "login": user.get("login"), - "node_id": user.get("node_id"), - "url": user.get("url"), - }, - } - validate_environment_approval_evidence( - evidence, - {**authorization, "environment_protection": environment_protection}, - ) - if (evidence["user"]["id"], evidence["user"]["login"]) not in required_reviewers: - raise RecoveryError( - "protected supersession approving user is not authorized by the current reviewer policy", - "plan-discovery", - ) - return evidence - - -def revalidate_supersession_authority(record: dict[str, Any], client: PublicClient) -> None: - authorization = record["authorization"] - protection, required_reviewers = protected_environment_evidence(client) - if protection != authorization["environment_protection"]: - raise RecoveryError( - "release plan failure protected environment policy no longer matches GitHub", - "plan-discovery", - ) - approval = protected_run_approval_evidence( - client, - authorization, - protection, - required_reviewers, - ) - if approval != authorization["environment_approval"]: - raise RecoveryError( - "release plan failure approved deployment evidence no longer matches GitHub", - "plan-discovery", - ) - - -def validate_release_preparation(preparation: Any, plan: dict[str, Any]) -> None: - if not isinstance(preparation, dict) or set(preparation) != { - "schema", - "release_plan", - "components", - }: - raise RecoveryError("release preparation has an invalid top-level shape", "plan-discovery") - if preparation["schema"] != PREPARATION_SCHEMA or preparation["release_plan"] != { - "tag": f"{PLAN_TAG_PREFIX}{plan['plan']}", - "sha256": manifest_digest(plan), - }: - raise RecoveryError("release preparation names a different immutable plan", "plan-discovery") - components = preparation["components"] - if not isinstance(components, dict) or set(components) != set(COMPONENTS): - raise RecoveryError("release preparation does not cover the exact component tuple", "plan-discovery") - release_dates: set[str] = set() - for name, entry in components.items(): - identity = plan["components"][name] - if not isinstance(entry, dict) or set(entry) != { - "version", - "source_commit", - "release_notes", - }: - raise RecoveryError(f"release preparation for {name} has an invalid shape", "plan-discovery") - if entry["version"] != identity["version"] or entry["source_commit"] != identity["commit"]: - raise RecoveryError( - f"release preparation for {name} names a different planned identity", - "plan-discovery", - ) - notes = entry["release_notes"] - if not isinstance(notes, dict) or set(notes) != { - "format", - "heading", - "markdown", - "release_date", - "sha256", - "source", - }: - raise RecoveryError(f"release preparation for {name} has invalid release notes", "plan-discovery") - release_date = notes["release_date"] - try: - parsed_date = dt.date.fromisoformat(release_date) - except (TypeError, ValueError) as error: - raise RecoveryError( - f"release preparation for {name} has an invalid release date", - "plan-discovery", - ) from error - heading = f"## [{identity['version']}] - {parsed_date.isoformat()}" - markdown = notes["markdown"] - if ( - notes["format"] != MARKDOWN_MEDIA_TYPE - or release_date != parsed_date.isoformat() - or notes["heading"] != heading - or not isinstance(markdown, str) - or not markdown.startswith(f"{heading}\n\n") - or not markdown.endswith("\n") - or notes["sha256"] != hashlib.sha256(markdown.encode()).hexdigest() - ): - raise RecoveryError( - f"release preparation for {name} has mismatched versioned note content", - "plan-discovery", - ) - source = notes["source"] - expected_kind = "changelog-unreleased" if name in SOURCE_CHANGELOGS else "source-commit-message" - expected_source_url = ( - f"https://github.com/{COMPONENTS[name].repository}/blob/{identity['commit']}/CHANGELOG.md" - if name in SOURCE_CHANGELOGS - else f"https://github.com/{COMPONENTS[name].repository}/commit/{identity['commit']}" - ) - if ( - not isinstance(source, dict) - or set(source) != {"kind", "sha256", "url"} - or source["kind"] != expected_kind - or not isinstance(source["sha256"], str) - or not SHA256_PATTERN.fullmatch(source["sha256"]) - or source["url"] != expected_source_url - ): - raise RecoveryError( - f"release preparation for {name} has invalid note-source evidence", - "plan-discovery", - ) - release_dates.add(release_date) - if len(release_dates) != 1: - raise RecoveryError("release preparation components do not share one release date", "plan-discovery") - - -def resolve_tag(client: PublicClient, repository: str, tag: str) -> str | None: - encoded = urllib.parse.quote(tag, safe="") - try: - ref = client.json(f"https://api.github.com/repos/{repository}/git/ref/tags/{encoded}") - except NotFound: - return None - target = ref.get("object", {}) - seen: set[str] = set() - while target.get("type") == "tag": - sha = target.get("sha") - if not isinstance(sha, str) or not COMMIT_PATTERN.fullmatch(sha) or sha in seen: - raise RecoveryError(f"invalid annotated tag chain for {repository}@{tag}", "tag-preflight") - seen.add(sha) - target = client.json(f"https://api.github.com/repos/{repository}/git/tags/{sha}").get("object", {}) - commit = target.get("sha") - if ( - target.get("type") != "commit" - or not isinstance(commit, str) - or not COMMIT_PATTERN.fullmatch(commit) - ): - raise RecoveryError(f"tag {repository}@{tag} does not resolve to a commit", "tag-preflight") - return commit - - -def read_record(client: PublicClient, tag: str, commit: str, filename: str) -> Any: - if resolve_tag(client, CONTROL_REPOSITORY, tag) != commit: - raise RecoveryError(f"immutable record {tag} does not resolve to {commit}", "plan-discovery") - encoded_filename = urllib.parse.quote(filename, safe="/") - raw = client.bytes( - f"https://api.github.com/repos/{CONTROL_REPOSITORY}/contents/{encoded_filename}?ref={commit}", - accept="application/vnd.github.raw+json", - ) - try: - return json.loads(raw) - except json.JSONDecodeError as error: - raise RecoveryError(f"immutable record {tag}:{filename} is not valid JSON", "plan-discovery") from error - - -def require_python_source_manifest_version( - client: PublicClient, - identity: dict[str, str], - existing_tag: str | None, -) -> dict[str, Any]: - component = COMPONENTS["sdk-python"] - path = PYTHON_SOURCE_MANIFEST["path"] - encoded_path = urllib.parse.quote(path, safe="/") - raw = client.bytes( - f"https://api.github.com/repos/{component.repository}/contents/{encoded_path}?ref={identity['commit']}", - accept="application/vnd.github.raw+json", - ) - if len(raw) > 1024 * 1024: - raise RecoveryError("sdk-python source manifest exceeds 1 MiB", "source-manifest-preflight") - try: - manifest = tomllib.loads(raw.decode("utf-8")) - except (UnicodeDecodeError, tomllib.TOMLDecodeError) as error: - raise RecoveryError( - "sdk-python pyproject.toml is not valid UTF-8 TOML at the planned commit", - "source-manifest-preflight", - ) from error - project = manifest.get("project") - declared_package = project.get("name") if isinstance(project, dict) else None - declared_version = project.get("version") if isinstance(project, dict) else None - if ( - declared_package != PYTHON_SOURCE_MANIFEST["package"] - or not isinstance(declared_version, str) - or not VERSION_PATTERN.fullmatch(declared_version) - ): - raise RecoveryError( - "sdk-python pyproject.toml has no exact durable-workflow project identity at the planned commit", - "source-manifest-preflight", - ) - - evidence = { - "declared_version": declared_version, - "package": declared_package, - "path": path, - "sha256": hashlib.sha256(raw).hexdigest(), - "source_commit": identity["commit"], - "url": f"https://github.com/{component.repository}/blob/{identity['commit']}/{path}", - } - if declared_version != identity["version"]: - source_tag = { - "tag": identity["version"], - "status": "present" if existing_tag is not None else "absent", - "commit": existing_tag, - } - if existing_tag is None: - tag_instruction = f"keep {component.repository}@{identity['version']} absent" - else: - tag_instruction = f"keep immutable {component.repository}@{identity['version']} at {existing_tag} unchanged" - raise RecoveryError( - f"planned sdk-python version {identity['version']} does not match pyproject.toml " - f"project.version {declared_version} at {identity['commit']}", - "source-manifest-preflight", - evidence={ - "classification": SOURCE_MANIFEST_VERSION_CONFLICT, - "planned_identity": dict(identity), - "source_manifest": evidence, - "source_tag": source_tag, - }, - resume_action=( - f"Hand off this terminal source-manifest conflict to the protected {CONTROL_REPOSITORY} " - f"control-plane release-plan supersession workflow for corrected successor allocation; " - f"{tag_instruction} and do not rerun repository recovery for this plan" - ), - ) - return evidence - - -def read_plan_authority(client: PublicClient, tag: str, commit: str) -> tuple[dict[str, Any], dict[str, Any] | None]: - plan = read_record(client, tag, commit, "release-plan.json") - try: - validate_plan(plan) - except RecoveryError as error: - raise RecoveryError(str(error), "plan-discovery") from error - if tag != f"{PLAN_TAG_PREFIX}{plan['plan']}": - raise RecoveryError("release plan tag and document identity differ", "plan-discovery") - try: - preparation = read_record(client, tag, commit, "release-preparation.json") - except NotFound: - preparation = None - if preparation is not None: - validate_release_preparation(preparation, plan) - return plan, preparation - - -def validate_release_mirrors( - client: PublicClient, - tag: str, - release: Any, - plan: dict[str, Any], - preparation: dict[str, Any] | None, -) -> None: - if not isinstance(release, dict) or release.get("tag_name") != tag: - raise RecoveryError(f"release plan {tag} has invalid GitHub Release metadata", "plan-discovery") - if release.get("draft"): - raise RecoveryError(f"release plan {tag} is still a draft", "plan-discovery") - assets_value = release.get("assets") - if not isinstance(assets_value, list) or not all(isinstance(asset, dict) for asset in assets_value): - raise RecoveryError(f"release plan {tag} has malformed Release assets", "plan-discovery") - assets = {asset.get("name"): asset for asset in assets_value} - if len(assets) != len(assets_value): - raise RecoveryError(f"release plan {tag} has duplicate Release asset names", "plan-discovery") - records = [("release-plan.json", plan)] - if preparation is not None: - records.append(("release-preparation.json", preparation)) - for filename, value in records: - asset = assets.get(filename) - if not isinstance(asset, dict) or not isinstance(asset.get("browser_download_url"), str): - raise RecoveryError( - f"release plan {tag} lacks its durable {filename} mirror asset", - "plan-discovery", - ) - mirror = client.bytes(asset["browser_download_url"]) - if mirror != canonical_json(value): - raise RecoveryError( - f"release plan {tag} {filename} mirror differs from immutable Git authority", - "plan-discovery", - ) - if preparation is None and "release-preparation.json" in assets: - raise RecoveryError( - f"release plan {tag} release-preparation.json mirror lacks immutable Git authority", - "plan-discovery", - ) - - -def immutable_plan_recorded_at(client: PublicClient, commit: str) -> dt.datetime: - value = client.json(f"https://api.github.com/repos/{CONTROL_REPOSITORY}/git/commits/{commit}") - committer = value.get("committer") if isinstance(value, dict) else None - recorded_at = committer.get("date") if isinstance(committer, dict) else None - try: - parsed = dt.datetime.fromisoformat(str(recorded_at).replace("Z", "+00:00")) - except ValueError as error: - raise RecoveryError("release plan Git commit lacks an immutable recorded-at time", "plan-discovery") from error - if not isinstance(value, dict) or value.get("sha") != commit or parsed.tzinfo is None or parsed.utcoffset() is None: - raise RecoveryError("release plan Git commit has invalid immutable metadata", "plan-discovery") - return parsed.astimezone(dt.UTC) - - -def list_release_plan_tags(client: PublicClient) -> list[str]: - url = f"https://api.github.com/repos/{CONTROL_REPOSITORY}/git/matching-refs/tags/{PLAN_TAG_PREFIX}" - refs = client.json(url) - if not isinstance(refs, list): - raise RecoveryError("GitHub did not return the immutable release-plan tag registry", "plan-discovery") - tags: list[str] = [] - for ref in refs: - value = ref.get("ref") if isinstance(ref, dict) else None - tag = value.removeprefix("refs/tags/") if isinstance(value, str) else "" - if ( - value != f"refs/tags/{tag}" - or not tag.startswith(PLAN_TAG_PREFIX) - or not PLAN_PATTERN.fullmatch(tag.removeprefix(PLAN_TAG_PREFIX)) - ): - raise RecoveryError( - "GitHub returned a malformed immutable release-plan tag registry entry", - "plan-discovery", - ) - tags.append(tag) - if not tags: - raise RecoveryError("no public release plan is available", "plan-discovery") - if len(tags) != len(set(tags)): - raise RecoveryError("immutable release-plan tag registry contains duplicate authorities", "plan-discovery") - return tags - - -def list_continuity_resolution_tags(client: PublicClient, interrupted_plan: str) -> list[str]: - prefix = f"{CONTINUITY_RESOLUTION_TAG_PREFIX}{interrupted_plan}/" - url = f"https://api.github.com/repos/{CONTROL_REPOSITORY}/git/matching-refs/tags/{prefix}" - refs = client.json(url) - if not isinstance(refs, list): - raise RecoveryError( - "GitHub did not return the immutable continuity-resolution tag registry", - "plan-discovery", - ) - tags: list[str] = [] - for ref in refs: - value = ref.get("ref") if isinstance(ref, dict) else None - tag = value.removeprefix("refs/tags/") if isinstance(value, str) else "" - digest = tag.removeprefix(prefix) - if value != f"refs/tags/{tag}" or not tag.startswith(prefix) or not re.fullmatch(r"[0-9a-f]{64}", digest): - raise RecoveryError( - "GitHub returned a malformed immutable continuity-resolution tag registry entry", - "plan-discovery", - ) - tags.append(tag) - if len(tags) != len(set(tags)): - raise RecoveryError( - "immutable continuity-resolution tag registry contains duplicate authorities", - "plan-discovery", - ) - return tags - - -def validate_continuity_resolution_qualification(qualification: Any, client: PublicClient) -> dict[str, Any]: - expected = { - "repository": CONTROL_REPOSITORY, - "workflow": CONTINUITY_RESOLUTION_QUALIFICATION_WORKFLOW, - "event": CONTINUITY_RESOLUTION_QUALIFICATION_EVENT, - "head_branch": CONTINUITY_RESOLUTION_QUALIFICATION_BRANCH, - "status": "completed", - "conclusion": "success", - } - if ( - not isinstance(qualification, dict) - or set(qualification) != {*expected, "head_sha", "run_id", "run_attempt"} - or any(qualification.get(field) != value for field, value in expected.items()) - or not COMMIT_PATTERN.fullmatch(str(qualification.get("head_sha", ""))) - or type(qualification.get("run_id")) is not int - or qualification["run_id"] < 1 - or type(qualification.get("run_attempt")) is not int - or qualification["run_attempt"] < 1 - ): - raise RecoveryError( - "continuity successor resolution has invalid Beta candidate qualification", - "plan-discovery", - ) - run_id = qualification["run_id"] - run_attempt = qualification["run_attempt"] - try: - run = client.json( - f"https://api.github.com/repos/{CONTROL_REPOSITORY}/actions/runs/{run_id}/attempts/{run_attempt}" - ) - except NotFound as error: - raise RecoveryError( - "continuity successor resolution Beta candidate qualification is absent", "plan-discovery" - ) from error - if not isinstance(run, dict): - raise RecoveryError("continuity successor resolution Beta candidate qualification is absent", "plan-discovery") - repository = run.get("repository") - head_repository = run.get("head_repository") - if ( - type(run.get("id")) is not int - or run.get("id") != run_id - or type(run.get("run_attempt")) is not int - or run.get("run_attempt") != run_attempt - or not isinstance(repository, dict) - or repository.get("full_name") != CONTROL_REPOSITORY - or not isinstance(head_repository, dict) - or head_repository.get("full_name") != CONTROL_REPOSITORY - or run.get("path") - not in { - CONTINUITY_RESOLUTION_QUALIFICATION_WORKFLOW, - f"{CONTINUITY_RESOLUTION_QUALIFICATION_WORKFLOW}@main", - f"{CONTINUITY_RESOLUTION_QUALIFICATION_WORKFLOW}@refs/heads/main", - } - or run.get("event") != CONTINUITY_RESOLUTION_QUALIFICATION_EVENT - or run.get("head_branch") != CONTINUITY_RESOLUTION_QUALIFICATION_BRANCH - ): - raise RecoveryError( - "continuity successor resolution qualification is from an untrusted workflow", - "plan-discovery", - ) - if run.get("head_sha") != qualification["head_sha"]: - raise RecoveryError( - "continuity successor resolution qualification is bound to another source revision", "plan-discovery" - ) - if run.get("status") != "completed": - raise RecoveryError("continuity successor resolution Beta candidate qualification is pending", "plan-discovery") - if run.get("conclusion") == "cancelled": - raise RecoveryError( - "continuity successor resolution Beta candidate qualification was cancelled", "plan-discovery" - ) - if run.get("conclusion") != "success": - raise RecoveryError("continuity successor resolution Beta candidate qualification failed", "plan-discovery") - return qualification - - -def completion_manifest( - plan: dict[str, Any], - commit: str, - preparation: dict[str, Any] | None, -) -> dict[str, Any]: - result = { - "schema": "durable-workflow.release-candidate/v1", - "candidate": plan["plan"], - "channel": plan["channel"], - "release_plan": { - "tag": f"{PLAN_TAG_PREFIX}{plan['plan']}", - "commit": commit, - "sha256": manifest_digest(plan), - }, - "components": plan["components"], - } - if preparation is not None: - result["release_preparation_sha256"] = manifest_digest(preparation) - return result - - -def direct_plan_lifecycle( - client: PublicClient, - tag: str, - commit: str, - plan: dict[str, Any], - preparation: dict[str, Any] | None, -) -> tuple[str, str | dict[str, Any] | None]: - completion_tag = f"{COMPLETION_TAG_PREFIX}{plan['channel']}/{plan['plan']}" - failure_tag = f"{FAILURE_TAG_PREFIX}{plan['plan']}" - completion_commit = resolve_tag(client, CONTROL_REPOSITORY, completion_tag) - failure_commit = resolve_tag(client, CONTROL_REPOSITORY, failure_tag) - if completion_commit is not None and failure_commit is not None: - raise RecoveryError( - f"release plan {tag} has conflicting completion and terminal-failure records", - "plan-discovery", - ) - if completion_commit is not None: - completion = read_record(client, completion_tag, completion_commit, "release-candidate.json") - if completion != completion_manifest(plan, commit, preparation): - raise RecoveryError( - f"release plan {tag} has an invalid immutable completion record", - "plan-discovery", - ) - return "completed", None - if failure_commit is not None: - failure = read_record(client, failure_tag, failure_commit, "release-plan-failure.json") - successor = read_record(client, failure_tag, failure_commit, "successor-release-plan.json") - validate_plan(successor) - validate_supersession_record(failure, plan, commit, successor) - revalidate_supersession_authority(failure, client) - expected_successor = { - "tag": f"{PLAN_TAG_PREFIX}{successor['plan']}", - "sha256": manifest_digest(successor), - } - return "superseded", { - **expected_successor, - "plan": successor, - } - - interruption_tag = f"{CONTINUITY_TAG_PREFIX}{plan['plan']}/interrupted" - interruption_commit = resolve_tag(client, CONTROL_REPOSITORY, interruption_tag) - if interruption_commit is None: - return "actionable", None - evidence = read_record(client, interruption_tag, interruption_commit, "continuity-evidence.json") - interrupted_plan = read_record(client, interruption_tag, interruption_commit, "release-plan.json") - digest = manifest_digest(plan) - if ( - interrupted_plan != plan - or not isinstance(evidence, dict) - or evidence.get("schema") != CONTINUITY_EVIDENCE_SCHEMA - or evidence.get("phase") != "interrupted" - or evidence.get("outcome") != "intentionally-interrupted" - or evidence.get("release_plan") != {"tag": tag, "sha256": digest} - or evidence.get("plan_record") != {"tag": tag, "commit": commit, "sha256": digest} - ): - raise RecoveryError( - f"release plan {tag} has an invalid immutable interruption record", - "plan-discovery", - ) - return "interrupted", interruption_tag - - -def accepted_continuity_supersession( - client: PublicClient, - authority: dict[str, Any], -) -> dict[str, Any] | None: - plan = authority["plan"] - accepted_tag = f"{CONTINUITY_TAG_PREFIX}{plan['plan']}/accepted" - accepted_commit = resolve_tag(client, CONTROL_REPOSITORY, accepted_tag) - if accepted_commit is None: - return None - evidence = read_record(client, accepted_tag, accepted_commit, "continuity-evidence.json") - accepted_plan = read_record(client, accepted_tag, accepted_commit, "release-plan.json") - digest = manifest_digest(plan) - if ( - accepted_plan != plan - or not isinstance(evidence, dict) - or evidence.get("schema") != CONTINUITY_EVIDENCE_SCHEMA - or evidence.get("phase") != "accepted" - or evidence.get("outcome") != "accepted" - or evidence.get("release_plan") != {"tag": authority["tag"], "sha256": digest} - or evidence.get("candidate_identity") != {"components": plan["components"], "plan_sha256": digest} - ): - raise RecoveryError( - f"release plan {authority['tag']} has an invalid immutable continuity acceptance", - "plan-discovery", - ) - superseded = evidence.get("superseded_interruption") - if superseded is None: - return None - if ( - not isinstance(superseded, dict) - or set(superseded) != {"commit", "evidence_sha256", "plan_sha256", "reason", "tag"} - or superseded.get("reason") != CONTINUITY_SUPERSESSION_REASON - or not isinstance(superseded.get("commit"), str) - or not COMMIT_PATTERN.fullmatch(superseded["commit"]) - or not isinstance(superseded.get("evidence_sha256"), str) - or not SHA256_PATTERN.fullmatch(superseded["evidence_sha256"]) - or not isinstance(superseded.get("plan_sha256"), str) - or not SHA256_PATTERN.fullmatch(superseded["plan_sha256"]) - or not isinstance(superseded.get("tag"), str) - or not superseded["tag"].startswith(CONTINUITY_TAG_PREFIX) - ): - raise RecoveryError( - f"release plan {authority['tag']} has an invalid superseded interruption identity", - "plan-discovery", - ) - return { - **superseded, - "continuity_claim": { - "plan": { - "tag": authority["tag"], - "commit": authority["commit"], - "sha256": digest, - }, - "acceptance": { - "tag": accepted_tag, - "commit": accepted_commit, - "sha256": manifest_digest(evidence), - }, - }, - } - - -def resolve_continuity_successor_fork( - client: PublicClient, - interrupted: dict[str, Any], - successors: list[dict[str, Any]], -) -> str: - resolution_tags = list_continuity_resolution_tags(client, interrupted["plan"]["plan"]) - if not resolution_tags: - raise RecoveryError( - f"release plan {interrupted['tag']} has multiple continuity successors", - "plan-discovery", - ) - if len(resolution_tags) != 1: - raise RecoveryError( - f"release plan {interrupted['tag']} has multiple continuity successor resolutions", - "plan-discovery", - ) - resolution_tag = resolution_tags[0] - resolution_commit = resolve_tag(client, CONTROL_REPOSITORY, resolution_tag) - if resolution_commit is None: - raise RecoveryError(f"continuity successor resolution {resolution_tag} is absent", "plan-discovery") - resolution = read_record(client, resolution_tag, resolution_commit, "continuity-successor-resolution.json") - first_supersession = successors[0]["supersession"] - expected_interruption = { - "plan": { - "tag": interrupted["tag"], - "commit": interrupted["commit"], - "sha256": manifest_digest(interrupted["plan"]), - }, - "evidence": { - "tag": first_supersession["tag"], - "commit": first_supersession["commit"], - "sha256": first_supersession["evidence_sha256"], - }, - } - expected_claims = sorted( - (successor["supersession"]["continuity_claim"] for successor in successors), - key=lambda claim: claim["plan"]["tag"], - ) - selected = resolution.get("selected_successor") if isinstance(resolution, dict) else None - expected_tag = ( - f"{CONTINUITY_RESOLUTION_TAG_PREFIX}{interrupted['plan']['plan']}/{manifest_digest(resolution)}" - if isinstance(resolution, dict) - else "" - ) - if ( - not isinstance(resolution, dict) - or set(resolution) - != {"interruption", "qualification", "schema", "selected_successor", "successor_claims"} - or resolution.get("schema") != CONTINUITY_RESOLUTION_SCHEMA - or resolution.get("interruption") != expected_interruption - or resolution.get("successor_claims") != expected_claims - or selected not in [claim["plan"] for claim in expected_claims] - or resolution_tag != expected_tag - ): - raise RecoveryError( - f"release plan {interrupted['tag']} has an invalid immutable continuity successor resolution", - "plan-discovery", - ) - validate_continuity_resolution_qualification(resolution["qualification"], client) - return str(selected["tag"]) - - -def classify_plan_authorities(client: PublicClient) -> list[dict[str, Any]]: - authorities: list[dict[str, Any]] = [] - tags = list_release_plan_tags(client) - for tag in tags: - commit = resolve_tag(client, CONTROL_REPOSITORY, tag) - if commit is None: - raise RecoveryError(f"release plan tag {tag} is absent", "plan-discovery") - plan, preparation = read_plan_authority(client, tag, commit) - lifecycle, successor = direct_plan_lifecycle(client, tag, commit, plan, preparation) - authorities.append( - { - "tag": tag, - "commit": commit, - "recorded_at": immutable_plan_recorded_at(client, commit), - "plan": plan, - "preparation": preparation, - "lifecycle": lifecycle, - "successor": successor, - } - ) - - authorities.sort(key=lambda item: item["recorded_at"]) - if len({item["recorded_at"] for item in authorities}) != len(authorities): - raise RecoveryError( - "release plans have ambiguous immutable Git recorded-at authority", - "plan-discovery", - ) - by_tag = {item["tag"]: item for item in authorities} - continuity_successors: dict[str, list[dict[str, Any]]] = {} - for successor in authorities: - superseded = accepted_continuity_supersession(client, successor) - if superseded is None: - continue - interruption_tag = superseded["tag"] - matches = [ - item for item in authorities if item["lifecycle"] == "interrupted" and item["successor"] == interruption_tag - ] - if len(matches) != 1: - raise RecoveryError( - f"continuity successor {successor['tag']} names an unknown or ambiguous interruption", - "plan-discovery", - ) - interrupted = matches[0] - interruption_commit = resolve_tag(client, CONTROL_REPOSITORY, interruption_tag) - interruption_evidence = read_record( - client, - interruption_tag, - superseded["commit"], - "continuity-evidence.json", - ) - if ( - interruption_commit != superseded["commit"] - or manifest_digest(interruption_evidence) != superseded["evidence_sha256"] - or manifest_digest(interrupted["plan"]) != superseded["plan_sha256"] - or successor["recorded_at"] <= interrupted["recorded_at"] - ): - raise RecoveryError( - f"continuity successor {successor['tag']} has conflicting interruption authority", - "plan-discovery", - ) - continuity_successors.setdefault(interrupted["tag"], []).append( - {"tag": successor["tag"], "supersession": superseded} - ) - - for interrupted_tag, successors in continuity_successors.items(): - interrupted = by_tag[interrupted_tag] - interrupted["lifecycle"] = "superseded" - successor_tag = ( - successors[0]["tag"] - if len(successors) == 1 - else resolve_continuity_successor_fork(client, interrupted, successors) - ) - successor = by_tag[successor_tag] - interrupted["successor"] = { - "tag": successor_tag, - "sha256": manifest_digest(successor["plan"]), - "plan": successor["plan"], - } - - for authority in authorities: - successor_identity = authority["successor"] - if authority["lifecycle"] != "superseded" or successor_identity is None: - continue - if not isinstance(successor_identity, dict): - raise RecoveryError( - f"superseded release plan {authority['tag']} has a malformed successor identity", - "plan-discovery", - ) - successor_tag = successor_identity.get("tag") - successor = by_tag.get(successor_tag) - if successor is None: - if authority is authorities[-1]: - raise RecoveryError( - f"latest release plan {authority['tag']} is superseded but its successor is not recorded", - "plan-discovery", - ) - raise RecoveryError( - f"superseded release plan {authority['tag']} has an incomplete successor authority", - "plan-discovery", - ) - expected_successor_identity = { - "tag": successor["tag"], - "sha256": manifest_digest(successor["plan"]), - "plan": successor["plan"], - } - if successor_identity != expected_successor_identity: - raise RecoveryError( - f"superseded release plan {authority['tag']} has a conflicting successor identity", - "plan-discovery", - ) - if successor["recorded_at"] <= authority["recorded_at"]: - raise RecoveryError( - f"superseded release plan {authority['tag']} names a non-successor Git authority", - "plan-discovery", - ) - - return authorities - - -def semver_precedence( - version: str, -) -> tuple[ - tuple[int, str], - tuple[int, str], - tuple[int, str], - int, - tuple[tuple[int, tuple[int, str] | str], ...], -]: - parsed = parse_semver(version) - if parsed is None: - raise ValueError("version is not exact SemVer") - return parsed.precedence - - -def current_product_train_authorities( - authorities: list[dict[str, Any]], -) -> list[dict[str, Any]]: - """Select one maximal SemVer train after resolving validated supersession edges.""" - - for authority in authorities: - try: - validate_plan(authority.get("plan")) - except RecoveryError as error: - raise RecoveryError(str(error), "plan-discovery") from error - - def immutable_identity( - authority: dict[str, Any], - ) -> tuple[tuple[str, str], ...]: - return tuple( - ( - authority["plan"]["components"][name]["version"], - authority["plan"]["components"][name]["commit"], - ) - for name in COMPONENTS - ) - - version_precedence = { - authority["tag"]: { - name: semver_precedence(identity["version"]) - for name, identity in authority["plan"]["components"].items() - } - for authority in authorities - } - - def dominates(candidate: dict[str, Any], other: dict[str, Any]) -> bool: - return all( - version_precedence[candidate["tag"]][name] >= version_precedence[other["tag"]][name] - for name in COMPONENTS - ) and any( - version_precedence[candidate["tag"]][name] > version_precedence[other["tag"]][name] - for name in COMPONENTS - ) - - maximal = [ - authority - for authority in authorities - if not any( - other is not authority and dominates(other, authority) - for other in authorities - ) - ] - by_tag = {authority["tag"]: authority for authority in authorities} - maximal_tags = {authority["tag"] for authority in maximal} - resolved_predecessors: set[str] = set() - for authority in maximal: - successor_identity = authority.get("successor") - if authority.get("lifecycle") != "superseded" or not isinstance(successor_identity, dict): - continue - successor = by_tag.get(successor_identity.get("tag")) - if successor is None or successor["tag"] not in maximal_tags: - continue - expected_successor_identity = { - "tag": successor["tag"], - "sha256": manifest_digest(successor["plan"]), - "plan": successor["plan"], - } - if successor_identity == expected_successor_identity: - resolved_predecessors.add(authority["tag"]) - - unresolved_maximal = [ - authority - for authority in maximal - if authority["tag"] not in resolved_predecessors - ] - current_identities = { - immutable_identity(authority) - for authority in unresolved_maximal - } - if len(current_identities) != 1: - raise RecoveryError( - "release plan authority has conflicting current product trains", - "plan-discovery", - ) - selected_identity = next(iter(current_identities)) - return [ - authority - for authority in authorities - if immutable_identity(authority) == selected_identity - ] - - -def classify_implicit_plan_authority( - client: PublicClient, -) -> tuple[dict[str, Any] | None, list[dict[str, Any]]]: - authorities = classify_plan_authorities(client) - current_train = current_product_train_authorities(authorities) - nonterminal_older = [ - item - for item in current_train[:-1] - if item["lifecycle"] in {"actionable", "interrupted"} - ] - if nonterminal_older: - raise RecoveryError( - f"release plan authority is ambiguous: {nonterminal_older[0]['tag']} remains " - f"{nonterminal_older[0]['lifecycle']} before {current_train[-1]['tag']}", - "plan-discovery", - ) - selected = current_train[-1] - if selected["lifecycle"] == "superseded": - selected = None - return selected, authorities - - -def implicit_plan_authority_converged( - client: PublicClient, - authority_snapshot: list[dict[str, Any]], -) -> bool: - _selected, current_snapshot = classify_implicit_plan_authority(client) - return current_snapshot == authority_snapshot - - -def select_implicit_plan_authority(client: PublicClient) -> dict[str, Any]: - for _attempt in range(IMPLICIT_AUTHORITY_MAX_ATTEMPTS): - selected, authority_snapshot = classify_implicit_plan_authority(client) - if implicit_plan_authority_converged(client, authority_snapshot): - if selected is None: - raise RecoveryError( - "no public release plan is available", - "plan-discovery", - ) - return {**selected, "authority_snapshot": authority_snapshot} - raise RecoveryError( - "release plan registry or lifecycle authority did not converge " - f"after {IMPLICIT_AUTHORITY_MAX_ATTEMPTS} attempts", - "plan-discovery", - ) - - -def revalidate_implicit_plan_authority( - client: PublicClient, - implicit_authority: dict[str, Any], -) -> None: - authority_snapshot = implicit_authority.get("authority_snapshot") - if not isinstance(authority_snapshot, list) or not implicit_plan_authority_converged( - client, - authority_snapshot, - ): - raise RecoveryError( - "implicit release plan authority changed during component preflight; " - "refusing a stale recovery action", - "plan-discovery", - ) - - -def select_explicit_plan_authority( - client: PublicClient, - tag: str, - commit: str, - plan: dict[str, Any], - preparation: dict[str, Any] | None, -) -> dict[str, Any]: - matches = [ - authority - for authority in classify_plan_authorities(client) - if authority["tag"] == tag - ] - if len(matches) != 1: - raise RecoveryError( - f"explicit release plan {tag} lacks exact lifecycle authority", - "plan-discovery", - ) - authority = matches[0] - if ( - authority["commit"] != commit - or authority["plan"] != plan - or authority["preparation"] != preparation - ): - raise RecoveryError( - f"explicit release plan {tag} changed while its lifecycle was classified", - "plan-discovery", - ) - if authority["lifecycle"] not in {"actionable", "interrupted", "completed"}: - lifecycle = "terminally superseded" - raise RecoveryError( - f"explicit release plan {tag} is {lifecycle} and cannot be recovered", - "plan-discovery", - ) - return {**authority, "selection": "explicit"} - - -def revalidate_explicit_plan_authority( - client: PublicClient, - explicit_authority: dict[str, Any], - action: str, -) -> None: - matches = [ - authority - for authority in classify_plan_authorities(client) - if authority["tag"] == explicit_authority["tag"] - ] - if len(matches) != 1: - raise RecoveryError( - f"explicit release plan {explicit_authority['tag']} lifecycle authority changed " - "during component preflight; refusing a stale recovery action", - "plan-discovery", - ) - current = matches[0] - if ( - current["commit"] != explicit_authority["commit"] - or current["plan"] != explicit_authority["plan"] - or current["preparation"] != explicit_authority["preparation"] - ): - raise RecoveryError( - f"explicit release plan {explicit_authority['tag']} identity changed " - "during component preflight; refusing a stale recovery action", - "plan-discovery", - ) - if current["lifecycle"] == "superseded": - raise RecoveryError( - f"explicit release plan {explicit_authority['tag']} became terminally superseded " - "during component preflight; refusing a stale recovery action", - "plan-discovery", - ) - require_completed_plan_verification(current, action) - - -def require_completed_plan_verification( - authority: dict[str, Any], - action: str, -) -> None: - if authority.get("lifecycle") == "completed" and action == "publish": - raise RecoveryError( - f"release plan {authority['tag']} is completed; " - "refusing publication instead of idempotent verification", - "plan-discovery", - ) - - -def discover_plan( - client: PublicClient, requested_tag: str | None, component_name: str -) -> tuple[ - str, - str, - dict[str, Any], - dict[str, Any] | None, - dict[str, Any] | None, -]: - if component_name not in COMPONENTS: - raise RecoveryError(f"unknown release component: {component_name}", "plan-discovery") - plan_authority = None - if requested_tag: - tag = requested_tag - if not tag.startswith(PLAN_TAG_PREFIX): - raise RecoveryError(f"release plan tag must start with {PLAN_TAG_PREFIX}", "plan-discovery") - try: - release = client.json( - f"https://api.github.com/repos/{CONTROL_REPOSITORY}/releases/tags/{urllib.parse.quote(tag, safe='')}" - ) - except NotFound as error: - raise RecoveryError(f"release plan {tag} has no durable GitHub Release", "plan-discovery") from error - commit = resolve_tag(client, CONTROL_REPOSITORY, tag) - if commit is None: - raise RecoveryError(f"release plan tag {tag} is absent", "plan-discovery") - plan, preparation = read_plan_authority(client, tag, commit) - plan_authority = select_explicit_plan_authority( - client, tag, commit, plan, preparation - ) - else: - selected = select_implicit_plan_authority(client) - plan_authority = selected - tag = selected["tag"] - commit = selected["commit"] - plan = selected["plan"] - preparation = selected["preparation"] - try: - release = client.json( - f"https://api.github.com/repos/{CONTROL_REPOSITORY}/releases/tags/{urllib.parse.quote(tag, safe='')}" - ) - except NotFound as error: - raise RecoveryError(f"release plan {tag} has no durable GitHub Release", "plan-discovery") from error - validate_release_mirrors(client, tag, release, plan, preparation) - if preparation is None: - try: - verify_component(client, component_name, plan["components"][component_name]) - except NotFound as error: - raise RecoveryError( - f"release plan {tag} lacks immutable release-preparation.json; " - "only completed legacy releases may recover without it", - "plan-discovery", - ) from error - return tag, commit, plan, preparation, plan_authority - - -def load_recovery_workflow_authority( - client: PublicClient, -) -> tuple[dict[str, dict[str, str]], dict[str, Any]]: - identities = {name: (component.repository, component.default_branch) for name, component in COMPONENTS.items()} - try: - return load_qualified_authority(client, identities) - except RecoveryWorkflowAuthorityError as error: - raise RecoveryError(str(error), "default-branch-preflight") from error - - -def verify_recovery_workflow_source(name: str, source: str, expected_sha256: str) -> str: - try: - return verify_workflow_source(name, source, expected_sha256) - except RecoveryWorkflowAuthorityError as error: - raise RecoveryError(str(error), "default-branch-preflight") from error - - -def select_publication_run( - release_tag: str, - release_commit: str, - release_plan: str, - runs: Any, -) -> dict[str, Any]: - if ( - not isinstance(release_tag, str) - or not VERSION_PATTERN.fullmatch(release_tag) - or not isinstance(release_commit, str) - or not COMMIT_PATTERN.fullmatch(release_commit) - ): - raise RecoveryError("publication run selection requires an exact release identity", "publication") - if not isinstance(release_plan, str) or not release_plan.startswith(PLAN_TAG_PREFIX) or not PLAN_PATTERN.fullmatch( - release_plan.removeprefix(PLAN_TAG_PREFIX) - ): - raise RecoveryError("publication run selection requires an exact release plan", "publication") - if not isinstance(runs, list): - raise RecoveryError("publication run metadata must be a JSON array", "publication") - - # A protected-main dispatch reports main's head SHA, so publish.yml carries - # the immutable release identity and publication intent in the run title. - expected_title = f"Publish {release_tag}@{release_commit} from {release_plan}" - exact_runs: list[dict[str, Any]] = [] - for run in runs: - if ( - not isinstance(run, dict) - or run.get("headBranch") != COMPONENTS["sdk-python"].default_branch - or run.get("displayTitle") != expected_title - ): - continue - if ( - type(run.get("databaseId")) is not int - or run["databaseId"] < 1 - or not isinstance(run.get("headSha"), str) - or not COMMIT_PATTERN.fullmatch(run["headSha"]) - or not isinstance(run.get("status"), str) - ): - raise RecoveryError("publication run metadata is incomplete", "publication") - exact_runs.append(run) - - active = next((run for run in exact_runs if run["status"] != "completed"), None) - if active is not None: - return { - "action": "wait", - "run_id": active["databaseId"], - "status": active["status"], - "conclusion": active.get("conclusion"), - } - - successful = next( - (run for run in exact_runs if run["status"] == "completed" and run.get("conclusion") == "success"), - None, - ) - if successful is not None: - return { - "action": "complete", - "run_id": successful["databaseId"], - "status": successful["status"], - "conclusion": successful.get("conclusion"), - } - - return {"action": "dispatch", "run_id": None, "status": None, "conclusion": None} - - -def verify_plan_authority( - client: PublicClient, plan: dict[str, Any] -) -> tuple[dict[str, str], dict[str, dict[str, Any]]]: - foundation_identity = plan["foundation"] - if ( - foundation_identity["tag"] != FOUNDATION_TAG - and resolve_tag(client, CONTROL_REPOSITORY, foundation_identity["tag"]) - != foundation_identity["commit"] - ): - raise RecoveryError( - "aggregate candidate foundation tag does not match its pinned commit", - "plan-preflight", - ) - foundation = read_record( - client, - foundation_identity["tag"], - foundation_identity["commit"], - "candidate.json", - ) - if foundation_identity["tag"] == FOUNDATION_TAG: - if foundation.get("candidate") != "beta-continuity-foundation": - raise RecoveryError("immutable candidate foundation has an unexpected identity", "plan-preflight") - else: - expected_foundation = { - "schema": "durable-workflow.beta-candidate/v2", - "candidate": f"rc-{plan['plan']}", - "components": plan["components"], - } - if foundation != expected_foundation: - raise RecoveryError( - "aggregate release-candidate foundation names a different exact tuple", - "plan-preflight", - ) - verification = read_record( - client, - foundation_identity["tag"], - foundation_identity["commit"], - "verification.json", - ) - verification_components = verification.get("components") if isinstance(verification, dict) else None - if ( - not isinstance(verification, dict) - or verification.get("schema") != "durable-workflow.beta-candidate-verification/v2" - or verification.get("candidate") != foundation["candidate"] - or verification.get("manifest_sha256") != manifest_digest(foundation) - or verification.get("outcome") != "verified" - or not isinstance(verification_components, dict) - or set(verification_components) != set(COMPONENTS) - or any( - result.get("version") != plan["components"][name]["version"] - or result.get("commit") != plan["components"][name]["commit"] - or result.get("outcome") != "verified" - for name, result in verification_components.items() - if isinstance(result, dict) - ) - or any(not isinstance(result, dict) for result in verification_components.values()) - ): - raise RecoveryError( - "aggregate release-candidate foundation lacks exact verification evidence", - "plan-preflight", - ) - authority, authority_source = load_recovery_workflow_authority(client) - branches: dict[str, str] = {} - recovery_workflows: dict[str, dict[str, Any]] = {} - for name, component in COMPONENTS.items(): - repository = client.json(f"https://api.github.com/repos/{component.repository}") - actual = repository.get("default_branch") - if actual != component.default_branch: - raise RecoveryError( - f"{component.repository} default branch is {actual!r}; recovery requires {component.default_branch!r}", - "default-branch-preflight", - ) - branches[name] = str(actual) - expected = authority[name] - expected_path = expected["path"] - workflow = client.json( - f"https://api.github.com/repos/{component.repository}/actions/workflows/release-plan-recovery.yml" - ) - if workflow.get("path") != expected_path or workflow.get("state") != expected["state"]: - raise RecoveryError( - f"{component.repository} does not expose an active {expected_path} on its default branch", - "default-branch-preflight", - ) - source = client.bytes( - f"https://api.github.com/repos/{component.repository}/contents/{expected_path}" - f"?ref={component.default_branch}", - accept="application/vnd.github.raw+json", - ).decode("utf-8") - source_sha256 = verify_recovery_workflow_source(name, source, expected["sha256"]) - recovery_workflows[name] = { - "authority": authority_source, - "default_branch": component.default_branch, - "path": expected_path, - "sha256": source_sha256, - "state": workflow["state"], - "workflow_id": workflow.get("id"), - "url": workflow.get("html_url"), - } - authorization = plan["beta_authorization"] - if authorization is not None: - record = read_record(client, authorization["tag"], authorization["commit"], "beta-authorization.json") - if not beta_authorization_matches_plan(plan, authorization, record): - raise RecoveryError( - "beta qualification does not authorize this prerelease transition", "channel-authorization" - ) - return branches, recovery_workflows - - -def require_source_tag(client: PublicClient, name: str, identity: dict[str, str]) -> str: - component = COMPONENTS[name] - source = resolve_tag(client, component.repository, identity["version"]) - if source is None: - raise NotFound( - f"source tag {component.repository}@{identity['version']} is not present", - "source-tag", - ) - if source != identity["commit"]: - raise RecoveryError( - f"source tag {component.repository}@{identity['version']} points to {source}, not {identity['commit']}", - "source-tag", - ) - return source - - -def verify_github_release(client: PublicClient, name: str, version: str) -> dict[str, Any]: - component = COMPONENTS[name] - encoded = urllib.parse.quote(version, safe="") - try: - release = client.json(f"https://api.github.com/repos/{component.repository}/releases/tags/{encoded}") - except NotFound as error: - raise NotFound(f"GitHub Release {component.repository}@{version} is absent", "github-release") from error - if release.get("draft") or release.get("tag_name") != version: - raise RecoveryError(f"GitHub Release {component.repository}@{version} is not public", "github-release") - return {"id": release.get("id"), "url": release.get("html_url")} - - -def verify_composer(client: PublicClient, component: Component, version: str, commit: str) -> dict[str, Any]: - encoded = "/".join(urllib.parse.quote(part, safe="") for part in component.package.split("/")) - url = f"https://repo.packagist.org/p2/{encoded}.json" - payload = client.json(url) - releases = payload.get("packages", {}).get(component.package, []) - release = next((item for item in releases if str(item.get("version", "")).lstrip("v") == version.lstrip("v")), None) - if release is None: - raise NotFound(f"Packagist does not expose {component.package}@{version}", "registry-publication") - source = release.get("source", {}).get("reference") - dist = release.get("dist", {}).get("reference") - if ( - not isinstance(source, str) - or not COMMIT_PATTERN.fullmatch(source) - or source != commit - or not isinstance(dist, str) - or not COMMIT_PATTERN.fullmatch(dist) - or dist != commit - ): - raise RecoveryError( - f"Packagist identity for {component.package}@{version} is {source}/{dist}, not {commit}", - "registry-publication", - ) - return {"kind": "composer", "registry": url, "source_reference": source, "dist_reference": dist} - - -def oci_json(client: PublicClient, url: str, token: str, accept: str) -> tuple[Any, str | None]: - response = client.request(url, headers={"Authorization": f"Bearer {token}"}, accept=accept) - with response: - return json.load(response), response.headers.get("Docker-Content-Digest") - - -def verify_oci(client: PublicClient, component: Component, version: str, commit: str) -> dict[str, Any]: - repository = component.package.split("/", 1)[1] - token_url = "https://auth.docker.io/token?service=registry.docker.io&scope=" + urllib.parse.quote( - f"repository:{repository}:pull" - ) - token = client.json(token_url).get("token") - if not token: - raise RecoveryError(f"Docker Hub did not grant public pull access to {component.package}:{version}") - accept = ", ".join( - ( - "application/vnd.oci.image.index.v1+json", - "application/vnd.docker.distribution.manifest.list.v2+json", - "application/vnd.oci.image.manifest.v1+json", - "application/vnd.docker.distribution.manifest.v2+json", - ) - ) - url = f"https://registry-1.docker.io/v2/{repository}/manifests/{urllib.parse.quote(version, safe='')}" - try: - manifest, digest = oci_json(client, url, str(token), accept) - except NotFound as error: - raise NotFound(f"Docker Hub does not expose {component.package}:{version}", "registry-publication") from error - if not isinstance(digest, str) or not OCI_DIGEST_PATTERN.fullmatch(digest): - raise RecoveryError(f"Docker Hub image {component.package}:{version} has no immutable digest") - descriptors = manifest.get("manifests") if isinstance(manifest, dict) else None - if not isinstance(descriptors, list): - raise RecoveryError(f"Docker Hub image {component.package}:{version} is not multi-platform") - platforms: set[str] = set() - for descriptor in descriptors: - if not isinstance(descriptor, dict): - raise RecoveryError(f"Docker Hub image {component.package}:{version} has a malformed platform") - descriptor_digest = descriptor.get("digest") - if not isinstance(descriptor_digest, str) or not OCI_DIGEST_PATTERN.fullmatch(descriptor_digest): - raise RecoveryError(f"Docker Hub image {component.package}:{version} has a malformed platform digest") - platform = descriptor.get("platform", {}) - if not isinstance(platform, dict): - raise RecoveryError(f"Docker Hub image {component.package}:{version} has a malformed platform") - label = f"{platform.get('os')}/{platform.get('architecture')}" - if label not in {"linux/amd64", "linux/arm64"}: - continue - child, child_digest = oci_json( - client, - f"https://registry-1.docker.io/v2/{repository}/manifests/{descriptor_digest}", - str(token), - accept, - ) - if ( - not isinstance(child_digest, str) - or not OCI_DIGEST_PATTERN.fullmatch(child_digest) - or child_digest != descriptor_digest - ): - raise RecoveryError(f"Docker Hub platform digest changed for {component.package}:{version}") - child_config = child.get("config") if isinstance(child, dict) else None - config_digest = child_config.get("digest") if isinstance(child_config, dict) else None - if not isinstance(config_digest, str) or not OCI_DIGEST_PATTERN.fullmatch(config_digest): - raise RecoveryError(f"Docker Hub image {component.package}:{version} has a malformed config digest") - config = client.json( - f"https://registry-1.docker.io/v2/{repository}/blobs/{config_digest}", - headers={"Authorization": f"Bearer {token}"}, - ) - config_value = config.get("config") if isinstance(config, dict) else None - labels = config_value.get("Labels") if isinstance(config_value, dict) else None - if not isinstance(labels, dict): - raise RecoveryError(f"Docker Hub image {component.package}:{version} has malformed source labels") - if labels.get("org.opencontainers.image.revision") != commit: - raise RecoveryError(f"Docker Hub image {component.package}:{version} names a different source commit") - if labels.get("dev.durable-workflow.release.tag") != version: - raise RecoveryError(f"Docker Hub image {component.package}:{version} names a different release tag") - platforms.add(label) - if platforms != {"linux/amd64", "linux/arm64"}: - raise RecoveryError(f"Docker Hub image {component.package}:{version} lacks required Linux platforms") - return {"kind": "oci", "image": f"{component.package}:{version}", "digest": digest, "platforms": sorted(platforms)} - - -def archive_files(path: Path, *, zipped: bool = False) -> dict[str, bytes]: - files: dict[str, bytes] = {} - if zipped: - with zipfile.ZipFile(path) as archive: - for member in archive.infolist(): - if not member.is_dir(): - files[member.filename] = archive.read(member) - return files - with tarfile.open(path, "r:*") as archive: - for member in archive.getmembers(): - if member.isfile() and (extracted := archive.extractfile(member)) is not None: - files[member.name] = extracted.read() - return files - - -def strip_root(files: dict[str, bytes]) -> dict[str, bytes]: - return { - relative: content - for name, content in files.items() - if (separator := name.partition("/"))[1] and (relative := separator[2]) - } - - -def verify_pypi(client: PublicClient, component: Component, version: str, commit: str) -> dict[str, Any]: - package = urllib.parse.quote(component.package, safe="") - encoded_version = urllib.parse.quote(version, safe="") - url = f"https://pypi.org/pypi/{package}/{encoded_version}/json" - try: - payload = client.json(url) - except NotFound as error: - raise NotFound(f"PyPI does not expose {component.package}=={version}", "registry-publication") from error - files = [item for item in payload.get("urls", []) if not item.get("yanked")] - sdist = next((item for item in files if item.get("packagetype") == "sdist"), None) - wheels = [item for item in files if item.get("packagetype") == "bdist_wheel"] - if sdist is None or not wheels: - raise RecoveryError(f"PyPI release {component.package}=={version} lacks a wheel or source archive") - digests = sdist.get("digests") - sdist_sha256 = digests.get("sha256") if isinstance(digests, dict) else None - if not isinstance(sdist_sha256, str) or not SHA256_PATTERN.fullmatch(sdist_sha256): - raise RecoveryError(f"PyPI release {component.package}=={version} has an invalid source digest") - with tempfile.TemporaryDirectory(prefix="release-recovery-pypi-") as temporary: - directory = Path(temporary) - source_path = directory / "source.tar.gz" - sdist_path = directory / str(sdist["filename"]) - client.download(f"https://github.com/{component.repository}/archive/{commit}.tar.gz", source_path) - client.download(sdist["url"], sdist_path, expected_sha256=sdist_sha256) - source_files = strip_root(archive_files(source_path)) - sdist_files = strip_root(archive_files(sdist_path)) - compared = 0 - for name, content in sdist_files.items(): - if ".egg-info/" in name or name.endswith(("/PKG-INFO", "PKG-INFO")): - continue - if name == "setup.cfg" and name not in source_files: - continue - if source_files.get(name) != content: - raise RecoveryError(f"PyPI source file {name} differs from source commit {commit}") - compared += 1 - if not compared: - raise RecoveryError(f"PyPI release {component.package}=={version} has no comparable source files") - return {"kind": "pypi", "registry": url, "source_files_compared": compared} - - -def verify_crate(client: PublicClient, component: Component, version: str, commit: str) -> dict[str, Any]: - package = urllib.parse.quote(component.package, safe="") - encoded_version = urllib.parse.quote(version, safe="") - url = f"https://crates.io/api/v1/crates/{package}/{encoded_version}" - try: - payload = client.json(url) - except NotFound as error: - raise NotFound(f"crates.io does not expose {component.package}@{version}", "registry-publication") from error - published = payload.get("version", {}) - if published.get("num") != version or published.get("yanked"): - raise RecoveryError(f"crates.io release {component.package}@{version} is not active") - checksum = published.get("checksum") - if not isinstance(checksum, str) or not SHA256_PATTERN.fullmatch(checksum): - raise RecoveryError(f"crates.io release {component.package}@{version} has an invalid checksum") - with tempfile.TemporaryDirectory(prefix="release-recovery-crate-") as temporary: - archive_path = Path(temporary) / f"{component.package}-{version}.crate" - client.download( - f"https://crates.io/api/v1/crates/{package}/{encoded_version}/download", - archive_path, - expected_sha256=checksum, - ) - with tarfile.open(archive_path, "r:gz") as archive: - members = [member for member in archive.getmembers() if member.name.endswith("/.cargo_vcs_info.json")] - if len(members) != 1 or (extracted := archive.extractfile(members[0])) is None: - raise RecoveryError("published crate has no unique source identity") - vcs = json.load(extracted) - if vcs.get("git", {}).get("sha1") != commit or vcs.get("git", {}).get("dirty", False): - raise RecoveryError(f"crates.io archive for {component.package}@{version} names a different source commit") - return {"kind": "crates.io", "registry": url, "checksum": checksum, "source_commit": commit} - - -def parse_checksums(raw: bytes) -> dict[str, str]: - checksums: dict[str, str] = {} - try: - lines = raw.decode("utf-8").splitlines() - except UnicodeDecodeError as error: - raise RecoveryError("CLI SHA256SUMS is not valid UTF-8", "registry-publication") from error - for line in lines: - match = re.fullmatch(r"([0-9a-fA-F]{64})\s+[*]?([^/\s]+)", line.strip()) - if match: - checksums[match.group(2)] = match.group(1).lower() - return checksums - - -def verify_cli(client: PublicClient, component: Component, version: str, commit: str) -> dict[str, Any]: - encoded = urllib.parse.quote(version, safe="") - try: - release = client.json(f"https://api.github.com/repos/{component.repository}/releases/tags/{encoded}") - except NotFound as error: - raise NotFound(f"CLI GitHub Release {version} is absent", "registry-publication") from error - assets = {asset.get("name"): asset for asset in release.get("assets", [])} - missing = CLI_ASSETS - set(assets) - if release.get("draft") or release.get("tag_name") != version or missing: - raise RecoveryError(f"CLI GitHub Release {version} is incomplete; missing assets: {sorted(missing)}") - - checksum_asset = assets["SHA256SUMS"] - checksum_raw = client.bytes(checksum_asset["browser_download_url"]) - checksums = parse_checksums(checksum_raw) - downloadable = sorted(CLI_ASSETS - {"SHA256SUMS"}) - missing_checksums = set(downloadable) - set(checksums) - if missing_checksums: - raise RecoveryError( - f"CLI SHA256SUMS does not cover every public release asset; missing: {sorted(missing_checksums)}", - "registry-publication", - ) - - verified_assets: list[dict[str, Any]] = [] - signer_workflow = f"{component.repository}/.github/workflows/release.yml" - attestation_modes = [ - ( - "exact-tag", - ["--source-ref", f"refs/tags/{version}", "--source-digest", commit], - {"mode": "exact-tag", "ref": f"refs/tags/{version}", "commit": commit}, - ), - ( - "qualified-main-workflow", - ["--source-ref", "refs/heads/main", "--signer-workflow", signer_workflow], - {"mode": "qualified-main-workflow", "ref": "refs/heads/main", "workflow": signer_workflow}, - ), - ] - selected_attestation_mode: tuple[str, list[str], dict[str, str]] | None = None - with tempfile.TemporaryDirectory(prefix="release-recovery-cli-") as temporary: - directory = Path(temporary) - downloaded_paths: list[Path] = [] - for name in downloadable: - asset = assets[name] - asset_path = directory / name - result = client.download( - asset["browser_download_url"], - asset_path, - expected_sha256=checksums[name], - ) - result.update({"name": name, "asset_id": asset.get("id")}) - verified_assets.append(result) - downloaded_paths.append(asset_path) - - checksum_path = directory / "SHA256SUMS" - checksum_path.write_bytes(checksum_raw) - verified_assets.append( - { - "name": "SHA256SUMS", - "asset_id": checksum_asset.get("id"), - "url": checksum_asset["browser_download_url"], - "size": len(checksum_raw), - "sha256": hashlib.sha256(checksum_raw).hexdigest(), - } - ) - downloaded_paths.append(checksum_path) - - if shutil.which("gh") is None: - raise RecoveryError( - "GitHub CLI is required to verify CLI release attestations", - "registry-publication", - ) - for asset_path in downloaded_paths: - base_arguments = ["gh", "attestation", "verify", str(asset_path), "--repo", component.repository] - candidates = attestation_modes if selected_attestation_mode is None else [selected_attestation_mode] - failures: list[str] = [] - for mode in candidates: - process = subprocess.run([*base_arguments, *mode[1]], check=False, text=True, capture_output=True) - if process.returncode == 0: - selected_attestation_mode = mode - break - failures.append(f"{mode[0]}: {process.stderr.strip()}") - else: - raise RecoveryError( - f"CLI build attestation failed for {asset_path.name}: {'; '.join(failures)}", - "registry-publication", - ) - - assert selected_attestation_mode is not None - if shutil.which("php") is None: - raise RecoveryError("PHP is required to verify CLI release source metadata", "registry-publication") - phar_version = subprocess.run( - ["php", str(directory / "dw.phar"), "--version"], - check=False, - text=True, - capture_output=True, - env={"PATH": os.environ.get("PATH", os.defpath)}, - ) - expected_identity = f"{version} (commit {commit[:12]}," - if phar_version.returncode or expected_identity not in phar_version.stdout: - raise RecoveryError( - f"CLI PHAR for {version} does not embed planned source commit {commit}", "registry-publication" - ) - - return { - "kind": "github-release", - "id": release.get("id"), - "url": release.get("html_url"), - "build_attestations_verified": True, - "build_attestation_authority": selected_attestation_mode[2], - "package_source": {"commit": commit, "embedded_phar_identity": phar_version.stdout.strip()}, - "assets": verified_assets, - } - - -VERIFIERS = { - "composer": verify_composer, - "oci": verify_oci, - "pypi": verify_pypi, - "crates.io": verify_crate, - "github-release": verify_cli, -} - - -def verify_component(client: PublicClient, name: str, identity: dict[str, str]) -> dict[str, Any]: - component = COMPONENTS[name] - require_source_tag(client, name, identity) - distribution = VERIFIERS[component.distribution](client, component, identity["version"], identity["commit"]) - github_release = ( - distribution - if component.distribution == "github-release" - else verify_github_release(client, name, identity["version"]) - ) - return { - "version": identity["version"], - "commit": identity["commit"], - "distribution": distribution, - "github_release": github_release, - } - - -def verify_distribution(client: PublicClient, name: str, identity: dict[str, str]) -> dict[str, Any]: - component = COMPONENTS[name] - require_source_tag(client, name, identity) - distribution = VERIFIERS[component.distribution](client, component, identity["version"], identity["commit"]) - return {"version": identity["version"], "commit": identity["commit"], "distribution": distribution} - - -def write_output(path: Path | None, values: dict[str, str]) -> None: - if path is None: - return - with path.open("a", encoding="utf-8") as output: - for key, value in values.items(): - output.write(f"{key}={value}\n") - - -def base_state(component: str, tag: str | None = None, plan: dict[str, Any] | None = None) -> dict[str, Any]: - return { - "schema": STATE_SCHEMA, - "component": component, - "release_plan_tag": tag, - "plan": plan.get("plan") if plan else None, - "channel": plan.get("channel") if plan else None, - "observed_at": dt.datetime.now(dt.UTC).replace(microsecond=0).isoformat().replace("+00:00", "Z"), - } - - -def scheduled_continuity_pause(client: PublicClient, plan: dict[str, Any]) -> dict[str, str] | None: - accepted_tag = f"{CONTINUITY_TAG_PREFIX}{plan['plan']}/accepted" - accepted_commit = resolve_tag(client, CONTROL_REPOSITORY, accepted_tag) - if accepted_commit is None: - return None - accepted_plan = read_record(client, accepted_tag, accepted_commit, "release-plan.json") - validate_plan(accepted_plan) - if canonical_json(accepted_plan) != canonical_json(plan): - raise RecoveryError("continuity acceptance record names a different release plan", "continuity-gate") - resumed_tag = f"{CONTINUITY_TAG_PREFIX}{plan['plan']}/resumed" - resumed_commit = resolve_tag(client, CONTROL_REPOSITORY, resumed_tag) - if resumed_commit is not None: - resumed_plan = read_record(client, resumed_tag, resumed_commit, "release-plan.json") - validate_plan(resumed_plan) - if canonical_json(resumed_plan) != canonical_json(plan): - raise RecoveryError("continuity resume record names a different release plan", "continuity-gate") - return None - return {"accepted_tag": accepted_tag, "accepted_commit": accepted_commit, "resumed_tag": resumed_tag} - - -def resolution_failure_state( - component_name: str, - tag: str | None, - record_commit: str | None, - plan: dict[str, Any] | None, - error: RecoveryError, -) -> dict[str, Any]: - failure = base_state(component_name, tag, plan) - if record_commit is not None: - failure["plan_record_commit"] = record_commit - durable_evidence: dict[str, Any] = { - "release_plan": tag, - "source_tag": f"https://github.com/{COMPONENTS[component_name].repository}/releases", - "actions": f"https://github.com/{COMPONENTS[component_name].repository}/actions", - } - if error.evidence is not None: - durable_evidence["failure"] = error.evidence - failure.update( - { - "phase": error.phase, - "outcome": "failed", - "reason": str(error), - "durable_evidence": durable_evidence, - "resume_action": error.resume_action - or ( - f"Run {COMPONENTS[component_name].repository} Actions workflow " - f"Release plan recovery{f' for {tag}' if tag else ''}" - ), - } - ) - return failure - - -def resolve_component( - client: PublicClient, - component_name: str, - tag: str, - record_commit: str, - plan: dict[str, Any], - preparation: dict[str, Any] | None, - plan_authority: dict[str, Any] | None = None, -) -> tuple[dict[str, Any], dict[str, str]]: - if component_name not in COMPONENTS: - raise RecoveryError(f"unknown release component: {component_name}") - branches, recovery_workflows = verify_plan_authority(client, plan) - component = COMPONENTS[component_name] - identity = plan["components"][component_name] - prepared_identity = None - if preparation is not None: - validate_release_preparation(preparation, plan) - prepared_identity = preparation["components"][component_name] - upstream: dict[str, Any] = {} - for dependency in component.dependencies: - try: - upstream[dependency] = verify_component(client, dependency, plan["components"][dependency]) - except NotFound as error: - raise RecoveryError( - f"{component_name} is waiting for upstream {dependency}: {error}", "upstream-publication" - ) from error - - existing_tag = resolve_tag(client, component.repository, identity["version"]) - if existing_tag is not None and existing_tag != identity["commit"]: - raise RecoveryError( - f"existing version tag {component.repository}@{identity['version']} " - f"points to {existing_tag}, not {identity['commit']}", - "tag-preflight", - ) - source_manifest = ( - require_python_source_manifest_version(client, identity, existing_tag) - if component_name == "sdk-python" - else None - ) - completed: dict[str, Any] | None = None - if existing_tag is not None: - with contextlib.suppress(NotFound): - completed = verify_component(client, component_name, identity) - if completed is not None: - action = "skip" - else: - if preparation is None: - raise RecoveryError( - f"release plan {tag} lacks release preparation required before publishing {component_name}", - "plan-discovery", - ) - if existing_tag is None: - with contextlib.suppress(NotFound): - VERIFIERS[component.distribution]( - client, - component, - identity["version"], - identity["commit"], - ) - action = "publish" - if plan_authority is not None and plan_authority.get("selection") == "explicit": - revalidate_explicit_plan_authority(client, plan_authority, action) - elif plan_authority is not None: - revalidate_implicit_plan_authority(client, plan_authority) - require_completed_plan_verification(plan_authority, action) - if scheduled_continuity_pause(client, plan) is not None: - raise RecoveryError( - "continuity pause authority changed during component preflight; " - "refusing a stale recovery action", - "continuity-gate", - ) - state = base_state(component_name, tag, plan) - state.update( - { - "phase": "complete" if action == "skip" else "publication", - "outcome": "verified" if action == "skip" else "ready", - "plan_record_commit": record_commit, - "default_branches": branches, - "recovery_workflows": recovery_workflows, - "upstream": upstream, - "source_tag": {"status": "present" if existing_tag else "absent", "commit": existing_tag}, - "source_manifest": source_manifest, - "declared_identity": identity, - "public_evidence": completed, - "resume_action": ( - "No action is required; repeated recovery verifies and skips this component" - if action == "skip" - else f"Run {component.repository} Actions workflow Release plan recovery for {tag}" - ), - } - ) - authority_evidence = next(iter(recovery_workflows.values()), {}).get("authority") - if authority_evidence is not None: - state["recovery_workflow_authority"] = authority_evidence - if prepared_identity is not None: - state["release_preparation"] = { - "record_commit": record_commit, - "record_sha256": manifest_digest(preparation), - "release_date": prepared_identity["release_notes"]["release_date"], - "release_notes_sha256": prepared_identity["release_notes"]["sha256"], - "source": prepared_identity["release_notes"]["source"], - } - outputs = { - "action": action, - "plan": str(plan["plan"]), - "channel": str(plan["channel"]), - "plan_tag": tag, - "plan_record_commit": record_commit, - "version": str(identity["version"]), - "commit": str(identity["commit"]), - "default_branch": component.default_branch, - "release_workflow": component.release_workflow or "", - "release_tag_input": component.release_tag_input or "", - } - if prepared_identity is not None: - outputs.update( - { - "release_date": str(prepared_identity["release_notes"]["release_date"]), - "release_notes_sha256": str(prepared_identity["release_notes"]["sha256"]), - } - ) - return state, outputs - - -def verify_with_retry( - client: PublicClient, - component_name: str, - plan: dict[str, Any], - attempts: int, - sleep_seconds: int, - registry_only: bool = False, -) -> dict[str, Any]: - last_error: RecoveryError | None = None - for attempt in range(1, attempts + 1): - try: - verifier = verify_distribution if registry_only else verify_component - return verifier(client, component_name, plan["components"][component_name]) - except NotFound as error: - last_error = error - if attempt < attempts: - print(f"waiting for public artifact ({attempt}/{attempts}): {error}", file=sys.stderr) - time.sleep(sleep_seconds) - assert last_error is not None - raise last_error - - -def main() -> int: - parser = argparse.ArgumentParser(description=__doc__) - subparsers = parser.add_subparsers(dest="command", required=True) - resolve = subparsers.add_parser("resolve") - resolve.add_argument("--component", required=True, choices=sorted(COMPONENTS)) - resolve.add_argument("--plan-tag") - resolve.add_argument("--plan-output", required=True, type=Path) - resolve.add_argument("--preparation-output", required=True, type=Path) - resolve.add_argument("--evidence", required=True, type=Path) - resolve.add_argument("--github-output", type=Path) - resolve.add_argument("--allow-empty", action="store_true") - - verify = subparsers.add_parser("verify") - verify.add_argument("--component", required=True, choices=sorted(COMPONENTS)) - verify.add_argument("--plan", required=True, type=Path) - verify.add_argument("--attempts", type=int, default=1) - verify.add_argument("--sleep", type=int, default=0) - verify.add_argument("--registry-only", action="store_true") - verify.add_argument("--evidence", required=True, type=Path) - - select_run = subparsers.add_parser("select-publication-run") - select_run.add_argument("--release-tag", required=True) - select_run.add_argument("--release-commit", required=True) - select_run.add_argument("--release-plan", required=True) - select_run.add_argument("--runs", required=True, type=Path) - - args = parser.parse_args() - token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN") - client = PublicClient(token) - try: - if args.command == "select-publication-run": - try: - runs = json.loads(args.runs.read_bytes()) - except (OSError, json.JSONDecodeError) as error: - raise RecoveryError(f"cannot read publication run metadata: {error}", "publication") from error - selection = select_publication_run(args.release_tag, args.release_commit, args.release_plan, runs) - print("\t".join(str(selection.get(field) or "") for field in ("action", "run_id", "status", "conclusion"))) - elif args.command == "resolve": - tag: str | None = args.plan_tag - record_commit: str | None = None - plan: dict[str, Any] | None = None - try: - tag, record_commit, plan, preparation, plan_authority = discover_plan( - client, - args.plan_tag, - args.component, - ) - args.plan_output.write_bytes(canonical_json(plan)) - if preparation is not None: - args.preparation_output.write_bytes(canonical_json(preparation)) - continuity_pause = scheduled_continuity_pause(client, plan) if args.plan_tag is None else None - if continuity_pause is not None: - assert plan_authority is not None - revalidate_implicit_plan_authority(client, plan_authority) - paused = base_state(args.component, tag, plan) - paused.update( - { - "phase": "continuity-gate", - "outcome": "paused", - "plan_record_commit": record_commit, - "continuity": continuity_pause, - "resume_action": ( - f"Wait for {continuity_pause['resumed_tag']} or explicitly recover exact plan {tag}" - ), - } - ) - args.evidence.write_bytes(canonical_json(paused)) - write_output( - args.github_output, - { - "action": "none", - "plan": str(plan["plan"]), - "channel": str(plan["channel"]), - "plan_tag": tag, - "plan_record_commit": record_commit, - }, - ) - return 0 - state, outputs = resolve_component( - client, - args.component, - tag, - record_commit, - plan, - preparation, - plan_authority, - ) - args.evidence.write_bytes(canonical_json(state)) - write_output(args.github_output, outputs) - except RecoveryError as error: - if ( - args.allow_empty - and args.plan_tag is None - and error.phase == "plan-discovery" - and str(error) == "no public release plan is available" - ): - no_op = base_state(args.component) - no_op.update( - { - "phase": "plan-discovery", - "outcome": "no-op", - "reason": str(error), - "resume_action": "No action is required; scheduled recovery found no eligible release plan", - } - ) - args.evidence.write_bytes(canonical_json(no_op)) - write_output(args.github_output, {"action": "none"}) - return 0 - failure = resolution_failure_state(args.component, tag, record_commit, plan, error) - args.evidence.write_bytes(canonical_json(failure)) - raise - else: - if args.attempts < 1 or args.sleep < 0: - raise RecoveryError("retry attempts must be positive and sleep must be non-negative") - try: - plan = json.loads(args.plan.read_bytes()) - except (OSError, json.JSONDecodeError) as error: - raise RecoveryError(f"cannot read canonical release plan: {error}") from error - validate_plan(plan) - try: - public = verify_with_retry( - client, - args.component, - plan, - args.attempts, - args.sleep, - registry_only=args.registry_only, - ) - state = base_state(args.component, f"{PLAN_TAG_PREFIX}{plan['plan']}", plan) - state.update( - { - "phase": "complete", - "outcome": "verified", - "public_evidence": public, - "resume_action": "No action is required", - } - ) - args.evidence.write_bytes(canonical_json(state)) - except RecoveryError as error: - state = base_state(args.component, f"{PLAN_TAG_PREFIX}{plan['plan']}", plan) - state.update( - { - "phase": error.phase, - "outcome": "failed", - "reason": str(error), - "resume_action": ( - f"Run {COMPONENTS[args.component].repository} Actions workflow Release plan recovery " - f"for {PLAN_TAG_PREFIX}{plan['plan']}" - ), - } - ) - args.evidence.write_bytes(canonical_json(state)) - raise - except PublicInfrastructureError as error: - if hasattr(args, "evidence") and hasattr(args, "component"): - transport = base_state(args.component) - transport.update( - { - "phase": "runner-transport", - "outcome": "runner-transport", - "transport": error.evidence, - "resume_action": "Retry recovery after trusted GitHub API transport is available", - } - ) - args.evidence.write_bytes(canonical_json(transport)) - if args.command == "resolve": - write_output(args.github_output, {"action": "none"}) - print(f"release recovery infrastructure failed: {error}", file=sys.stderr) - return INFRASTRUCTURE_EXIT_CODE - except RecoveryError as error: - print(f"release recovery error: {error}", file=sys.stderr) - return 1 - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/ci/recovery_workflow_authority.py b/scripts/ci/recovery_workflow_authority.py deleted file mode 100644 index d4f01cc..0000000 --- a/scripts/ci/recovery_workflow_authority.py +++ /dev/null @@ -1,223 +0,0 @@ -"""Resolve and validate the qualified component recovery-workflow authority.""" - -from __future__ import annotations - -import hashlib -import hmac -import json -from collections.abc import Mapping -from typing import Any - -SCHEMA = "durable-workflow.component-release-recovery-authority/v2" -CONTROL_REPOSITORY = "durable-workflow/.github" -AUTHORITY_REF = "main" -AUTHORITY_PATH = "release-recovery/authority.json" -QUALIFICATION_WORKFLOW = ".github/workflows/beta-candidate.yml" -QUALIFICATION_EVENT = "push" -QUALIFICATION_REF_PATH = f"{QUALIFICATION_WORKFLOW}@{AUTHORITY_REF}" -WORKFLOW_PATH = ".github/workflows/release-plan-recovery.yml" -SOURCE_IDENTITY = { - "repository": CONTROL_REPOSITORY, - "ref": f"refs/heads/{AUTHORITY_REF}", - "path": AUTHORITY_PATH, - "qualification": { - "workflow": QUALIFICATION_WORKFLOW, - "event": QUALIFICATION_EVENT, - }, -} - - -class RecoveryWorkflowAuthorityError(ValueError): - """The protected recovery-workflow authority is malformed or mismatched.""" - - -def normalized_source_sha256(source: str) -> str: - return hashlib.sha256(source.replace("\r\n", "\n").encode("utf-8")).hexdigest() - - -def authority_ref_url() -> str: - return f"https://api.github.com/repos/{CONTROL_REPOSITORY}/commits/{AUTHORITY_REF}" - - -def authority_url(commit: str) -> str: - return ( - f"https://api.github.com/repos/{CONTROL_REPOSITORY}/contents/{AUTHORITY_PATH}" - f"?ref={commit}" - ) - - -def qualification_runs_url(commit: str) -> str: - workflow = QUALIFICATION_WORKFLOW.rsplit("/", 1)[-1] - return ( - f"https://api.github.com/repos/{CONTROL_REPOSITORY}/actions/workflows/{workflow}/runs" - f"?branch={AUTHORITY_REF}&event={QUALIFICATION_EVENT}&head_sha={commit}&per_page=100" - ) - - -def validate_authority_commit(value: Any) -> str: - commit = value.get("sha") if isinstance(value, dict) else None - if ( - not isinstance(commit, str) - or len(commit) != 40 - or any(character not in "0123456789abcdef" for character in commit) - ): - raise RecoveryWorkflowAuthorityError("recovery workflow authority ref has an invalid commit") - return commit - - -def _qualification_evidence(run: dict[str, Any], commit: str) -> dict[str, Any]: - run_id = run.get("id") - run_attempt = run.get("run_attempt") - if ( - not isinstance(run_id, int) - or isinstance(run_id, bool) - or run_id < 1 - or not isinstance(run_attempt, int) - or isinstance(run_attempt, bool) - or run_attempt < 1 - ): - raise RecoveryWorkflowAuthorityError( - "recovery workflow authority qualification has an invalid run identity" - ) - return { - "workflow": QUALIFICATION_WORKFLOW, - "path": run["path"], - "event": QUALIFICATION_EVENT, - "head_branch": AUTHORITY_REF, - "head_sha": commit, - "run_id": run_id, - "run_attempt": run_attempt, - "status": "completed", - "conclusion": "success", - "url": f"https://github.com/{CONTROL_REPOSITORY}/actions/runs/{run_id}", - } - - -def validate_authority_qualification(value: Any, commit: str) -> dict[str, Any]: - runs = value.get("workflow_runs") if isinstance(value, dict) else None - if not isinstance(runs, list): - raise RecoveryWorkflowAuthorityError( - "recovery workflow authority qualification response has an invalid shape" - ) - - candidates = [ - run - for run in runs - if isinstance(run, dict) - and run.get("path") in (QUALIFICATION_WORKFLOW, QUALIFICATION_REF_PATH) - and run.get("event") == QUALIFICATION_EVENT - and run.get("head_branch") == AUTHORITY_REF - ] - if not candidates: - raise RecoveryWorkflowAuthorityError( - "recovery workflow authority qualification is absent for the resolved commit" - ) - if any(run.get("head_sha") != commit for run in candidates): - raise RecoveryWorkflowAuthorityError( - "recovery workflow authority qualification is bound to another commit" - ) - - successful = [ - run - for run in candidates - if run.get("status") == "completed" and run.get("conclusion") == "success" - ] - if successful: - return _qualification_evidence(successful[0], commit) - if any(run.get("status") != "completed" for run in candidates): - raise RecoveryWorkflowAuthorityError( - "recovery workflow authority qualification is pending for the resolved commit" - ) - if any(run.get("conclusion") == "cancelled" for run in candidates): - raise RecoveryWorkflowAuthorityError( - "recovery workflow authority qualification was cancelled for the resolved commit" - ) - raise RecoveryWorkflowAuthorityError( - "recovery workflow authority qualification failed for the resolved commit" - ) - - -def qualified_source_identity( - raw: bytes, - commit: str, - qualification: dict[str, Any], -) -> dict[str, Any]: - return { - "repository": CONTROL_REPOSITORY, - "ref": f"refs/heads/{AUTHORITY_REF}", - "commit": commit, - "path": AUTHORITY_PATH, - "sha256": hashlib.sha256(raw).hexdigest(), - "qualification": qualification, - } - - -def validate_authority( - value: Any, - components: Mapping[str, tuple[str, str]], -) -> dict[str, dict[str, str]]: - if not isinstance(value, dict) or set(value) != {"schema", "source", "workflows"}: - raise RecoveryWorkflowAuthorityError("recovery workflow authority has an invalid document shape") - if value.get("schema") != SCHEMA or value.get("source") != SOURCE_IDENTITY: - raise RecoveryWorkflowAuthorityError("recovery workflow authority has an unexpected protected source") - - workflows = value.get("workflows") - if not isinstance(workflows, dict) or set(workflows) != set(components): - raise RecoveryWorkflowAuthorityError("recovery workflow authority does not name the complete component set") - - validated: dict[str, dict[str, str]] = {} - for name, (repository, default_branch) in components.items(): - entry = workflows.get(name) - expected_identity = { - "repository": repository, - "ref": f"refs/heads/{default_branch}", - "path": WORKFLOW_PATH, - "state": "active", - } - if not isinstance(entry, dict) or set(entry) != {*expected_identity, "sha256"}: - raise RecoveryWorkflowAuthorityError(f"{name} recovery workflow authority has an invalid shape") - if any(entry.get(field) != expected for field, expected in expected_identity.items()): - raise RecoveryWorkflowAuthorityError(f"{name} recovery workflow authority has a mismatched identity") - digest = entry.get("sha256") - if ( - not isinstance(digest, str) - or len(digest) != 64 - or any(character not in "0123456789abcdef" for character in digest) - ): - raise RecoveryWorkflowAuthorityError(f"{name} recovery workflow authority has an invalid SHA-256") - validated[name] = dict(entry) - return validated - - -def decode_authority( - raw: bytes, - components: Mapping[str, tuple[str, str]], -) -> dict[str, dict[str, str]]: - try: - value = json.loads(raw) - except (UnicodeDecodeError, json.JSONDecodeError) as error: - raise RecoveryWorkflowAuthorityError("recovery workflow authority is not valid UTF-8 JSON") from error - return validate_authority(value, components) - - -def load_qualified_authority( - client: Any, - components: Mapping[str, tuple[str, str]], -) -> tuple[dict[str, dict[str, str]], dict[str, Any]]: - commit = validate_authority_commit(client.json(authority_ref_url())) - qualification = validate_authority_qualification( - client.json(qualification_runs_url(commit)), - commit, - ) - raw = client.bytes(authority_url(commit), accept="application/vnd.github.raw+json") - workflows = decode_authority(raw, components) - return workflows, qualified_source_identity(raw, commit, qualification) - - -def verify_workflow_source(name: str, source: str, expected_sha256: str) -> str: - actual_sha256 = normalized_source_sha256(source) - if not hmac.compare_digest(actual_sha256, expected_sha256): - raise RecoveryWorkflowAuthorityError( - f"{name} recovery workflow does not match the protected source identity" - ) - return actual_sha256 diff --git a/scripts/ci/release-recovery-consumer-adapter.json b/scripts/ci/release-recovery-consumer-adapter.json deleted file mode 100644 index a0e798d..0000000 --- a/scripts/ci/release-recovery-consumer-adapter.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "component": "sdk-python", - "consumer": "scripts/ci/component-release-recovery.py", - "contract": { - "path": "scripts/ci/release-recovery-consumer-contract.json", - "sha256": "72bf2ef467a6153ff347c3210cb91aa99bf340fa1a5556715f3a7808468aeedf", - "version": "1.8.2" - }, - "distribution_verification": { - "command": [ - "{python}", - "scripts/ci/test-component-release-recovery.py" - ] - }, - "repository": "durable-workflow/sdk-python", - "schema": "durable-workflow.release-recovery-consumer-adapter/v1", - "suite": { - "path": "scripts/ci/release_recovery_consumer_conformance.py", - "sha256": "87ec235eb15d1abb63abf0199607f66d0a49faa6903217858348bc0be67f309c" - }, - "target_branch": "main" -} diff --git a/scripts/ci/release-recovery-consumer-contract.json b/scripts/ci/release-recovery-consumer-contract.json deleted file mode 100644 index 02879cf..0000000 --- a/scripts/ci/release-recovery-consumer-contract.json +++ /dev/null @@ -1,102 +0,0 @@ -{ - "cases": [ - { - "id": "immutable-plan-enumeration", - "requirement": "Enumerate the complete immutable Git tag registry and reject a missing, duplicate, or malformed plan authority." - }, - { - "id": "current-plan-schema", - "requirement": "Accept the current release-plan v2 authority, retain only the exact digest-allowlisted historical v1 plans, and reject unrecorded v1 or unsupported schema values." - }, - { - "id": "completed-plan-lifecycle", - "requirement": "Select the unique completed current plan for verification-only recovery while refusing any attempt to republish it whether selected explicitly or implicitly." - }, - { - "id": "superseded-plan-lifecycle", - "requirement": "Resolve a superseded plan only through its digest-bound successor and select the surviving product train." - }, - { - "id": "exact-successor-identity", - "requirement": "Reject a successor edge whose tag, plan document, or canonical plan digest is not an exact identity match." - }, - { - "id": "malformed-authority-rejection", - "requirement": "Reject malformed and type-coercible authority values before plan selection, including non-exact SemVer." - }, - { - "id": "continuity-ambiguity-rejection", - "requirement": "Accept one exactly qualified immutable digest-bound resolution selecting a member of the exact continuity claim set independent of enumeration order only when discovery, record, and qualification lookups use their declared authority identities, and reject every unresolved or invalid fork." - }, - { - "id": "explicit-terminal-plan-rejection", - "requirement": "Allow an explicitly requested completed plan to verify as a no-op, and reject a superseded plan before publication or distribution verification." - }, - { - "id": "bounded-authority-convergence", - "requirement": "Re-enumerate lifecycle authority and fail closed when the registry does not converge within the declared attempt bound." - }, - { - "id": "release-candidate-beta-qualification", - "requirement": "Accept an exact release-candidate plan only when it retains one coherent immutable beta qualification, and reject non-beta qualification records." - }, - { - "id": "authoritative-rc-foundation", - "requirement": "Accept the aggregate release-candidate foundation authenticated by the selected immutable plan only when its tag, commit, exact seven-component tuple, and verification evidence agree; reject substitutions, malformed identities, and conflicting authority." - }, - { - "id": "scheduled-empty-no-op", - "requirement": "Record a successful neutral no-op when scheduled recovery has no eligible release plan, while retaining failures for malformed or unavailable authority." - }, - { - "id": "trusted-github-api-transport", - "requirement": "Use the GitHub Actions-supported gh API transport with its certificate-verifying runner trust for GitHub API calls; retry a transient certificate transport failure within the declared bound, classify persistent trust failure as runner transport, and never retry an ordinary API rejection." - }, - { - "id": "transport-fail-closed-publication", - "requirement": "Record structured runner-transport evidence with no publication action after transport retry exhaustion while preserving the explicitly authorized publication-ready path." - } - ], - "consumers": [ - { - "component": "workflow", - "repository": "durable-workflow/workflow", - "target_branch": "v2" - }, - { - "component": "waterline", - "repository": "durable-workflow/waterline", - "target_branch": "v2" - }, - { - "component": "server", - "repository": "durable-workflow/server", - "target_branch": "main" - }, - { - "component": "cli", - "repository": "durable-workflow/cli", - "target_branch": "main" - }, - { - "component": "sdk-php", - "repository": "durable-workflow/sdk-php", - "target_branch": "main" - }, - { - "component": "sdk-python", - "repository": "durable-workflow/sdk-python", - "target_branch": "main" - }, - { - "component": "sdk-rust", - "repository": "durable-workflow/sdk-rust", - "target_branch": "main" - } - ], - "schema": "durable-workflow.release-recovery-consumer-conformance/v1", - "suite": { - "sha256": "87ec235eb15d1abb63abf0199607f66d0a49faa6903217858348bc0be67f309c" - }, - "version": "1.8.2" -} diff --git a/scripts/ci/release_recovery_consumer_conformance.py b/scripts/ci/release_recovery_consumer_conformance.py deleted file mode 100644 index c88aa1b..0000000 --- a/scripts/ci/release_recovery_consumer_conformance.py +++ /dev/null @@ -1,1961 +0,0 @@ -#!/usr/bin/env python3 -"""Run the versioned release-recovery consumer conformance contract.""" - -from __future__ import annotations - -import argparse -import copy -import datetime as dt -import hashlib -import importlib.util -import json -import os -import re -import subprocess -import sys -import tempfile -import urllib.error -import urllib.parse -import urllib.request -from pathlib import Path, PurePosixPath -from types import ModuleType -from typing import Any -from unittest import mock - -CONTRACT_SCHEMA = "durable-workflow.release-recovery-consumer-conformance/v1" -ADAPTER_SCHEMA = "durable-workflow.release-recovery-consumer-adapter/v1" -EVIDENCE_SCHEMA = "durable-workflow.release-recovery-consumer-conformance-evidence/v1" -VERSION_PATTERN = re.compile( - r"(?P0|[1-9][0-9]*)\.(?P0|[1-9][0-9]*)\.(?P0|[1-9][0-9]*)" - r"(?:-(?P(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)" - r"(?:\.(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*))?" - r"(?:\+(?P[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?" -) -COMMIT_PATTERN = re.compile(r"[0-9a-f]{40}") -SHA256_PATTERN = re.compile(r"[0-9a-f]{64}") -CURRENT_PLAN_SCHEMA = "durable-workflow.release-plan/v2" -HISTORICAL_PLAN_SCHEMA = "durable-workflow.release-plan/v1" -EXPECTED_LEGACY_PLAN_DIGESTS = frozenset( - { - "0be354d5ea603170b6aef8ae0d9861886c4ccc0f75e6acb763239b30dd5d8ba3", - "295a3f654716ea8cd8dc693c1cd15a4b487737e5f01184bad7363fbde6717c40", - "486d9ef7c5a7f4443a89566cab33d7f2bccc518254ab6698d918a431d6a1c9ce", - "498804a2c7fd5b0e34f93ef080bea3073bc98e420e8bf84a98ca4cdb94729973", - "7bd737c92f139eec33026bc88a6491dc635d819a87a61c985e14e06aca645582", - "80e88698fa37b6d738d111dd2be3e3c145607973f8147c54cc25e5d91d415b17", - "9c0a5879652a2d5f4806a9167399687328c1764fa10dbc8d76215b43ac83b9d6", - "db90616c98f305c61d7eb2fb9ed03cc28f06963e9ca020c8ef6d7c6a8557f7bc", - "e1fc6e20c9d2ded0b5e7ac4d6be75ba861d31fc4b2db651dc0272dca623f2c7f", - } -) -REQUIRED_CASES = ( - "immutable-plan-enumeration", - "current-plan-schema", - "completed-plan-lifecycle", - "superseded-plan-lifecycle", - "exact-successor-identity", - "malformed-authority-rejection", - "continuity-ambiguity-rejection", - "explicit-terminal-plan-rejection", - "bounded-authority-convergence", - "release-candidate-beta-qualification", - "authoritative-rc-foundation", - "scheduled-empty-no-op", - "trusted-github-api-transport", - "transport-fail-closed-publication", -) -CONSUMERS = ( - { - "component": "workflow", - "repository": "durable-workflow/workflow", - "target_branch": "v2", - }, - { - "component": "waterline", - "repository": "durable-workflow/waterline", - "target_branch": "v2", - }, - { - "component": "server", - "repository": "durable-workflow/server", - "target_branch": "main", - }, - { - "component": "cli", - "repository": "durable-workflow/cli", - "target_branch": "main", - }, - { - "component": "sdk-php", - "repository": "durable-workflow/sdk-php", - "target_branch": "main", - }, - { - "component": "sdk-python", - "repository": "durable-workflow/sdk-python", - "target_branch": "main", - }, - { - "component": "sdk-rust", - "repository": "durable-workflow/sdk-rust", - "target_branch": "main", - }, -) - - -class ConformanceError(RuntimeError): - """The contract, adapter, or consumer does not conform.""" - - -def canonical_json(value: Any) -> bytes: - return (json.dumps(value, indent=2, sort_keys=True, ensure_ascii=True) + "\n").encode() - - -def sha256_bytes(value: bytes) -> str: - return hashlib.sha256(value).hexdigest() - - -def load_json_object(path: Path, label: str) -> tuple[dict[str, Any], bytes]: - try: - raw = path.read_bytes() - value = json.loads(raw) - except (OSError, json.JSONDecodeError) as error: - raise ConformanceError(f"{label} is not readable canonical JSON: {path}") from error - if not isinstance(value, dict): - raise ConformanceError(f"{label} must be a JSON object: {path}") - if raw != canonical_json(value): - raise ConformanceError(f"{label} must use canonical sorted JSON formatting: {path}") - return value, raw - - -def relative_file(root: Path, value: Any, label: str) -> Path: - if not isinstance(value, str) or not value: - raise ConformanceError(f"{label} must be a non-empty repository-relative path") - path = PurePosixPath(value) - if path.is_absolute() or ".." in path.parts: - raise ConformanceError(f"{label} must stay within the repository") - repository_root = root.resolve() - resolved = root.joinpath(*path.parts).resolve() - try: - resolved.relative_to(repository_root) - except ValueError as error: - raise ConformanceError(f"{label} must stay within the repository") from error - if not resolved.is_file(): - raise ConformanceError(f"{label} does not exist: {value}") - return resolved - - -def validate_contract( - contract: dict[str, Any], - contract_raw: bytes, - suite_path: Path, -) -> str: - expected_keys = {"cases", "consumers", "schema", "suite", "version"} - if set(contract) != expected_keys or contract.get("schema") != CONTRACT_SCHEMA: - raise ConformanceError("shared contract does not satisfy the v1 document shape") - version = contract.get("version") - if not isinstance(version, str) or VERSION_PATTERN.fullmatch(version) is None: - raise ConformanceError("shared contract version must be exact SemVer") - if contract.get("consumers") != list(CONSUMERS): - raise ConformanceError("shared contract must declare the exact seven-consumer target topology") - suite = contract.get("suite") - if ( - not isinstance(suite, dict) - or set(suite) != {"sha256"} - or not isinstance(suite.get("sha256"), str) - or SHA256_PATTERN.fullmatch(suite["sha256"]) is None - ): - raise ConformanceError("shared contract suite must contain one exact SHA-256") - actual_suite_sha256 = sha256_bytes(suite_path.read_bytes()) - if suite["sha256"] != actual_suite_sha256: - raise ConformanceError("shared conformance runner differs from the suite digest declared by the contract") - cases = contract.get("cases") - if not isinstance(cases, list): - raise ConformanceError("shared contract cases must be a JSON array") - case_ids: list[str] = [] - for case in cases: - if ( - not isinstance(case, dict) - or set(case) != {"id", "requirement"} - or not isinstance(case.get("id"), str) - or not isinstance(case.get("requirement"), str) - or not case["requirement"] - ): - raise ConformanceError("every shared contract case needs exactly an id and requirement") - case_ids.append(case["id"]) - if tuple(case_ids) != REQUIRED_CASES: - raise ConformanceError("shared contract omits or reorders required authority cases") - return sha256_bytes(contract_raw) - - -def parse_semver( - version: Any, - label: str, -) -> tuple[tuple[int, int, int], tuple[str, ...] | None]: - if not isinstance(version, str): - raise ConformanceError(f"{label} must be exact SemVer") - match = VERSION_PATTERN.fullmatch(version) - if match is None: - raise ConformanceError(f"{label} must be exact SemVer") - core = tuple(int(match.group(field)) for field in ("major", "minor", "patch")) - prerelease = match.group("prerelease") - return core, None if prerelease is None else tuple(prerelease.split(".")) - - -def compare_semver_precedence(left: str, right: str) -> int: - left_core, left_prerelease = parse_semver(left, "left version") - right_core, right_prerelease = parse_semver(right, "right version") - if left_core != right_core: - return 1 if left_core > right_core else -1 - if left_prerelease is None or right_prerelease is None: - if left_prerelease == right_prerelease: - return 0 - return 1 if left_prerelease is None else -1 - for left_identifier, right_identifier in zip(left_prerelease, right_prerelease, strict=False): - if left_identifier == right_identifier: - continue - left_numeric = left_identifier.isdigit() - right_numeric = right_identifier.isdigit() - if left_numeric and right_numeric: - return 1 if int(left_identifier) > int(right_identifier) else -1 - if left_numeric != right_numeric: - return -1 if left_numeric else 1 - return 1 if left_identifier > right_identifier else -1 - if len(left_prerelease) == len(right_prerelease): - return 0 - return 1 if len(left_prerelease) > len(right_prerelease) else -1 - - -def validate_adapter( - adapter: dict[str, Any], - contract: dict[str, Any], - contract_sha256: str, - repository_root: Path, - current_suite: Path, - current_contract: Path, -) -> tuple[Path, list[str]]: - expected_keys = { - "component", - "consumer", - "contract", - "distribution_verification", - "repository", - "schema", - "suite", - "target_branch", - } - if set(adapter) != expected_keys or adapter.get("schema") != ADAPTER_SCHEMA: - raise ConformanceError("consumer adapter does not satisfy the v1 document shape") - identity = { - "component": adapter.get("component"), - "repository": adapter.get("repository"), - "target_branch": adapter.get("target_branch"), - } - if identity not in CONSUMERS or identity not in contract["consumers"]: - raise ConformanceError("consumer adapter is not in the contract target topology") - contract_pin = adapter.get("contract") - if not isinstance(contract_pin, dict) or set(contract_pin) != {"path", "sha256", "version"}: - raise ConformanceError("consumer adapter does not declare the exact contract pin shape") - adapter_contract = relative_file(repository_root, contract_pin["path"], "adapter contract") - invoked_contract = current_contract.resolve() - if adapter_contract != invoked_contract: - raise ConformanceError("the invoked contract is not the adapter's declared contract") - declared_contract, declared_contract_raw = load_json_object(adapter_contract, "adapter contract") - if declared_contract.get("version") != contract_pin.get("version") or sha256_bytes( - declared_contract_raw - ) != contract_pin.get("sha256"): - raise ConformanceError("the adapter's declared contract does not match its version and digest pins") - if contract_pin.get("version") != contract["version"] or contract_pin.get("sha256") != contract_sha256: - raise ConformanceError("consumer adapter does not pin the exact invoked contract version and digest") - suite_pin = adapter.get("suite") - if ( - not isinstance(suite_pin, dict) - or set(suite_pin) != {"path", "sha256"} - or suite_pin.get("sha256") != contract["suite"]["sha256"] - ): - raise ConformanceError("consumer adapter does not pin the exact shared suite digest") - adapter_suite = relative_file(repository_root, suite_pin["path"], "adapter suite") - if adapter_suite.resolve() != current_suite.resolve(): - raise ConformanceError("the invoked suite is not the adapter's declared suite") - consumer = relative_file(repository_root, adapter.get("consumer"), "adapter consumer") - distribution = adapter.get("distribution_verification") - if ( - not isinstance(distribution, dict) - or set(distribution) != {"command"} - or not isinstance(distribution.get("command"), list) - or len(distribution["command"]) < 2 - or distribution["command"][0] != "{python}" - or not all(isinstance(item, str) and item for item in distribution["command"]) - ): - raise ConformanceError("distribution verification must declare a local Python command") - relative_file( - repository_root, - distribution["command"][1], - "distribution verification entry point", - ) - return consumer, distribution["command"] - - -def previous_contract( - repository_root: Path, - contract_path: Path, - previous_ref: str | None, -) -> dict[str, Any] | None: - if previous_ref is None: - return None - if COMMIT_PATTERN.fullmatch(previous_ref) is None or previous_ref == "0" * 40: - raise ConformanceError("previous contract ref must be an exact nonzero commit") - relative = contract_path.resolve().relative_to(repository_root.resolve()).as_posix() - commit = subprocess.run( - ["git", "cat-file", "-e", f"{previous_ref}^{{commit}}"], - cwd=repository_root, - check=False, - capture_output=True, - text=False, - ) - if commit.returncode != 0: - raise ConformanceError("previous contract commit is unavailable") - tree = subprocess.run( - ["git", "ls-tree", "--name-only", "-z", previous_ref, "--", relative], - cwd=repository_root, - check=False, - capture_output=True, - text=False, - ) - if tree.returncode != 0: - raise ConformanceError("previous contract commit cannot be inspected") - if tree.stdout == b"": - return None - if tree.stdout != f"{relative}\0".encode(): - raise ConformanceError("previous contract path could not be resolved exactly") - result = subprocess.run( - ["git", "show", f"{previous_ref}:{relative}"], - cwd=repository_root, - check=False, - capture_output=True, - text=False, - ) - if result.returncode != 0: - raise ConformanceError("previous shared contract is unreadable") - try: - value = json.loads(result.stdout) - except json.JSONDecodeError as error: - raise ConformanceError("previous shared contract is not valid JSON") from error - if not isinstance(value, dict): - raise ConformanceError("previous shared contract is not a JSON object") - return value - - -def require_versioned_contract_change( - previous: dict[str, Any] | None, - current: dict[str, Any], -) -> None: - if previous is None or previous == current: - return - previous_version = previous.get("version") - current_version = current.get("version") - parse_semver(previous_version, "previous shared contract version") - parse_semver(current_version, "current shared contract version") - if compare_semver_precedence(current_version, previous_version) <= 0: - raise ConformanceError("shared contract content changed without a strictly advancing SemVer version") - - -def load_consumer(path: Path) -> ModuleType: - parent = str(path.parent) - if parent not in sys.path: - sys.path.insert(0, parent) - module_name = f"release_recovery_consumer_{sha256_bytes(str(path).encode())[:12]}" - spec = importlib.util.spec_from_file_location(module_name, path) - if spec is None or spec.loader is None: - raise ConformanceError(f"cannot load recovery consumer: {path}") - module = importlib.util.module_from_spec(spec) - sys.modules[module_name] = module - try: - spec.loader.exec_module(module) - except Exception as error: - raise ConformanceError(f"cannot import recovery consumer: {path}") from error - return module - - -def plan(module: ModuleType, identity: str = "conformance") -> dict[str, Any]: - components: dict[str, dict[str, str]] = {} - for index, name in enumerate(module.COMPONENTS): - components[name] = { - "version": (f"2.0.0-beta.{index + 1}" if name in {"workflow", "waterline"} else f"1.{index}.0"), - "commit": f"{index + 1:040x}", - } - return { - "schema": module.SCHEMA, - "plan": identity, - "channel": "beta", - "foundation": { - "tag": module.FOUNDATION_TAG, - "commit": module.FOUNDATION_COMMIT, - }, - "components": components, - "beta_authorization": { - "tag": f"beta-authorization/{identity}", - "commit": "f" * 40, - }, - } - - -def legacy_beta_one_plan() -> dict[str, Any]: - return { - "schema": HISTORICAL_PLAN_SCHEMA, - "plan": "beta-1-e743e3760000", - "channel": "beta", - "foundation": { - "tag": "beta-candidate/beta-continuity-foundation", - "commit": "4995052410bd4301c5796ffba54e0b6d2f490ed1", - }, - "components": { - "workflow": { - "version": "2.0.0-beta.1", - "commit": "22bbf2a1469f4a38b1a6e1006ca8e46835c2fea4", - }, - "waterline": { - "version": "2.0.0-beta.1", - "commit": "0fb3caaba1e8a77f9bfa63ba3dcb2bcbaa825c31", - }, - "server": { - "version": "0.2.699", - "commit": "d6e8fb6c76c1d71cc7d3a1d38bdebd324150acad", - }, - "cli": { - "version": "0.1.95", - "commit": "bc036e94604329612b65a2a9effe2e929f91f4e1", - }, - "sdk-php": { - "version": "0.1.16", - "commit": "3b79813b1bbcb811277cc30d8dcfc359ea53f65c", - }, - "sdk-python": { - "version": "0.4.106", - "commit": "13037ddcb1f55d72c24256591e346b991ad64273", - }, - "sdk-rust": { - "version": "0.1.22", - "commit": "6fa98425c8ec7690ef96f8296a21407aa8d03067", - }, - }, - "beta_authorization": { - "tag": "beta-authorization/beta-1-e743e3760000", - "commit": "bef98bfd61b604d48459c15e968e3ace8e5124b0", - }, - } - - -def authority( - module: ModuleType, - candidate: dict[str, Any], - lifecycle: str, - successor: dict[str, Any] | None = None, -) -> dict[str, Any]: - return { - "tag": f"{module.PLAN_TAG_PREFIX}{candidate['plan']}", - "commit": "a" * 40, - "recorded_at": dt.datetime(2026, 7, 25, tzinfo=dt.UTC), - "plan": candidate, - "preparation": None, - "lifecycle": lifecycle, - "successor": successor, - } - - -def expect_recovery_error(module: ModuleType, action: Any, message: str) -> None: - try: - action() - except module.RecoveryError: - return - raise ConformanceError(message) - - -def continuity_resolution_qualification(module: ModuleType) -> dict[str, Any]: - return { - "repository": module.CONTROL_REPOSITORY, - "workflow": module.CONTINUITY_RESOLUTION_QUALIFICATION_WORKFLOW, - "event": module.CONTINUITY_RESOLUTION_QUALIFICATION_EVENT, - "head_branch": module.CONTINUITY_RESOLUTION_QUALIFICATION_BRANCH, - "head_sha": "9" * 40, - "run_id": 987, - "run_attempt": 2, - "status": "completed", - "conclusion": "success", - } - - -def continuity_resolution_qualification_run( - module: ModuleType, - qualification: dict[str, Any], -) -> dict[str, Any]: - return { - "id": qualification["run_id"], - "run_attempt": qualification["run_attempt"], - "repository": {"full_name": module.CONTROL_REPOSITORY}, - "head_repository": {"full_name": module.CONTROL_REPOSITORY}, - "path": ( - f"{module.CONTINUITY_RESOLUTION_QUALIFICATION_WORKFLOW}" - f"@{module.CONTINUITY_RESOLUTION_QUALIFICATION_BRANCH}" - ), - "event": qualification["event"], - "head_branch": qualification["head_branch"], - "head_sha": qualification["head_sha"], - "status": qualification["status"], - "conclusion": qualification["conclusion"], - } - - -def continuity_resolution_fixture( - module: ModuleType, -) -> tuple[dict[str, Any], list[dict[str, Any]], dict[str, Any], dict[str, Any]]: - interrupted_plan = {"plan": "interrupted-conformance"} - interrupted = { - "tag": "release-plan/interrupted-conformance", - "commit": "a" * 40, - "plan": interrupted_plan, - } - interruption = { - "tag": "beta-continuity/interrupted-conformance/interrupted", - "commit": "b" * 40, - "evidence_sha256": "c" * 64, - } - successors: list[dict[str, Any]] = [] - for index, name in enumerate(("first-successor", "second-successor"), start=1): - successors.append( - { - "tag": f"release-plan/{name}", - "supersession": { - **interruption, - "continuity_claim": { - "plan": { - "tag": f"release-plan/{name}", - "commit": str(index) * 40, - "sha256": str(index + 2) * 64, - }, - "acceptance": { - "tag": f"beta-continuity/{name}/accepted", - "commit": str(index + 4) * 40, - "sha256": str(index + 6) * 64, - }, - }, - }, - } - ) - claims = sorted( - (successor["supersession"]["continuity_claim"] for successor in successors), - key=lambda claim: claim["plan"]["tag"], - ) - qualification = continuity_resolution_qualification(module) - resolution = { - "schema": module.CONTINUITY_RESOLUTION_SCHEMA, - "qualification": qualification, - "interruption": { - "plan": { - "tag": interrupted["tag"], - "commit": interrupted["commit"], - "sha256": module.manifest_digest(interrupted_plan), - }, - "evidence": { - "tag": interruption["tag"], - "commit": interruption["commit"], - "sha256": interruption["evidence_sha256"], - }, - }, - "successor_claims": claims, - "selected_successor": claims[1]["plan"], - } - return ( - interrupted, - successors, - resolution, - continuity_resolution_qualification_run(module, qualification), - ) - - -def continuity_resolution_tag( - module: ModuleType, - interrupted: dict[str, Any], - resolution: dict[str, Any], -) -> str: - return ( - f"{module.CONTINUITY_RESOLUTION_TAG_PREFIX}{interrupted['plan']['plan']}/" - f"{module.manifest_digest(resolution)}" - ) - - -def exercise_continuity_resolution( - module: ModuleType, - interrupted: dict[str, Any], - successors: list[dict[str, Any]], - resolution: Any, - resolution_tags: list[str], - resolution_commit: str | None, - qualification_run: Any, -) -> str: - client = mock.Mock() - resolution_tag = resolution_tags[0] if len(resolution_tags) == 1 else None - qualification = resolution.get("qualification") if isinstance(resolution, dict) else None - resolution_prefix = ( - f"{module.CONTINUITY_RESOLUTION_TAG_PREFIX}{interrupted['plan']['plan']}/" - ) - registry_url = ( - f"https://api.github.com/repos/{module.CONTROL_REPOSITORY}" - f"/git/matching-refs/tags/{resolution_prefix}" - ) - tag_url = ( - ( - f"https://api.github.com/repos/{module.CONTROL_REPOSITORY}/git/ref/tags/" - f"{urllib.parse.quote(resolution_tag, safe='')}" - ) - if resolution_tag is not None - else None - ) - record_url = ( - ( - f"https://api.github.com/repos/{module.CONTROL_REPOSITORY}/contents/" - f"continuity-successor-resolution.json?ref={resolution_commit}" - ) - if resolution_tag is not None and resolution_commit is not None - else None - ) - qualification_url = ( - ( - f"https://api.github.com/repos/{module.CONTROL_REPOSITORY}/actions/runs/" - f"{qualification['run_id']}/attempts/{qualification['run_attempt']}" - ) - if ( - isinstance(qualification, dict) - and isinstance(qualification.get("repository"), str) - and type(qualification.get("run_id")) is int - and type(qualification.get("run_attempt")) is int - ) - else None - ) - json_urls: list[str] = [] - bytes_urls: list[str] = [] - - def read_json(url: Any, **kwargs: Any) -> Any: - if not isinstance(url, str) or kwargs: - raise ConformanceError( - f"consumer used invalid JSON transport arguments for continuity authority: {url!r}" - ) - json_urls.append(url) - if url == registry_url: - return [{"ref": f"refs/tags/{tag}"} for tag in resolution_tags] - if tag_url is not None and url == tag_url: - if resolution_commit is None: - raise module.NotFound( - f"continuity resolution tag is absent: {resolution_tag}", - "plan-discovery", - ) - return {"object": {"sha": resolution_commit, "type": "commit"}} - if qualification_url is not None and url == qualification_url: - return qualification_run - raise ConformanceError( - f"consumer queried an undeclared continuity JSON authority: {url}" - ) - - def read_bytes(url: Any, **kwargs: Any) -> bytes: - if ( - not isinstance(url, str) - or url != record_url - or kwargs != {"accept": "application/vnd.github.raw+json"} - ): - raise ConformanceError( - f"consumer queried an undeclared continuity record authority: {url!r}" - ) - bytes_urls.append(url) - return canonical_json(resolution) - - client.json.side_effect = read_json - client.bytes.side_effect = read_bytes - selected = module.resolve_continuity_successor_fork(client, interrupted, successors) - required_json_urls = {registry_url, tag_url, qualification_url} - missing_json_urls = { - url for url in required_json_urls if url is not None and url not in json_urls - } - if missing_json_urls or record_url is None or record_url not in bytes_urls: - raise ConformanceError( - "consumer returned a continuity successor without reading every exact declared authority" - ) - return selected - - -def expect_continuity_transport_rejection( - module: ModuleType, - action: Any, - message: str, -) -> None: - try: - action() - except ConformanceError: - return - except module.RecoveryError as error: - raise ConformanceError( - f"{message}; the focused mutant did not reach the strict transport" - ) from error - raise ConformanceError(message) - - -def assert_continuity_transport_mutants_rejected( - module: ModuleType, - interrupted: dict[str, Any], - successors: list[dict[str, Any]], - resolution: dict[str, Any], - resolution_tag: str, - resolution_commit: str, - qualification_run: dict[str, Any], -) -> None: - def exercise() -> str: - return exercise_continuity_resolution( - module, - interrupted, - successors, - resolution, - [resolution_tag], - resolution_commit, - qualification_run, - ) - - def wrong_registry_route(client: Any, interrupted_plan: str) -> list[str]: - prefix = f"{module.CONTINUITY_RESOLUTION_TAG_PREFIX}{interrupted_plan}/" - client.json( - f"https://api.github.com/repos/{module.CONTROL_REPOSITORY}" - f"/git/matching-refs/heads/{prefix}" - ) - return [resolution_tag] - - with mock.patch.object( - module, - "list_continuity_resolution_tags", - side_effect=wrong_registry_route, - ): - expect_continuity_transport_rejection( - module, - exercise, - "shared conformance accepted a wrong continuity registry route", - ) - - resolve_tag = module.resolve_tag - tag_mutants = ( - ( - lambda client, _repository, tag: resolve_tag( - client, - "durable-workflow/unrelated", - tag, - ), - "shared conformance accepted a continuity tag lookup in the wrong repository", - ), - ( - lambda client, repository, _tag: resolve_tag( - client, - repository, - "main", - ), - "shared conformance accepted a mutable continuity tag ref", - ), - ) - for mutant, message in tag_mutants: - with mock.patch.object(module, "resolve_tag", side_effect=mutant): - expect_continuity_transport_rejection(module, exercise, message) - - def record_ref_mutant(ref: str) -> Any: - def read_record( - client: Any, - _tag: str, - _commit: str, - filename: str, - ) -> Any: - encoded_filename = urllib.parse.quote(filename, safe="/") - raw = client.bytes( - f"https://api.github.com/repos/{module.CONTROL_REPOSITORY}/contents/" - f"{encoded_filename}?ref={ref}", - accept="application/vnd.github.raw+json", - ) - return json.loads(raw) - - return read_record - - record_ref_mutants = ( - ( - "main", - "shared conformance accepted a mutable continuity record ref", - ), - ( - "e" * 40, - "shared conformance accepted an unrelated continuity record ref", - ), - ) - for ref, message in record_ref_mutants: - with mock.patch.object( - module, - "read_record", - side_effect=record_ref_mutant(ref), - ): - expect_continuity_transport_rejection(module, exercise, message) - - def qualification_route_mutant( - repository: str, - run_id: int, - ) -> Any: - def validate(qualification: dict[str, Any], client: Any) -> dict[str, Any]: - client.json( - f"https://api.github.com/repos/{repository}/actions/runs/{run_id}" - f"/attempts/{qualification['run_attempt']}" - ) - return qualification - - return validate - - qualification = resolution["qualification"] - qualification_route_mutants = ( - ( - "durable-workflow/unrelated", - qualification["run_id"], - "shared conformance accepted a qualification lookup in the wrong repository", - ), - ( - module.CONTROL_REPOSITORY, - qualification["run_id"] + 1, - "shared conformance accepted the wrong qualification run lookup", - ), - ) - for repository, run_id, message in qualification_route_mutants: - with mock.patch.object( - module, - "validate_continuity_resolution_qualification", - side_effect=qualification_route_mutant(repository, run_id), - ): - expect_continuity_transport_rejection(module, exercise, message) - - -def case_immutable_plan_enumeration(module: ModuleType) -> None: - tags = ["release-plan/conformance-a", "release-plan/conformance-b"] - client = mock.Mock() - client.json.return_value = [{"ref": f"refs/tags/{tag}"} for tag in tags] - if module.list_release_plan_tags(client) != tags: - raise ConformanceError("consumer did not enumerate the complete immutable tag registry") - client.json.return_value.append({"ref": f"refs/tags/{tags[0]}"}) - expect_recovery_error( - module, - lambda: module.list_release_plan_tags(client), - "consumer accepted duplicate immutable plan authority", - ) - client.json.return_value = [] - expect_recovery_error( - module, - lambda: module.list_release_plan_tags(client), - "consumer accepted a missing immutable plan registry", - ) - malformed_registry_entries = ( - None, - {}, - {"ref": 7}, - {"ref": f"refs/heads/{tags[0]}"}, - {"ref": "refs/tags/release-plan/"}, - {"ref": "refs/tags/release-plan/Invalid"}, - ) - for malformed in malformed_registry_entries: - client.json.return_value = [malformed] - expect_recovery_error( - module, - lambda: module.list_release_plan_tags(client), - f"consumer accepted malformed immutable plan authority: {malformed!r}", - ) - - -def case_current_plan_schema(module: ModuleType) -> None: - if getattr(module, "SCHEMA", None) != CURRENT_PLAN_SCHEMA: - raise ConformanceError("consumer does not accept the current release-plan schema") - if getattr(module, "LEGACY_SCHEMA", None) != HISTORICAL_PLAN_SCHEMA: - raise ConformanceError("consumer does not identify the historical release-plan schema") - if getattr(module, "LEGACY_PLAN_DIGESTS", None) != EXPECTED_LEGACY_PLAN_DIGESTS: - raise ConformanceError("consumer does not pin the exact historical release-plan authorities") - - current = plan(module, "current-schema-conformance") - module.validate_plan(current) - - historical = legacy_beta_one_plan() - if module.manifest_digest(historical) != "e1fc6e20c9d2ded0b5e7ac4d6be75ba861d31fc4b2db651dc0272dca623f2c7f": - raise ConformanceError("shared historical release-plan fixture has an unexpected digest") - module.validate_plan(historical) - - unrecorded = copy.deepcopy(historical) - unrecorded["plan"] = "beta-1-replacement" - expect_recovery_error( - module, - lambda: module.validate_plan(unrecorded), - "consumer accepted an unrecorded historical release plan", - ) - - unsupported = copy.deepcopy(current) - unsupported["schema"] = "durable-workflow.release-plan/v3" - expect_recovery_error( - module, - lambda: module.validate_plan(unsupported), - "consumer accepted an unsupported current release-plan schema", - ) - - -def case_completed_plan_lifecycle(module: ModuleType) -> None: - completed = authority(module, plan(module, "completed-conformance"), "completed") - with mock.patch.object(module, "classify_plan_authorities", return_value=[completed]): - selected, snapshot = module.classify_implicit_plan_authority(mock.Mock()) - if selected != completed or snapshot != [completed]: - raise ConformanceError("consumer did not select the completed current plan for verification") - - preparation = { - "components": { - "sdk-php": { - "release_notes": { - "release_date": "2026-07-25", - "sha256": "c" * 64, - "source": {}, - } - } - } - } - completed["preparation"] = preparation - implicit_authority = { - **completed, - "selection": "implicit", - "authority_snapshot": [completed], - } - component = module.COMPONENTS["sdk-php"] - with ( - mock.patch.object(module, "verify_plan_authority", return_value=({}, {})), - mock.patch.object(module, "validate_release_preparation"), - mock.patch.object(module, "resolve_tag", return_value=None), - mock.patch.object( - module, - "classify_implicit_plan_authority", - return_value=(completed, [completed]), - ), - mock.patch.object( - module, - "continuity_authority_snapshot", - return_value={ - "accepted": {"tag": None, "commit": None}, - "resumed": {"tag": None, "commit": None}, - }, - create=True, - ), - mock.patch.object( - module, - "scheduled_continuity_pause", - return_value=None, - create=True, - ), - mock.patch.dict( - module.VERIFIERS, - {component.distribution: mock.Mock(side_effect=module.NotFound("not published"))}, - ), - ): - expect_recovery_error( - module, - lambda: module.resolve_component( - mock.Mock(), - "sdk-php", - completed["tag"], - completed["commit"], - completed["plan"], - preparation, - implicit_authority, - ), - "consumer returned publication-ready for an implicitly selected completed plan", - ) - - -def supersession_pair(module: ModuleType) -> tuple[dict[str, Any], dict[str, Any]]: - predecessor_plan = plan(module, "superseded-conformance") - successor_plan = copy.deepcopy(predecessor_plan) - successor_plan["plan"] = "successor-conformance" - predecessor_plan["components"]["server"]["version"] = "3.0.0" - predecessor_plan["components"]["cli"]["version"] = "3.0.1" - successor_plan["components"]["server"]["version"] = "3.0.1" - successor_plan["components"]["cli"]["version"] = "3.0.0" - successor = authority(module, successor_plan, "actionable") - predecessor = authority( - module, - predecessor_plan, - "superseded", - { - "tag": successor["tag"], - "sha256": module.manifest_digest(successor_plan), - "plan": successor_plan, - }, - ) - predecessor["commit"] = "b" * 40 - predecessor["recorded_at"] = dt.datetime(2026, 7, 24, tzinfo=dt.UTC) - return predecessor, successor - - -def case_superseded_plan_lifecycle(module: ModuleType) -> None: - predecessor, successor = supersession_pair(module) - selected = module.current_product_train_authorities([predecessor, successor]) - if [item["tag"] for item in selected] != [successor["tag"]]: - raise ConformanceError("consumer did not resolve a superseded plan to its successor") - - -def case_exact_successor_identity(module: ModuleType) -> None: - predecessor, successor = supersession_pair(module) - predecessor["successor"] = {**predecessor["successor"], "sha256": "0" * 64} - expect_recovery_error( - module, - lambda: module.current_product_train_authorities([predecessor, successor]), - "consumer accepted an inexact successor digest", - ) - - predecessor, successor = supersession_pair(module) - predecessor["successor"] = { - **predecessor["successor"], - "tag": "release-plan/wrong-successor-conformance", - } - expect_recovery_error( - module, - lambda: module.current_product_train_authorities([predecessor, successor]), - "consumer accepted an inexact successor tag", - ) - - predecessor, successor = supersession_pair(module) - mismatched_plan = copy.deepcopy(successor["plan"]) - mismatched_plan["components"]["sdk-rust"]["commit"] = "c" * 40 - predecessor["successor"] = { - **predecessor["successor"], - "plan": mismatched_plan, - } - expect_recovery_error( - module, - lambda: module.current_product_train_authorities([predecessor, successor]), - "consumer accepted an inexact successor plan document", - ) - - -def case_malformed_authority_rejection(module: ModuleType) -> None: - for malformed in ("01.0.0", "1.0.0-alpha.01", "1.0.0-alpha..1", 100): - candidate = plan(module, "malformed-conformance") - candidate["components"]["server"]["version"] = malformed - expect_recovery_error( - module, - lambda candidate=candidate: module.validate_plan(candidate), - f"consumer accepted malformed authority value: {malformed!r}", - ) - - -def case_continuity_ambiguity_rejection(module: ModuleType) -> None: - interrupted, successors, resolution, qualification_run = continuity_resolution_fixture(module) - resolution_tag = continuity_resolution_tag(module, interrupted, resolution) - expected_selected = resolution["selected_successor"]["tag"] - - for ordering in (successors, list(reversed(successors))): - selected = exercise_continuity_resolution( - module, - interrupted, - ordering, - resolution, - [resolution_tag], - "f" * 40, - qualification_run, - ) - if selected != expected_selected: - raise ConformanceError( - "consumer did not select the exact digest-bound continuity successor independent of enumeration order" - ) - - assert_continuity_transport_mutants_rejected( - module, - interrupted, - successors, - resolution, - resolution_tag, - "f" * 40, - qualification_run, - ) - - absent_authorities = ( - ([], "f" * 40, "consumer accepted continuity successors without a resolution authority"), - ( - [resolution_tag], - None, - "consumer accepted a continuity resolution authority without an immutable record", - ), - ( - [ - resolution_tag, - ( - f"{module.CONTINUITY_RESOLUTION_TAG_PREFIX}{interrupted['plan']['plan']}/" - f"{'e' * 64}" - ), - ], - "f" * 40, - "consumer accepted multiple continuity resolution authorities", - ), - ) - for resolution_tags, resolution_commit, message in absent_authorities: - expect_recovery_error( - module, - lambda resolution_tags=resolution_tags, resolution_commit=resolution_commit: exercise_continuity_resolution( - module, - interrupted, - successors, - resolution, - resolution_tags, - resolution_commit, - qualification_run, - ), - message, - ) - - malformed_records: tuple[Any, ...] = ( - None, - {**resolution, "unexpected": True}, - ) - for malformed in malformed_records: - malformed_tag = ( - resolution_tag - if not isinstance(malformed, dict) - else continuity_resolution_tag(module, interrupted, malformed) - ) - expect_recovery_error( - module, - lambda malformed=malformed, malformed_tag=malformed_tag: exercise_continuity_resolution( - module, - interrupted, - successors, - malformed, - [malformed_tag], - "f" * 40, - qualification_run, - ), - "consumer accepted a malformed continuity resolution record", - ) - - interruption_mismatch = copy.deepcopy(resolution) - interruption_mismatch["interruption"]["plan"]["commit"] = "e" * 40 - claim_set_mismatch = copy.deepcopy(resolution) - claim_set_mismatch["successor_claims"][0]["acceptance"]["sha256"] = "e" * 64 - selected_outside_claim_set = copy.deepcopy(resolution) - selected_outside_claim_set["selected_successor"] = { - "tag": "release-plan/outside-claim-set", - "commit": "e" * 40, - "sha256": "e" * 64, - } - invalid_qualification = copy.deepcopy(resolution) - invalid_qualification["qualification"]["repository"] = "durable-workflow/untrusted" - semantic_mismatches = ( - (interruption_mismatch, "consumer accepted a continuity resolution for another interruption"), - (claim_set_mismatch, "consumer accepted a continuity resolution for another claim set"), - (selected_outside_claim_set, "consumer accepted a successor outside the exact claim set"), - (invalid_qualification, "consumer accepted an invalid qualification identity"), - ) - for mismatched, message in semantic_mismatches: - mismatched_tag = continuity_resolution_tag(module, interrupted, mismatched) - expect_recovery_error( - module, - lambda mismatched=mismatched, mismatched_tag=mismatched_tag: exercise_continuity_resolution( - module, - interrupted, - successors, - mismatched, - [mismatched_tag], - "f" * 40, - qualification_run, - ), - message, - ) - - digest_mismatch_tag = ( - f"{module.CONTINUITY_RESOLUTION_TAG_PREFIX}{interrupted['plan']['plan']}/" - f"{'0' * 64}" - ) - expect_recovery_error( - module, - lambda: exercise_continuity_resolution( - module, - interrupted, - successors, - resolution, - [digest_mismatch_tag], - "f" * 40, - qualification_run, - ), - "consumer accepted a continuity resolution with the wrong immutable digest", - ) - - mismatched_run = {**qualification_run, "head_sha": "8" * 40} - expect_recovery_error( - module, - lambda: exercise_continuity_resolution( - module, - interrupted, - successors, - resolution, - [resolution_tag], - "f" * 40, - mismatched_run, - ), - "consumer accepted qualification evidence for another source identity", - ) - - -def case_explicit_terminal_plan_rejection(module: ModuleType) -> None: - candidate = plan(module, "terminal-conformance") - completed = authority(module, candidate, "completed") - with mock.patch.object(module, "classify_plan_authorities", return_value=[completed]): - selected = module.select_explicit_plan_authority( - mock.Mock(), - completed["tag"], - completed["commit"], - candidate, - None, - ) - if selected != {**completed, "selection": "explicit"}: - raise ConformanceError("consumer did not select an explicitly requested completed plan") - - superseded = authority(module, candidate, "superseded") - with mock.patch.object(module, "classify_plan_authorities", return_value=[superseded]): - expect_recovery_error( - module, - lambda: module.select_explicit_plan_authority( - mock.Mock(), - superseded["tag"], - superseded["commit"], - candidate, - None, - ), - "consumer accepted an explicitly selected superseded plan", - ) - - -def case_bounded_authority_convergence(module: ModuleType) -> None: - candidate = authority(module, plan(module, "convergence-conformance"), "actionable") - with ( - mock.patch.object( - module, - "classify_implicit_plan_authority", - return_value=(candidate, [candidate]), - ) as classify, - mock.patch.object( - module, - "implicit_plan_authority_converged", - return_value=False, - ) as converged, - ): - expect_recovery_error( - module, - lambda: module.select_implicit_plan_authority(mock.Mock()), - "consumer did not fail closed after bounded authority churn", - ) - expected = module.IMPLICIT_AUTHORITY_MAX_ATTEMPTS - if classify.call_count != expected or converged.call_count != expected: - raise ConformanceError("consumer did not enforce the declared convergence attempt bound") - - -def case_release_candidate_beta_qualification(module: ModuleType) -> None: - candidate = plan(module, "release-candidate-conformance") - candidate["channel"] = "rc" - for identity in candidate["components"].values(): - identity["version"] = "2.0.0-rc.1" - module.validate_plan(candidate) - - beta_components = copy.deepcopy(candidate["components"]) - for identity in beta_components.values(): - identity["version"] = "2.0.0-beta.21" - record = { - "schema": "durable-workflow.beta-authorization/v1", - "channel": "beta", - "candidate": "coherent-beta-qualification", - "components": beta_components, - } - candidate["beta_authorization"]["tag"] = "beta-authorization/coherent-beta-qualification" - if not module.beta_authorization_matches_plan(candidate, candidate["beta_authorization"], record): - raise ConformanceError("consumer rejected coherent beta qualification for a release-candidate plan") - - record["components"]["server"]["version"] = "2.0.0-rc.1" - if module.beta_authorization_matches_plan(candidate, candidate["beta_authorization"], record): - raise ConformanceError("consumer accepted non-beta qualification for a release-candidate plan") - - -def aggregate_rc_plan(module: ModuleType) -> dict[str, Any]: - candidate = plan(module, "authoritative-rc-conformance") - candidate["channel"] = "rc" - for identity in candidate["components"].values(): - identity["version"] = "2.0.0-rc.5" - candidate["foundation"] = { - "tag": f"beta-candidate/rc-{candidate['plan']}", - "commit": "e" * 40, - } - candidate["beta_authorization"] = None - return candidate - - -def aggregate_rc_verification(module: ModuleType, candidate: dict[str, Any]) -> dict[str, Any]: - foundation = { - "schema": "durable-workflow.beta-candidate/v2", - "candidate": f"rc-{candidate['plan']}", - "components": candidate["components"], - } - return { - "schema": "durable-workflow.beta-candidate-verification/v2", - "candidate": foundation["candidate"], - "manifest_sha256": module.manifest_digest(foundation), - "outcome": "verified", - "components": { - name: { - "version": identity["version"], - "commit": identity["commit"], - "outcome": "verified", - } - for name, identity in candidate["components"].items() - }, - } - - -def case_authoritative_rc_foundation(module: ModuleType) -> None: - candidate = aggregate_rc_plan(module) - module.validate_plan(candidate) - - malformed_tag = copy.deepcopy(candidate) - malformed_tag["foundation"]["tag"] = "beta-candidate/rc-substitution" - expect_recovery_error( - module, - lambda: module.validate_plan(malformed_tag), - "consumer accepted an aggregate foundation for a different release plan", - ) - - malformed_commit = copy.deepcopy(candidate) - malformed_commit["foundation"]["commit"] = "not-a-commit" - expect_recovery_error( - module, - lambda: module.validate_plan(malformed_commit), - "consumer accepted a malformed aggregate foundation commit", - ) - - unapproved = copy.deepcopy(candidate) - unapproved["beta_authorization"] = { - "tag": f"beta-authorization/{candidate['plan']}", - "commit": "f" * 40, - } - expect_recovery_error( - module, - lambda: module.validate_plan(unapproved), - "consumer accepted conflicting authority for an aggregate foundation", - ) - - foundation = { - "schema": "durable-workflow.beta-candidate/v2", - "candidate": f"rc-{candidate['plan']}", - "components": candidate["components"], - } - verification = aggregate_rc_verification(module, candidate) - - class FoundationAccepted(RuntimeError): - pass - - with ( - mock.patch.object( - module, - "resolve_tag", - return_value=candidate["foundation"]["commit"], - ), - mock.patch.object( - module, - "read_record", - side_effect=(foundation, verification), - ), - mock.patch.object( - module, - "load_recovery_workflow_authority", - side_effect=FoundationAccepted, - ), - ): - try: - module.verify_plan_authority(mock.Mock(), candidate) - except FoundationAccepted: - pass - else: - raise ConformanceError( - "consumer did not continue after verifying the exact aggregate foundation" - ) - - with ( - mock.patch.object(module, "resolve_tag", return_value="d" * 40), - mock.patch.object(module, "read_record") as read_record, - ): - expect_recovery_error( - module, - lambda: module.verify_plan_authority(mock.Mock(), candidate), - "consumer accepted a moved aggregate foundation tag", - ) - read_record.assert_not_called() - - substituted_foundation = copy.deepcopy(foundation) - substituted_foundation["components"]["server"]["commit"] = "d" * 40 - with ( - mock.patch.object( - module, - "resolve_tag", - return_value=candidate["foundation"]["commit"], - ), - mock.patch.object( - module, - "read_record", - return_value=substituted_foundation, - ), - ): - expect_recovery_error( - module, - lambda: module.verify_plan_authority(mock.Mock(), candidate), - "consumer accepted a substituted aggregate component tuple", - ) - - malformed_verification = copy.deepcopy(verification) - malformed_verification["components"]["server"]["outcome"] = "failed" - with ( - mock.patch.object( - module, - "resolve_tag", - return_value=candidate["foundation"]["commit"], - ), - mock.patch.object( - module, - "read_record", - side_effect=(foundation, malformed_verification), - ), - ): - expect_recovery_error( - module, - lambda: module.verify_plan_authority(mock.Mock(), candidate), - "consumer accepted aggregate foundation evidence that was not verified", - ) - - -def case_scheduled_empty_no_op(module: ModuleType) -> None: - with tempfile.TemporaryDirectory() as directory: - root = Path(directory) - evidence = root / "release-recovery-evidence.json" - github_output = root / "github-output" - component = next(iter(module.COMPONENTS)) - arguments = [ - "component-release-recovery.py", - "resolve", - "--component", - component, - "--plan-output", - str(root / "release-plan.json"), - "--preparation-output", - str(root / "release-preparation.json"), - "--evidence", - str(evidence), - "--github-output", - str(github_output), - "--allow-empty", - ] - with ( - mock.patch.object(sys, "argv", arguments), - mock.patch.object( - module, - "discover_plan", - side_effect=module.RecoveryError( - "no public release plan is available", - "plan-discovery", - ), - ), - ): - result = module.main() - - state = json.loads(evidence.read_bytes()) - if ( - result != 0 - or state.get("phase") != "plan-discovery" - or state.get("outcome") != "no-op" - or github_output.read_text(encoding="utf-8") != "action=none\n" - ): - raise ConformanceError( - "scheduled recovery without eligible work did not record a neutral no-op" - ) - - failure_evidence = root / "release-recovery-failure-evidence.json" - failure_output = root / "failure-github-output" - failure_arguments = [ - *arguments[: arguments.index("--evidence")], - "--evidence", - str(failure_evidence), - "--github-output", - str(failure_output), - "--allow-empty", - ] - with ( - mock.patch.object(sys, "argv", failure_arguments), - mock.patch.object( - module, - "discover_plan", - side_effect=module.RecoveryError( - "release plan registry is malformed", - "plan-discovery", - ), - ), - ): - failure_result = module.main() - - failure_state = json.loads(failure_evidence.read_bytes()) - if ( - failure_result != 1 - or failure_state.get("outcome") != "failed" - or failure_output.exists() - ): - raise ConformanceError( - "scheduled empty handling weakened unrelated plan-discovery failures" - ) - - -def github_cli_result( - status: int = 200, - body: bytes = b"[]", - *, - stderr: bytes = b"", - **headers: str, -) -> subprocess.CompletedProcess[bytes]: - response_headers = b"".join(f"{name}: {value}\r\n".encode() for name, value in headers.items()) - output = f"HTTP/2.0 {status} response\r\n".encode() + response_headers + b"\r\n" + body - return subprocess.CompletedProcess( - ["gh", "api"], - 0 if 200 <= status <= 299 else 1, - output, - stderr, - ) - - -def case_trusted_github_api_transport(module: ModuleType) -> None: - url = "https://api.github.com/repos/durable-workflow/.github/releases" - sleeps: list[float] = [] - client = module.PublicClient( - token="conformance-token", - max_attempts=2, - retry_base_seconds=1, - sleep=sleeps.append, - ) - with ( - mock.patch.dict(module.os.environ, {"GITHUB_ACTIONS": "true"}), - mock.patch.object( - module.urllib.request, - "urlopen", - side_effect=AssertionError("runner transport bypassed the GitHub CLI mock"), - ) as open_url, - mock.patch.object( - module.subprocess, - "run", - side_effect=( - github_cli_result(0, stderr=b"x509: certificate signed by unknown authority"), - github_cli_result(), - ), - ) as run, - ): - result = client.json(url) - command = run.call_args.args[0] - if ( - result != [] - or sleeps != [1] - or open_url.called - or command[:7] != ["gh", "api", "--hostname", "github.com", "--include", "--method", "GET"] - or command[-1] != "repos/durable-workflow/.github/releases" - or "--insecure" in command - or run.call_args.kwargs.get("env", {}).get("GH_TOKEN") != "conformance-token" - or run.call_args.kwargs.get("env", {}).get("GH_PROMPT_DISABLED") != "1" - ): - raise ConformanceError( - "consumer did not retry transient certificate failure through the GitHub CLI trust transport" - ) - - rate_limit_sleeps: list[float] = [] - rate_limited = module.PublicClient( - token="conformance-token", - max_attempts=2, - retry_base_seconds=1, - sleep=rate_limit_sleeps.append, - now=lambda: 100, - ) - with ( - mock.patch.dict(module.os.environ, {"GITHUB_ACTIONS": "true"}), - mock.patch.object( - module.subprocess, - "run", - side_effect=( - github_cli_result( - 403, - b'{"message":"Forbidden"}', - **{ - "x-ratelimit-remaining": "0", - "X-rAtElImIt-ReSeT": "112", - }, - ), - github_cli_result(), - ), - ) as run, - ): - result = rate_limited.json(url) - if result != [] or rate_limit_sleeps != [12] or run.call_count != 2: - raise ConformanceError( - "consumer did not classify mixed-case GitHub CLI rate-limit headers or honor reset delay" - ) - - persistent = module.PublicClient( - token="conformance-token", - max_attempts=2, - retry_base_seconds=1, - sleep=lambda _delay: None, - ) - with ( - mock.patch.dict(module.os.environ, {"GITHUB_ACTIONS": "true"}), - mock.patch.object( - module.subprocess, - "run", - return_value=github_cli_result(0, stderr=b"x509: certificate signed by unknown authority"), - ) as run, - ): - try: - persistent.json(url) - except module.PublicInfrastructureError as error: - expected = { - "classification": "github-read-transient", - "endpoint_class": "releases-api", - "attempts": 2, - "reason": "retry-exhausted", - "failure": "transport=tls-certificate-verification", - } - if getattr(error, "evidence", None) != expected: - raise ConformanceError( - "persistent certificate failure did not retain structured transport evidence" - ) from error - else: - raise ConformanceError("persistent certificate failure did not fail closed") - if run.call_count != 2: - raise ConformanceError("persistent certificate failure escaped the retry bound") - - api_error = github_cli_result( - 422, - b'{"message":"invalid release authority"}', - ) - - def fail_on_sleep(_delay: float) -> None: - raise ConformanceError("deterministic API failure was retried") - - deterministic = module.PublicClient(token="conformance-token", max_attempts=3, sleep=fail_on_sleep) - with ( - mock.patch.dict(module.os.environ, {"GITHUB_ACTIONS": "true"}), - mock.patch.object( - module.subprocess, - "run", - return_value=api_error, - ) as run, - ): - expect_recovery_error( - module, - lambda: deterministic.json(url), - "ordinary GitHub API failure was accepted", - ) - if run.call_count != 1: - raise ConformanceError("ordinary GitHub API failure was retried") - - -def case_transport_fail_closed_publication(module: ModuleType) -> None: - with tempfile.TemporaryDirectory() as directory: - root = Path(directory) - evidence = root / "release-recovery-evidence.json" - github_output = root / "github-output" - arguments = [ - "component-release-recovery.py", - "resolve", - "--component", - "workflow", - "--plan-output", - str(root / "release-plan.json"), - "--preparation-output", - str(root / "release-preparation.json"), - "--evidence", - str(evidence), - "--github-output", - str(github_output), - "--allow-empty", - ] - unavailable = module.PublicInfrastructureError( - "releases-api", - 5, - reason="retry-exhausted", - failure="transport=tls-certificate-verification", - ) - with ( - mock.patch.object(sys, "argv", arguments), - mock.patch.object(module, "discover_plan", side_effect=unavailable), - ): - result = module.main() - state = json.loads(evidence.read_bytes()) - if ( - result != module.INFRASTRUCTURE_EXIT_CODE - or state.get("phase") != "runner-transport" - or state.get("outcome") != "runner-transport" - or state.get("transport") != unavailable.evidence - or github_output.read_text(encoding="utf-8") != "action=none\n" - ): - raise ConformanceError( - "persistent transport failure did not suppress publication with runner evidence" - ) - - candidate = plan(module, "authorized-publication-conformance") - preparation = { - "components": { - "workflow": { - "release_notes": { - "release_date": "2026-08-12", - "sha256": "a" * 64, - "source": {}, - } - } - } - } - component = module.COMPONENTS["workflow"] - verifier = mock.Mock(side_effect=module.NotFound("not published")) - authority = mock.Mock(return_value=({}, {})) - with ( - mock.patch.object(module, "verify_plan_authority", authority), - mock.patch.object(module, "validate_release_preparation"), - mock.patch.object( - module, - "source_product_train_evidence", - return_value={}, - create=True, - ), - mock.patch.object(module, "resolve_tag", return_value=None), - mock.patch.dict(module.VERIFIERS, {component.distribution: verifier}), - ): - state, outputs = module.resolve_component( - mock.Mock(), - "workflow", - f"release-plan/{candidate['plan']}", - "b" * 40, - candidate, - preparation, - ) - if ( - outputs.get("action") != "publish" - or state.get("outcome") != "ready" - or authority.call_count != 1 - or verifier.call_count != 1 - ): - raise ConformanceError( - "authorized publication path did not remain available after transport hardening" - ) - - -CASE_RUNNERS = { - "immutable-plan-enumeration": case_immutable_plan_enumeration, - "current-plan-schema": case_current_plan_schema, - "completed-plan-lifecycle": case_completed_plan_lifecycle, - "superseded-plan-lifecycle": case_superseded_plan_lifecycle, - "exact-successor-identity": case_exact_successor_identity, - "malformed-authority-rejection": case_malformed_authority_rejection, - "continuity-ambiguity-rejection": case_continuity_ambiguity_rejection, - "explicit-terminal-plan-rejection": case_explicit_terminal_plan_rejection, - "bounded-authority-convergence": case_bounded_authority_convergence, - "release-candidate-beta-qualification": case_release_candidate_beta_qualification, - "authoritative-rc-foundation": case_authoritative_rc_foundation, - "scheduled-empty-no-op": case_scheduled_empty_no_op, - "trusted-github-api-transport": case_trusted_github_api_transport, - "transport-fail-closed-publication": case_transport_fail_closed_publication, -} - - -def run_cases(module: ModuleType) -> tuple[list[dict[str, str]], list[str]]: - results: list[dict[str, str]] = [] - failures: list[str] = [] - for case_id in REQUIRED_CASES: - try: - CASE_RUNNERS[case_id](module) - except Exception as error: - results.append({"id": case_id, "status": "fail"}) - failures.append(f"{case_id}: {error}") - else: - results.append({"id": case_id, "status": "pass"}) - return results, failures - - -def run_distribution(command: list[str], repository_root: Path) -> tuple[dict[str, Any], str | None]: - resolved = [sys.executable if item == "{python}" else item for item in command] - result = subprocess.run(resolved, cwd=repository_root, check=False) - evidence = { - "command": resolved, - "status": "pass" if result.returncode == 0 else "fail", - } - failure = None - if result.returncode != 0: - failure = f"distribution verification exited with status {result.returncode}" - return evidence, failure - - -def fetch_public(url: str) -> bytes: - request = urllib.request.Request( - url, - headers={"Accept": "application/vnd.github.raw+json", "User-Agent": "release-recovery-conformance"}, - ) - try: - with urllib.request.urlopen(request, timeout=20) as response: - return response.read() - except (OSError, urllib.error.HTTPError) as error: - raise ConformanceError(f"cannot read public conformance target: {url}") from error - - -def audit_public_targets(contract: dict[str, Any], contract_raw: bytes) -> list[dict[str, str]]: - results: list[dict[str, str]] = [] - for consumer in contract["consumers"]: - repository = consumer["repository"].removeprefix("durable-workflow/") - branch = consumer["target_branch"] - base = f"https://raw.githubusercontent.com/durable-workflow/{repository}/{branch}" - remote_contract = fetch_public(f"{base}/scripts/ci/release-recovery-consumer-contract.json") - remote_adapter_raw = fetch_public(f"{base}/scripts/ci/release-recovery-consumer-adapter.json") - remote_suite = fetch_public(f"{base}/scripts/ci/release_recovery_consumer_conformance.py") - try: - remote_adapter = json.loads(remote_adapter_raw) - except json.JSONDecodeError as error: - raise ConformanceError(f"{consumer['component']} adapter is not valid JSON") from error - if remote_contract != contract_raw: - raise ConformanceError(f"{consumer['component']} does not carry the current shared contract") - if not isinstance(remote_adapter, dict): - raise ConformanceError(f"{consumer['component']} adapter is not a JSON object") - expected_identity = { - "component": consumer["component"], - "repository": consumer["repository"], - "target_branch": consumer["target_branch"], - } - if any(remote_adapter.get(field) != value for field, value in expected_identity.items()): - raise ConformanceError(f"{consumer['component']} adapter has the wrong target identity") - if remote_adapter.get("contract") != { - "path": "scripts/ci/release-recovery-consumer-contract.json", - "sha256": sha256_bytes(contract_raw), - "version": contract["version"], - }: - raise ConformanceError(f"{consumer['component']} adapter does not pin the current contract") - if sha256_bytes(remote_suite) != contract["suite"]["sha256"]: - raise ConformanceError(f"{consumer['component']} does not carry the current shared suite") - results.append( - { - "component": consumer["component"], - "status": "pass", - "target_branch": consumer["target_branch"], - } - ) - return results - - -def source_commit(repository_root: Path) -> str: - github_sha = os.environ.get("GITHUB_SHA", "") - if COMMIT_PATTERN.fullmatch(github_sha): - return github_sha - result = subprocess.run( - ["git", "rev-parse", "HEAD"], - cwd=repository_root, - check=False, - capture_output=True, - text=True, - ) - commit = result.stdout.strip() - return commit if result.returncode == 0 and COMMIT_PATTERN.fullmatch(commit) else "unknown" - - -def write_evidence(path: Path, evidence: dict[str, Any]) -> None: - path.parent.mkdir(parents=True, exist_ok=True) - path.write_bytes(canonical_json(evidence)) - - -def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser() - parser.add_argument("--contract", required=True, type=Path) - parser.add_argument("--adapter", type=Path) - parser.add_argument("--evidence", type=Path) - parser.add_argument("--previous-ref") - parser.add_argument("--shared-only", action="store_true") - parser.add_argument("--audit-public-targets", action="store_true") - return parser.parse_args() - - -def main() -> int: - args = parse_args() - suite_path = Path(__file__).resolve() - contract_path = args.contract.resolve() - repository_root = Path.cwd().resolve() - contract, contract_raw = load_json_object(contract_path, "shared contract") - contract_sha256 = validate_contract(contract, contract_raw, suite_path) - previous = previous_contract(repository_root, contract_path, args.previous_ref) - require_versioned_contract_change(previous, contract) - - if args.audit_public_targets: - targets = audit_public_targets(contract, contract_raw) - evidence = { - "schema": EVIDENCE_SCHEMA, - "contract": { - "sha256": contract_sha256, - "suite_sha256": contract["suite"]["sha256"], - "version": contract["version"], - }, - "generated_at": dt.datetime.now(dt.UTC).isoformat(), - "outcome": "pass", - "source_commit": source_commit(repository_root), - "targets": targets, - } - if args.evidence is not None: - write_evidence(args.evidence, evidence) - print(f"release-recovery consumer contract {contract['version']} passed for {len(targets)} public targets") - return 0 - - if args.adapter is None: - print(f"release-recovery consumer contract {contract['version']} is valid ({contract_sha256})") - return 0 - - adapter, _adapter_raw = load_json_object(args.adapter.resolve(), "consumer adapter") - consumer_path, distribution_command = validate_adapter( - adapter, - contract, - contract_sha256, - repository_root, - suite_path, - contract_path, - ) - module = load_consumer(consumer_path) - cases, failures = run_cases(module) - distribution: dict[str, Any] = {"command": distribution_command, "status": "not-run"} - if not failures and not args.shared_only: - distribution, distribution_failure = run_distribution(distribution_command, repository_root) - if distribution_failure is not None: - failures.append(distribution_failure) - evidence = { - "schema": EVIDENCE_SCHEMA, - "component": adapter["component"], - "contract": { - "sha256": contract_sha256, - "suite_sha256": contract["suite"]["sha256"], - "version": contract["version"], - }, - "cases": cases, - "distribution_verification": distribution, - "generated_at": dt.datetime.now(dt.UTC).isoformat(), - "outcome": "fail" if failures else "pass", - "repository": adapter["repository"], - "source_commit": source_commit(repository_root), - "target_branch": adapter["target_branch"], - } - if failures: - evidence["failures"] = failures - if args.evidence is not None: - write_evidence(args.evidence, evidence) - if failures: - for failure in failures: - print(f"FAIL: {failure}", file=sys.stderr) - return 1 - print( - f"{adapter['component']} satisfies release-recovery consumer contract {contract['version']} ({contract_sha256})" - ) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/ci/sdk-rust-release-plan-recovery.fixture.yml b/scripts/ci/sdk-rust-release-plan-recovery.fixture.yml deleted file mode 100644 index e90c5c4..0000000 --- a/scripts/ci/sdk-rust-release-plan-recovery.fixture.yml +++ /dev/null @@ -1,171 +0,0 @@ -name: Release plan recovery - -run-name: Recover Rust SDK from ${{ inputs.plan_tag || 'latest public release plan' }} - -on: - schedule: - - cron: '47 * * * *' - workflow_dispatch: - inputs: - plan_tag: - description: Immutable release-plan tag; empty selects the newest public plan - required: false - type: string - default: '' - -permissions: - attestations: read - contents: read - -concurrency: - group: release-plan-recovery-sdk-rust-${{ inputs.plan_tag || 'latest' }} - cancel-in-progress: false - -jobs: - discover: - name: Discover exact Rust SDK release - runs-on: ubuntu-latest - timeout-minutes: 10 - outputs: - action: ${{ steps.recovery.outputs.action }} - plan: ${{ steps.recovery.outputs.plan }} - plan_tag: ${{ steps.recovery.outputs.plan_tag }} - version: ${{ steps.recovery.outputs.version }} - commit: ${{ steps.recovery.outputs.commit }} - steps: - - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - - - name: Discover plan and verify upstream public artifacts - id: recovery - env: - GITHUB_TOKEN: ${{ github.token }} - REQUESTED_PLAN_TAG: ${{ inputs.plan_tag }} - run: | - arguments=( - resolve - --component sdk-rust - --plan-output release-plan.json - --preparation-output release-preparation.json - --evidence release-recovery-evidence.json - --github-output "$GITHUB_OUTPUT" - ) - if [ "$GITHUB_EVENT_NAME" = schedule ]; then - arguments+=(--allow-empty) - elif [ -n "$REQUESTED_PLAN_TAG" ]; then - arguments+=(--plan-tag "$REQUESTED_PLAN_TAG") - fi - python scripts/ci/component-release-recovery.py "${arguments[@]}" - - - name: Retain recovery evidence - if: always() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 - with: - name: sdk-rust-release-recovery-${{ steps.recovery.outputs.plan || github.run_id }} - path: | - release-plan.json - release-preparation.json - release-recovery-evidence.json - if-no-files-found: warn - - publish: - name: Publish exact Rust SDK release - needs: discover - if: >- - github.repository == 'durable-workflow/sdk-rust' && - github.ref == 'refs/heads/main' && - needs.discover.outputs.action == 'publish' - runs-on: ubuntu-latest - timeout-minutes: 30 - environment: release-plan-publication - permissions: - actions: write - contents: read - steps: - - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - with: - fetch-depth: 0 - ssh-key: ${{ secrets.RELEASE_PLAN_DEPLOY_KEY }} - - - name: Restore the immutable release plan - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 - with: - name: sdk-rust-release-recovery-${{ needs.discover.outputs.plan }} - path: recovery-input - - - name: Create or verify the exact planned source tag - env: - PLAN_TAG: ${{ needs.discover.outputs.plan_tag }} - RELEASE_TAG: ${{ needs.discover.outputs.version }} - RELEASE_COMMIT: ${{ needs.discover.outputs.commit }} - run: | - python scripts/ci/publish-planned-tag.py \ - --tag "$RELEASE_TAG" --commit "$RELEASE_COMMIT" --plan-tag "$PLAN_TAG" \ - --evidence release-tag-publication-evidence.json - - - name: Start or resume repository-owned publication - env: - GH_TOKEN: ${{ github.token }} - PLAN_TAG: ${{ needs.discover.outputs.plan_tag }} - RELEASE_TAG: ${{ needs.discover.outputs.version }} - RELEASE_COMMIT: ${{ needs.discover.outputs.commit }} - run: | - set -euo pipefail - - select_publication_run() { - gh run list --workflow release.yml --event workflow_dispatch --branch main --limit 100 \ - --json databaseId,event,displayTitle,headBranch,headSha,status,conclusion \ - > publication-runs.json - python scripts/ci/component-release-recovery.py select-publication-run \ - --release-tag "$RELEASE_TAG" --release-commit "$RELEASE_COMMIT" \ - --release-plan "$PLAN_TAG" \ - --runs publication-runs.json - } - - decision="$(select_publication_run)" - IFS=$'\t' read -r publication_action run_id status conclusion <<< "$decision" - if [ "$publication_action" = dispatch ]; then - gh workflow run release.yml --ref main \ - -f release_tag="$RELEASE_TAG" -f release_commit="$RELEASE_COMMIT" \ - -f release_plan="$PLAN_TAG" - for attempt in {1..12}; do - decision="$(select_publication_run)" - IFS=$'\t' read -r publication_action run_id status conclusion <<< "$decision" - [ "$publication_action" != dispatch ] && break - [ "$attempt" -eq 12 ] || sleep 5 - done - if [ "$publication_action" = dispatch ]; then - printf 'Publication dispatch did not become observable for %s at %s.\n' \ - "$RELEASE_TAG" "$RELEASE_COMMIT" >&2 - exit 1 - fi - fi - - if [ "$publication_action" = rerun ]; then - gh run rerun "$run_id" - publication_action=wait - fi - if [ "$publication_action" = wait ]; then - gh run watch "$run_id" --exit-status --interval 10 - else - printf 'Durable publication run %s is %s/%s; no duplicate dispatch is needed.\n' \ - "$run_id" "$status" "${conclusion:-pending}" - fi - - - name: Verify crates.io source identity and the GitHub Release - env: - GITHUB_TOKEN: ${{ github.token }} - run: | - python scripts/ci/component-release-recovery.py verify \ - --component sdk-rust --plan recovery-input/release-plan.json \ - --attempts 6 --sleep 10 --evidence release-completion-evidence.json - - - name: Retain publication evidence - if: always() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 - with: - name: sdk-rust-release-publication-${{ needs.discover.outputs.plan }} - path: | - release-tag-publication-evidence.json - publication-runs.json - release-completion-evidence.json - if-no-files-found: warn diff --git a/scripts/ci/test-component-release-recovery.py b/scripts/ci/test-component-release-recovery.py deleted file mode 100644 index 477276b..0000000 --- a/scripts/ci/test-component-release-recovery.py +++ /dev/null @@ -1,3310 +0,0 @@ -#!/usr/bin/env python3 -"""Focused regressions for release recovery workflow source verification.""" - -from __future__ import annotations - -import copy -import datetime as dt -import hashlib -import importlib.util -import io -import json -import shutil -import subprocess -import sys -import tempfile -import unittest -import urllib.error -from pathlib import Path -from unittest import mock - -import release_recovery_consumer_conformance as consumer_conformance -from cli_release_verifier_contract import ( # noqa: F401 - imported for unittest discovery - CliRecoveryWorkflowSourceTest, - CliReleaseAuthorityTest, -) -from recovery_workflow_authority import ( - SCHEMA as AUTHORITY_SCHEMA, -) -from recovery_workflow_authority import ( - SOURCE_IDENTITY, - authority_ref_url, - authority_url, - qualification_runs_url, -) - -RECOVERY_SCRIPT = Path(__file__).with_name("component-release-recovery.py") -CONSUMER_CONFORMANCE_SCRIPT = Path(__file__).with_name("release_recovery_consumer_conformance.py") -CONSUMER_CONTRACT_PATH = Path(__file__).with_name("release-recovery-consumer-contract.json") -CONSUMER_ADAPTER_PATH = Path(__file__).with_name("release-recovery-consumer-adapter.json") -RUST_WORKFLOW_FIXTURE = Path(__file__).with_name("sdk-rust-release-plan-recovery.fixture.yml") -REPOSITORY_ROOT = Path(__file__).resolve().parents[2] -RECOVERY_WORKFLOW = REPOSITORY_ROOT / ".github/workflows/release-plan-recovery.yml" -PUBLISH_WORKFLOW = REPOSITORY_ROOT / ".github/workflows/publish.yml" - -# This is the complete public sdk-rust workflow identified by the verifier's -# pinned digest, not a reduced semantic approximation of its shell commands. -CURRENT_RUST_RECOVERY_WORKFLOW = RUST_WORKFLOW_FIXTURE.read_text() - -GENERIC_RECOVERY_WORKFLOW = r"""on: - schedule: - workflow_dispatch: -jobs: - recover: - steps: - - run: | - python recovery.py resolve --preparation-output release-preparation.json - gh api --method POST "repos/$GITHUB_REPOSITORY/git/refs" \ - -f ref="refs/tags/$RELEASE_TAG" -f sha="$RELEASE_COMMIT" - select-publication-run \ - --release-tag "$RELEASE_TAG" --release-commit "$RELEASE_COMMIT" - gh run list --json databaseId,displayTitle,headBranch,headSha,status,conclusion - gh workflow run release.yml --ref "$RELEASE_TAG" -f tag="$RELEASE_TAG" -""" - - -def load_recovery_module(): - spec = importlib.util.spec_from_file_location("component_release_recovery_test", RECOVERY_SCRIPT) - assert spec is not None and spec.loader is not None - module = importlib.util.module_from_spec(spec) - sys.modules[spec.name] = module - spec.loader.exec_module(module) - return module - - -def github_http_error(status: int, body: bytes = b"error", **headers: str) -> urllib.error.HTTPError: - return urllib.error.HTTPError( - "https://api.github.com/repos/durable-workflow/.github/releases", - status, - "request failed", - headers, - io.BytesIO(body), - ) - - -def github_cli_result( - status: int = 200, - body: bytes = b"[]", - *, - stderr: bytes = b"", - **headers: str, -) -> subprocess.CompletedProcess[bytes]: - response_headers = b"".join(f"{name}: {value}\r\n".encode() for name, value in headers.items()) - output = f"HTTP/2.0 {status} response\r\n".encode() + response_headers + b"\r\n" + body - return subprocess.CompletedProcess( - ["gh", "api"], - 0 if 200 <= status <= 299 else 1, - output, - stderr, - ) - - -class SharedContractVersionGuardTest(unittest.TestCase): - def contract(self, version: str, content_marker: str) -> dict[str, object]: - contract = json.loads(CONSUMER_CONTRACT_PATH.read_text()) - contract["version"] = version - contract["cases"][0]["requirement"] += f" ({content_marker})" - return contract - - def write_contract(self, root: Path, contract: dict[str, object]) -> Path: - path = root / "scripts/ci/release-recovery-consumer-contract.json" - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text( - json.dumps( - contract, - indent=2, - sort_keys=True, - ensure_ascii=True, - ) - + "\n" - ) - return path - - def git(self, root: Path, *arguments: str) -> str: - result = subprocess.run( - ["git", *arguments], - cwd=root, - check=True, - capture_output=True, - text=True, - ) - return result.stdout.strip() - - def run_transition( - self, - previous: dict[str, object] | None, - current: dict[str, object], - *, - previous_ref: str | None = None, - ) -> subprocess.CompletedProcess[str]: - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - self.git(root, "init", "--quiet") - if previous is None: - (root / "README.md").write_text("contract not adopted\n") - else: - self.write_contract(root, previous) - self.git(root, "add", "--all") - self.git( - root, - "-c", - "user.name=Release Recovery Test", - "-c", - "user.email=release-recovery@example.invalid", - "commit", - "--quiet", - "--message=baseline", - ) - baseline = self.git(root, "rev-parse", "HEAD") - contract_path = self.write_contract(root, current) - return subprocess.run( - [ - sys.executable, - str(CONSUMER_CONFORMANCE_SCRIPT), - "--contract", - str(contract_path), - "--previous-ref", - previous_ref or baseline, - ], - cwd=root, - check=False, - capture_output=True, - text=True, - ) - - def assert_transition_passes( - self, - previous: dict[str, object] | None, - current: dict[str, object], - ) -> None: - result = self.run_transition(previous, current) - self.assertEqual(0, result.returncode, result.stderr) - - def assert_transition_fails( - self, - previous: dict[str, object] | None, - current: dict[str, object], - message: str, - *, - previous_ref: str | None = None, - ) -> None: - result = self.run_transition( - previous, - current, - previous_ref=previous_ref, - ) - self.assertNotEqual(0, result.returncode, result.stdout) - self.assertIn(message, result.stderr) - - def test_changed_content_with_unchanged_version_is_rejected(self): - self.assert_transition_fails( - self.contract("1.3.0", "previous"), - self.contract("1.3.0", "current"), - "strictly advancing SemVer version", - ) - - def test_suite_digest_change_requires_strictly_advancing_version(self): - current = json.loads(CONSUMER_CONTRACT_PATH.read_text()) - current["version"] = "1.4.2" - previous = copy.deepcopy(current) - previous["suite"]["sha256"] = "0" * 64 - - self.assert_transition_fails( - previous, - current, - "strictly advancing SemVer version", - ) - - current["version"] = "1.5.0" - self.assert_transition_passes(previous, current) - - def test_patch_minor_and_major_advances_are_accepted(self): - for label, current_version in { - "patch": "1.2.4", - "minor": "1.3.0", - "major": "2.0.0", - }.items(): - with self.subTest(label=label): - self.assert_transition_passes( - self.contract("1.2.3", "previous"), - self.contract(current_version, "current"), - ) - - def test_prerelease_advance_is_accepted(self): - self.assert_transition_passes( - self.contract("1.3.0-rc.1", "previous"), - self.contract("1.3.0-rc.2", "current"), - ) - - def test_downgrade_is_rejected(self): - self.assert_transition_fails( - self.contract("2.0.0", "previous"), - self.contract("1.9.9", "current"), - "strictly advancing SemVer version", - ) - - def test_build_metadata_only_change_is_rejected(self): - self.assert_transition_fails( - self.contract("1.3.0+previous", "previous"), - self.contract("1.3.0+current", "current"), - "strictly advancing SemVer version", - ) - - def test_leading_zero_numeric_prerelease_is_rejected(self): - self.assert_transition_fails( - self.contract("1.2.0", "previous"), - self.contract("1.3.0-rc.01", "current"), - "shared contract version must be exact SemVer", - ) - - def test_first_adoption_without_a_previous_contract_is_accepted(self): - self.assert_transition_passes( - None, - self.contract("1.0.0", "current"), - ) - - def test_unavailable_previous_commit_is_rejected(self): - self.assert_transition_fails( - self.contract("1.2.0", "previous"), - self.contract("1.3.0", "current"), - "previous contract commit is unavailable", - previous_ref="f" * 40, - ) - - -class ConsumerContractIdentityRegressionTest(unittest.TestCase): - def adapter_fixture( - self, - root: Path, - ) -> tuple[dict[str, object], dict[str, object], str, Path, Path]: - ci_root = root / "scripts/ci" - ci_root.mkdir(parents=True) - suite_path = ci_root / CONSUMER_CONFORMANCE_SCRIPT.name - contract_path = ci_root / CONSUMER_CONTRACT_PATH.name - consumer_path = ci_root / RECOVERY_SCRIPT.name - verifier_path = ci_root / Path(__file__).name - shutil.copyfile(CONSUMER_CONFORMANCE_SCRIPT, suite_path) - contract = json.loads(CONSUMER_CONTRACT_PATH.read_text()) - contract_raw = consumer_conformance.canonical_json(contract) - contract_path.write_bytes(contract_raw) - consumer_path.write_text("# consumer fixture\n") - verifier_path.write_text("# verifier fixture\n") - adapter = json.loads(CONSUMER_ADAPTER_PATH.read_text()) - return ( - adapter, - contract, - consumer_conformance.sha256_bytes(contract_raw), - suite_path, - contract_path, - ) - - def test_matching_declared_and_invoked_contract_passes(self): - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - adapter, contract, digest, suite_path, contract_path = self.adapter_fixture(root) - - consumer, command = consumer_conformance.validate_adapter( - adapter, - contract, - digest, - root, - suite_path, - contract_path, - ) - - self.assertEqual("component-release-recovery.py", consumer.name) - self.assertEqual(["{python}", "scripts/ci/test-component-release-recovery.py"], command) - - def test_alternate_invoked_contract_is_rejected(self): - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - adapter, contract, digest, suite_path, contract_path = self.adapter_fixture(root) - alternate_path = contract_path.with_name("alternate-contract.json") - alternate_path.write_bytes(contract_path.read_bytes()) - - with self.assertRaisesRegex( - consumer_conformance.ConformanceError, - "invoked contract is not the adapter's declared contract", - ): - consumer_conformance.validate_adapter( - adapter, - contract, - digest, - root, - suite_path, - alternate_path, - ) - - def test_stale_declared_contract_is_rejected(self): - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - adapter, contract, digest, suite_path, contract_path = self.adapter_fixture(root) - stale_contract = copy.deepcopy(contract) - stale_contract["version"] = "1.4.0" - contract_path.write_bytes(consumer_conformance.canonical_json(stale_contract)) - - with self.assertRaisesRegex( - consumer_conformance.ConformanceError, - "declared contract does not match its version and digest pins", - ): - consumer_conformance.validate_adapter( - adapter, - contract, - digest, - root, - suite_path, - contract_path, - ) - - def test_mismatched_declared_contract_bytes_are_rejected(self): - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - adapter, contract, digest, suite_path, contract_path = self.adapter_fixture(root) - mismatched_contract = copy.deepcopy(contract) - mismatched_contract["cases"][0]["requirement"] += " (mismatched declared bytes)" - contract_path.write_bytes(consumer_conformance.canonical_json(mismatched_contract)) - - with self.assertRaisesRegex( - consumer_conformance.ConformanceError, - "declared contract does not match its version and digest pins", - ): - consumer_conformance.validate_adapter( - adapter, - contract, - digest, - root, - suite_path, - contract_path, - ) - - -def load_recovery_for_retry_tests(): - loaded = globals().get("recovery") - if loaded is not None: - return loaded - loader = globals().get("load_recovery_module") - if not callable(loader): - raise RuntimeError("release recovery module loader is unavailable") - return loader() - - -AUTHORITY_COMMIT = "a" * 40 - - -def continuity_resolution_qualification() -> dict[str, object]: - return { - "repository": "durable-workflow/.github", - "workflow": ".github/workflows/beta-candidate.yml", - "event": "push", - "head_branch": "main", - "head_sha": "9" * 40, - "run_id": 987, - "run_attempt": 2, - "status": "completed", - "conclusion": "success", - } - - -def continuity_resolution_qualification_run() -> dict[str, object]: - qualification = continuity_resolution_qualification() - return { - "id": qualification["run_id"], - "run_attempt": qualification["run_attempt"], - "repository": {"full_name": "durable-workflow/.github"}, - "head_repository": {"full_name": "durable-workflow/.github"}, - "path": ".github/workflows/beta-candidate.yml@main", - "event": qualification["event"], - "head_branch": qualification["head_branch"], - "head_sha": qualification["head_sha"], - "status": qualification["status"], - "conclusion": qualification["conclusion"], - } - - -def lifecycle_plan(module, channel: str = "alpha") -> dict[str, object]: - prerelease = channel - return { - "schema": module.SCHEMA, - "plan": "component-recovery", - "channel": channel, - "foundation": {"tag": module.FOUNDATION_TAG, "commit": module.FOUNDATION_COMMIT}, - "components": { - name: { - "version": (f"2.0.0-{prerelease}.{index + 1}" if name in {"workflow", "waterline"} else f"1.0.{index}"), - "commit": f"{index + 1:040x}", - } - for index, name in enumerate(module.COMPONENTS) - }, - "beta_authorization": ( - {"tag": "beta-authorization/component-recovery", "commit": "f" * 40} - if channel in {"beta", "rc"} - else None - ), - } - - -def supersession_record(module, failed, successor, failed_commit: str) -> dict[str, object]: - identity = failed["components"]["workflow"] - observed_commit = "e" * 40 - environment_url = ( - "https://github.com/durable-workflow/.github/deployments/activity_log?" - "environments_filter=release-plan-supersession" - ) - protection = { - "custom_branch_policies": [{"id": 22, "name": "main"}], - "deployment_branch_policy": { - "custom_branch_policies": True, - "protected_branches": False, - }, - "environment_id": 11, - "environment_url": environment_url, - "required_reviewer_rule_ids": [33], - } - return { - "schema": "durable-workflow.release-plan-failure/v1", - "outcome": "terminal-failure", - "failed_plan": { - "tag": f"release-plan/{failed['plan']}", - "commit": failed_commit, - "sha256": module.manifest_digest(failed), - }, - "conflicts": [ - { - "component": "workflow", - "version": identity["version"], - "planned_commit": identity["commit"], - "observed_commit": observed_commit, - "reason": "published-version-source-conflict", - "github_release": { - "id": 44, - "url": "https://github.com/durable-workflow/workflow/releases/44", - }, - "distribution": { - "kind": "composer", - "source_reference": observed_commit, - "dist_reference": observed_commit, - }, - } - ], - "successor_plan": { - "tag": f"release-plan/{successor['plan']}", - "sha256": module.manifest_digest(successor), - }, - "authorization": { - "actor": "release-operator", - "environment": "release-plan-supersession", - "environment_approval": { - "comment": "approved", - "environments": [ - { - "html_url": environment_url, - "id": 11, - "name": "release-plan-supersession", - "node_id": "environment-node", - "url": ( - "https://api.github.com/repos/durable-workflow/.github/" - "environments/release-plan-supersession" - ), - } - ], - "run_attempt": 1, - "run_id": 456, - "state": "approved", - "user": { - "html_url": "https://github.com/release-reviewer", - "id": 55, - "login": "release-reviewer", - "node_id": "reviewer-node", - "url": "https://api.github.com/users/release-reviewer", - }, - }, - "environment_protection": protection, - "repository": "durable-workflow/.github", - "run_attempt": 1, - "run_id": 456, - "run_url": "https://github.com/durable-workflow/.github/actions/runs/456", - "workflow_commit": "f" * 40, - "workflow_ref": ( - "durable-workflow/.github/.github/workflows/release-plan-supersession.yml@refs/heads/main" - ), - }, - } - - -def captured_github_authority(module, record: dict[str, object]) -> list[object]: - authorization = record["authorization"] - protection = authorization["environment_protection"] - approval = authorization["environment_approval"] - return [ - { - "id": protection["environment_id"], - "html_url": protection["environment_url"], - "protection_rules": [ - { - "id": protection["required_reviewer_rule_ids"][0], - "type": "required_reviewers", - "reviewers": [ - { - "type": "User", - "reviewer": { - **approval["user"], - "avatar_url": "https://avatars.githubusercontent.com/u/55?v=4", - "site_admin": False, - "type": "User", - }, - } - ], - } - ], - "deployment_branch_policy": protection["deployment_branch_policy"], - }, - { - "total_count": 1, - "branch_policies": [ - {**protection["custom_branch_policies"][0], "type": "branch"} - ], - }, - { - "actor": {"login": authorization["actor"]}, - "conclusion": "success", - "event": "workflow_dispatch", - "head_branch": "main", - "head_sha": authorization["workflow_commit"], - "html_url": authorization["run_url"], - "id": authorization["run_id"], - "path": f"{module.SUPERSESSION_WORKFLOW}@main", - "repository": {"full_name": module.CONTROL_REPOSITORY}, - "run_attempt": authorization["run_attempt"], - "status": "completed", - }, - [ - { - "comment": approval["comment"], - "environments": [ - { - **approval["environments"][0], - "can_admins_bypass": True, - "created_at": "2026-07-23T00:00:00Z", - "updated_at": "2026-07-23T00:00:00Z", - } - ], - "state": approval["state"], - "user": { - **approval["user"], - "avatar_url": "https://avatars.githubusercontent.com/u/55?v=4", - "site_admin": False, - "type": "User", - }, - } - ], - ] - - -def qualification_run( - status: str = "completed", - conclusion: str | None = "success", - *, - head_sha: str = AUTHORITY_COMMIT, - head_branch: str = "main", - path: str = ".github/workflows/beta-candidate.yml", -) -> dict[str, object]: - return { - "id": 81, - "run_attempt": 2, - "name": "Beta candidate", - "workflow_id": 37, - "path": path, - "event": "push", - "head_branch": head_branch, - "head_sha": head_sha, - "status": status, - "conclusion": conclusion, - "url": "https://api.github.com/repos/durable-workflow/.github/actions/runs/81", - "html_url": "https://github.com/durable-workflow/.github/actions/runs/81", - } - - -class QualifiedAuthorityConsumerTest(unittest.TestCase): - @classmethod - def setUpClass(cls) -> None: - cls.recovery = load_recovery_for_retry_tests() - - def authority(self) -> dict[str, object]: - return { - "schema": AUTHORITY_SCHEMA, - "source": SOURCE_IDENTITY, - "workflows": { - name: { - "repository": component.repository, - "ref": f"refs/heads/{component.default_branch}", - "path": ".github/workflows/release-plan-recovery.yml", - "state": "active", - "sha256": "b" * 64, - } - for name, component in self.recovery.COMPONENTS.items() - }, - } - - def client(self, runs: list[dict[str, object]]): - authority_raw = json.dumps(self.authority()).encode("utf-8") - - class Client: - def __init__(self) -> None: - self.requests: list[tuple[str, str]] = [] - - def json(self, url: str) -> dict[str, object]: - self.requests.append(("json", url)) - if url == authority_ref_url(): - return {"sha": AUTHORITY_COMMIT} - if url == qualification_runs_url(AUTHORITY_COMMIT): - return {"total_count": len(runs), "workflow_runs": runs} - raise AssertionError(f"peer source was read before authority qualification: {url}") - - def bytes(self, url: str, *, accept: str | None = None) -> bytes: - self.requests.append(("bytes", url)) - if url != authority_url(AUTHORITY_COMMIT): - raise AssertionError(f"peer source was read before authority qualification: {url}") - return authority_raw - - return Client(), authority_raw - - def test_green_qualification_binds_manifest_bytes_and_revision(self) -> None: - client, authority_raw = self.client([qualification_run()]) - workflows, source = self.recovery.load_recovery_workflow_authority(client) - - self.assertEqual(set(self.recovery.COMPONENTS), set(workflows)) - self.assertEqual(AUTHORITY_COMMIT, source["commit"]) - self.assertEqual(hashlib.sha256(authority_raw).hexdigest(), source["sha256"]) - self.assertEqual(AUTHORITY_COMMIT, source["qualification"]["head_sha"]) - self.assertEqual(".github/workflows/beta-candidate.yml", source["qualification"]["path"]) - self.assertEqual("main", source["qualification"]["head_branch"]) - self.assertEqual( - [ - ("json", authority_ref_url()), - ("json", qualification_runs_url(AUTHORITY_COMMIT)), - ("bytes", authority_url(AUTHORITY_COMMIT)), - ], - client.requests, - ) - - def test_non_green_fails_before_authority_or_peer_source_reads(self) -> None: - cases = ( - ("pending", [qualification_run("in_progress", None)], "pending"), - ("failed", [qualification_run("completed", "failure")], "failed"), - ("cancelled", [qualification_run("completed", "cancelled")], "cancelled"), - ("absent", [], "absent"), - ("revision-mismatch", [qualification_run(head_sha="c" * 40)], "another commit"), - ( - "wrong-workflow", - [qualification_run(path=".github/workflows/source-qualification.yml")], - "absent", - ), - ("wrong-ref", [qualification_run(head_branch="v2")], "absent"), - ( - "wrong-path-ref", - [qualification_run(path=".github/workflows/beta-candidate.yml@v2")], - "absent", - ), - ) - for label, runs, message in cases: - with self.subTest(state=label): - client, _authority_raw = self.client(runs) - with self.assertRaisesRegex(self.recovery.RecoveryError, message): - self.recovery.load_recovery_workflow_authority(client) - self.assertEqual( - [ - ("json", authority_ref_url()), - ("json", qualification_runs_url(AUTHORITY_COMMIT)), - ], - client.requests, - ) - - -class ContinuityGateTest(unittest.TestCase): - @classmethod - def setUpClass(cls) -> None: - cls.recovery = load_recovery_for_retry_tests() - - def test_scheduled_recovery_pauses_until_remote_resume(self) -> None: - plan = {"plan": "workspace-unavailable-test"} - with ( - mock.patch.object( - self.recovery, - "resolve_tag", - side_effect=["a" * 40, None], - ), - mock.patch.object(self.recovery, "read_record", return_value=plan), - mock.patch.object(self.recovery, "validate_plan"), - ): - paused = self.recovery.scheduled_continuity_pause(mock.Mock(), plan) - - self.assertEqual( - "beta-continuity/workspace-unavailable-test/resumed", - paused["resumed_tag"], - ) - with ( - mock.patch.object( - self.recovery, - "resolve_tag", - side_effect=["a" * 40, "b" * 40], - ), - mock.patch.object(self.recovery, "read_record", return_value=plan), - mock.patch.object(self.recovery, "validate_plan"), - ): - self.assertIsNone(self.recovery.scheduled_continuity_pause(mock.Mock(), plan)) - - -class PublicClientRetryTest(unittest.TestCase): - @classmethod - def setUpClass(cls) -> None: - cls.recovery = load_recovery_for_retry_tests() - - def test_authenticated_requests_preserve_endpoint_api_versions(self) -> None: - cases = ( - ({"X-GitHub-Api-Version": self.recovery.SUPERSESSION_API_VERSION}, self.recovery.SUPERSESSION_API_VERSION), - ({}, "2022-11-28"), - ) - for headers, expected_version in cases: - with self.subTest(expected_version=expected_version): - client = self.recovery.PublicClient(token="test-token") - with ( - mock.patch.dict( - self.recovery.os.environ, - {"GITHUB_ACTIONS": "true", "GH_HOST": "redirected.example"}, - ), - mock.patch.object( - self.recovery.subprocess, - "run", - return_value=github_cli_result(), - ) as run, - ): - response = client.request( - "https://api.github.com/repos/durable-workflow/.github/actions/runs/456", - headers=headers, - ) - - self.assertEqual(b"[]", response.read()) - command = run.call_args.args[0] - self.assertEqual( - ["gh", "api", "--hostname", "github.com", "--include", "--method", "GET"], - command[:7], - ) - declared_headers = [ - command[index + 1] - for index, argument in enumerate(command) - if argument == "--header" - ] - self.assertIn(f"X-GitHub-Api-Version: {expected_version}", declared_headers) - self.assertFalse(any(header.lower().startswith("authorization:") for header in declared_headers)) - self.assertEqual("test-token", run.call_args.kwargs["env"]["GH_TOKEN"]) - - def test_runner_environment_never_uses_a_live_urllib_api_call(self) -> None: - client = self.recovery.PublicClient(token="test-token") - with ( - mock.patch.dict(self.recovery.os.environ, {"GITHUB_ACTIONS": "true"}), - mock.patch.object( - self.recovery.urllib.request, - "urlopen", - side_effect=AssertionError("runner transport bypassed the GitHub CLI mock"), - ) as open_url, - mock.patch.object( - self.recovery.subprocess, - "run", - return_value=github_cli_result(), - ) as run, - ): - self.assertEqual( - [], - client.json("https://api.github.com/repos/durable-workflow/.github/releases"), - ) - - open_url.assert_not_called() - run.assert_called_once() - - def test_runner_rate_limit_headers_are_case_insensitive_and_use_reset_delay(self) -> None: - sleeps: list[float] = [] - client = self.recovery.PublicClient( - token="test-token", - max_attempts=2, - retry_base_seconds=1, - sleep=sleeps.append, - now=lambda: 100, - ) - with ( - mock.patch.dict(self.recovery.os.environ, {"GITHUB_ACTIONS": "true"}), - mock.patch.object( - self.recovery.subprocess, - "run", - side_effect=[ - github_cli_result( - 403, - b'{"message":"Forbidden"}', - **{ - "x-ratelimit-remaining": "0", - "X-rAtElImIt-ReSeT": "112", - }, - ), - github_cli_result(), - ], - ) as run, - ): - result = client.json("https://api.github.com/repos/durable-workflow/.github/releases") - - self.assertEqual([], result) - self.assertEqual([12], sleeps) - self.assertEqual(2, run.call_count) - - def test_retries_service_failures_connection_resets_and_timeouts(self) -> None: - failures = ( - ("service", github_http_error(503, **{"Retry-After": "4"}), 4), - ("connection-reset", urllib.error.URLError(ConnectionResetError("reset")), 1), - ("timeout", urllib.error.URLError(TimeoutError("timed out")), 1), - ) - - for label, failure, expected_delay in failures: - with self.subTest(label=label): - sleeps: list[float] = [] - client = self.recovery.PublicClient( - max_attempts=2, - retry_base_seconds=1, - sleep=sleeps.append, - ) - with mock.patch.object( - self.recovery.urllib.request, - "urlopen", - side_effect=[failure, io.BytesIO(b"[]")], - ) as open_url: - self.assertEqual( - [], - client.json("https://api.github.com/repos/durable-workflow/.github/releases?per_page=100"), - ) - - self.assertEqual([expected_delay], sleeps) - self.assertEqual(2, open_url.call_count) - - def test_authentication_is_terminal_even_with_rate_limit_guidance(self) -> None: - sleeps: list[float] = [] - client = self.recovery.PublicClient(max_attempts=3, sleep=sleeps.append) - error = github_http_error( - 401, - b"Bad credentials: API rate limit exceeded", - **{"Retry-After": "20", "X-RateLimit-Remaining": "0"}, - ) - - with ( - mock.patch.object(self.recovery.urllib.request, "urlopen", side_effect=error) as open_url, - self.assertRaisesRegex(self.recovery.RecoveryError, r"public request failed \(401\)"), - ): - client.json("https://api.github.com/repos/durable-workflow/.github/releases?per_page=100") - - self.assertEqual([], sleeps) - self.assertEqual(1, open_url.call_count) - - def test_authorization_requires_explicit_rate_limit_guidance(self) -> None: - client = self.recovery.PublicClient( - max_attempts=2, - sleep=lambda _delay: self.fail("ordinary authorization failure was retried"), - ) - with ( - mock.patch.object( - self.recovery.urllib.request, - "urlopen", - side_effect=github_http_error(403, b"Resource not accessible"), - ) as open_url, - self.assertRaisesRegex(self.recovery.RecoveryError, r"public request failed \(403\)"), - ): - client.json("https://api.github.com/repos/durable-workflow/.github/releases?per_page=100") - self.assertEqual(1, open_url.call_count) - - sleeps: list[float] = [] - client = self.recovery.PublicClient(max_attempts=2, retry_base_seconds=1, sleep=sleeps.append) - with mock.patch.object( - self.recovery.urllib.request, - "urlopen", - side_effect=[ - github_http_error( - 403, - b"API rate limit exceeded", - **{"X-RateLimit-Remaining": "0"}, - ), - io.BytesIO(b"[]"), - ], - ) as open_url: - self.assertEqual( - [], - client.json("https://api.github.com/repos/durable-workflow/.github/releases?per_page=100"), - ) - self.assertEqual([1], sleeps) - self.assertEqual(2, open_url.call_count) - - def test_retry_exhaustion_has_a_distinct_infrastructure_classification(self) -> None: - client = self.recovery.PublicClient(max_attempts=2, retry_base_seconds=1, sleep=lambda _delay: None) - with ( - mock.patch.object( - self.recovery.urllib.request, - "urlopen", - side_effect=[github_http_error(503), github_http_error(502)], - ) as open_url, - self.assertRaisesRegex( - self.recovery.PublicInfrastructureError, - r"classification=github-read-transient, endpoint_class=releases-api, " - r"attempts=2, reason=retry-exhausted, status=502", - ), - ): - client.json("https://api.github.com/repos/durable-workflow/.github/releases?per_page=100") - self.assertEqual(2, open_url.call_count) - - def test_download_rejects_coercible_content_digest_before_publication_read(self) -> None: - client = self.recovery.PublicClient() - with ( - mock.patch.object(client, "_run") as public_read, - self.assertRaisesRegex( - self.recovery.RecoveryError, - "invalid expected SHA-256", - ), - ): - client.download( - "https://example.invalid/artifact", - Path("unused-artifact"), - expected_sha256=int("8" * 64), - ) - public_read.assert_not_called() - - -class ImmutablePlanDiscoveryTest(unittest.TestCase): - @classmethod - def setUpClass(cls) -> None: - cls.recovery = load_recovery_for_retry_tests() - - def test_updated_older_release_cannot_override_newer_immutable_plan(self) -> None: - older = lifecycle_plan(self.recovery) - older["plan"] = "older-alpha" - newer = lifecycle_plan(self.recovery, "beta") - newer["plan"] = "newer-beta" - tags = ["release-plan/older-alpha", "release-plan/newer-beta"] - commits = {tags[0]: "a" * 40, tags[1]: "b" * 40} - recorded = { - "a" * 40: dt.datetime(2026, 7, 20, tzinfo=dt.UTC), - "b" * 40: dt.datetime(2026, 7, 22, tzinfo=dt.UTC), - } - - with ( - mock.patch.object( - self.recovery, - "list_release_plan_tags", - # The older Release may now appear first, but Release order is not authority. - return_value=tags, - ), - mock.patch.object( - self.recovery, - "resolve_tag", - side_effect=lambda _client, _repository, tag: commits[tag], - ), - mock.patch.object( - self.recovery, - "read_plan_authority", - side_effect=[(older, None), (newer, None), (older, None), (newer, None)], - ), - mock.patch.object( - self.recovery, - "direct_plan_lifecycle", - side_effect=[ - ("actionable", None), - ("completed", None), - ("actionable", None), - ("completed", None), - ], - ), - mock.patch.object( - self.recovery, - "immutable_plan_recorded_at", - side_effect=lambda _client, commit: recorded[commit], - ), - mock.patch.object( - self.recovery, - "accepted_continuity_supersession", - return_value=None, - ), - ): - selected = self.recovery.select_implicit_plan_authority(mock.Mock()) - self.assertEqual(tags[1], selected["tag"]) - self.assertEqual("completed", selected["lifecycle"]) - - def test_equal_versions_with_different_source_commits_are_conflicting(self) -> None: - first = lifecycle_plan(self.recovery, "beta") - first["plan"] = "first-beta-authority" - second = json.loads(json.dumps(first)) - second["plan"] = "conflicting-beta-authority" - second["components"]["workflow"]["commit"] = "f" * 40 - authorities = [ - {"tag": f"release-plan/{first['plan']}", "plan": first}, - {"tag": f"release-plan/{second['plan']}", "plan": second}, - ] - - with self.assertRaisesRegex( - self.recovery.RecoveryError, - "conflicting current product trains", - ): - self.recovery.current_product_train_authorities(authorities) - - def test_strict_semver_validation_precedes_authority_selection(self) -> None: - for malformed in ("01.0.0", "1.0.0-alpha.01", "1.0.0-alpha..1", "1.0.0\n"): - candidate = lifecycle_plan(self.recovery, "beta") - candidate["components"]["server"]["version"] = malformed - authority = { - "tag": f"release-plan/{candidate['plan']}", - "plan": candidate, - } - - with self.subTest(version=malformed), self.assertRaisesRegex( - self.recovery.RecoveryError, - "components.server.version is not exact SemVer", - ): - self.recovery.current_product_train_authorities([authority]) - - for valid in ("1.0.0-alpha.1", "1.0.0-alpha.1+build.01", "1.0.0+build.01"): - candidate = lifecycle_plan(self.recovery, "beta") - candidate["components"]["server"]["version"] = valid - - with self.subTest(version=valid): - self.recovery.validate_plan(candidate) - - def test_unbounded_numeric_semver_identifiers_are_selected(self) -> None: - long_numeric = "9" * 4301 - cases = ( - ("core", "1.0.0", f"{long_numeric}.0.0"), - ("prerelease", "1.0.0-alpha.1", f"1.0.0-alpha.{long_numeric}"), - ) - - for kind, lower_version, higher_version in cases: - lower = lifecycle_plan(self.recovery, "beta") - lower["plan"] = f"unbounded-{kind}-lower" - lower["components"]["server"]["version"] = lower_version - higher = json.loads(json.dumps(lower)) - higher["plan"] = f"unbounded-{kind}-higher" - higher["components"]["server"]["version"] = higher_version - authorities = [ - {"tag": f"release-plan/{lower['plan']}", "plan": lower}, - {"tag": f"release-plan/{higher['plan']}", "plan": higher}, - ] - - with self.subTest(kind=kind): - self.assertEqual( - [f"release-plan/{higher['plan']}"], - [ - authority["tag"] - for authority in self.recovery.current_product_train_authorities( - authorities - ) - ], - ) - - def test_semver_successors_cover_both_terminal_conflict_paths(self) -> None: - long_numeric = "9" * 4301 - cases = ( - ("release", "1.2.3", "1.2.4"), - ("prerelease", "1.2.3-alpha.9", "1.2.3-alpha.10"), - ("release-build", "1.2.3+build.1", "1.2.4+build.2"), - ("prerelease-build", "1.2.3-alpha.9+build.1", "1.2.3-alpha.10+build.2"), - ("single-numeric-prerelease", "1.2.3-9", "1.2.3-10"), - ("single-numeric-prerelease-build", "1.2.3-9+build.1", "1.2.3-10+build.2"), - ("nonnumeric-prerelease", "1.2.3-rc", "1.2.3-rc.1"), - ("nonnumeric-prerelease-build", "1.2.3-rc+build.1", "1.2.3-rc.1+build.2"), - ("long-core", f"1.2.{long_numeric}", f"1.2.1{'0' * 4301}"), - ("long-prerelease", f"1.2.3-alpha.{long_numeric}", f"1.2.3-alpha.1{'0' * 4301}"), - ) - reasons = ( - self.recovery.SUPERSESSION_REASON, - self.recovery.OCCUPIED_SOURCE_MANIFEST_REASON, - ) - - for reason in reasons: - for label, previous_version, successor_version in cases: - failed = lifecycle_plan(self.recovery, "beta") - failed["plan"] = f"semver-{label}-failed" - failed["components"]["server"]["version"] = previous_version - successor = json.loads(json.dumps(failed)) - successor["plan"] = f"semver-{label}-successor" - successor["components"]["server"]["version"] = successor_version - if reason == self.recovery.OCCUPIED_SOURCE_MANIFEST_REASON: - successor["components"]["server"]["commit"] = "e" * 40 - - with self.subTest(reason=reason, kind=label): - self.recovery.validate_successor_transition( - failed, - successor, - [{"component": "server", "reason": reason}], - ) - - failed = lifecycle_plan(self.recovery, "beta") - failed["plan"] = "semver-long-skipped-failed" - failed["components"]["server"]["version"] = f"1.2.{long_numeric}" - successor = json.loads(json.dumps(failed)) - successor["plan"] = "semver-long-skipped-successor" - successor["components"]["server"]["version"] = f"1.2.2{'0' * 4301}" - if reason == self.recovery.OCCUPIED_SOURCE_MANIFEST_REASON: - successor["components"]["server"]["commit"] = "e" * 40 - - with self.subTest(reason=reason, kind="invalid"), self.assertRaises(self.recovery.RecoveryError) as raised: - self.recovery.validate_successor_transition( - failed, - successor, - [{"component": "server", "reason": reason}], - ) - self.assertEqual("plan-discovery", raised.exception.phase) - - def test_validated_source_manifest_supersession_selects_successor(self) -> None: - predecessor = lifecycle_plan(self.recovery, "beta") - predecessor["plan"] = "source-manifest-predecessor" - successor = json.loads(json.dumps(predecessor)) - successor["plan"] = "source-manifest-successor" - successor["components"]["workflow"]["commit"] = "f" * 40 - successor_tag = f"release-plan/{successor['plan']}" - successor_authority = { - "tag": successor_tag, - "plan": successor, - "lifecycle": "actionable", - "successor": None, - } - authorities = [ - { - "tag": f"release-plan/{predecessor['plan']}", - "plan": predecessor, - "lifecycle": "superseded", - "successor": { - "tag": successor_tag, - "sha256": self.recovery.manifest_digest(successor), - "plan": successor, - }, - }, - successor_authority, - ] - - self.assertEqual( - [successor_authority], - self.recovery.current_product_train_authorities(authorities), - ) - - def test_scheduled_recovery_without_plan_authority_records_no_op(self) -> None: - with tempfile.TemporaryDirectory() as directory: - root = Path(directory) - evidence = root / "release-recovery-evidence.json" - github_output = root / "github-output" - arguments = [ - "component-release-recovery.py", - "resolve", - "--component", - "workflow", - "--plan-output", - str(root / "release-plan.json"), - "--preparation-output", - str(root / "release-preparation.json"), - "--evidence", - str(evidence), - "--github-output", - str(github_output), - "--allow-empty", - ] - - with ( - mock.patch.object(sys, "argv", arguments), - mock.patch.object( - self.recovery, - "discover_plan", - side_effect=self.recovery.RecoveryError( - "no public release plan is available", - "plan-discovery", - ), - ), - mock.patch.object(self.recovery, "resolve_component") as recover_component, - ): - self.assertEqual(0, self.recovery.main()) - - recover_component.assert_not_called() - state = json.loads(evidence.read_text()) - self.assertEqual("plan-discovery", state["phase"]) - self.assertEqual("no-op", state["outcome"]) - self.assertEqual("action=none\n", github_output.read_text()) - - def test_explicit_completed_plan_is_selected_for_verification(self) -> None: - candidate = lifecycle_plan(self.recovery, "beta") - tag = f"release-plan/{candidate['plan']}" - commit = "a" * 40 - authority = { - "tag": tag, - "commit": commit, - "recorded_at": dt.datetime(2026, 7, 24, tzinfo=dt.UTC), - "plan": candidate, - "preparation": None, - "lifecycle": "completed", - "successor": None, - } - with mock.patch.object(self.recovery, "classify_plan_authorities", return_value=[authority]): - selected = self.recovery.select_explicit_plan_authority( - mock.Mock(), tag, commit, candidate, None - ) - self.assertEqual({**authority, "selection": "explicit"}, selected) - - def test_concurrent_terminal_supersession_retries_before_returning_action(self) -> None: - older = lifecycle_plan(self.recovery) - older["plan"] = "older-plan" - successor = lifecycle_plan(self.recovery) - successor["plan"] = "successor-plan" - older_tag = "release-plan/older-plan" - successor_tag = "release-plan/successor-plan" - commits = {older_tag: "a" * 40, successor_tag: "b" * 40} - plans = {older_tag: older, successor_tag: successor} - recorded = { - commits[older_tag]: dt.datetime(2026, 7, 20, tzinfo=dt.UTC), - commits[successor_tag]: dt.datetime(2026, 7, 21, tzinfo=dt.UTC), - } - terminal_failure: dict[str, object] = {} - registry_reads = 0 - - def list_tags(_client: mock.Mock) -> list[str]: - nonlocal registry_reads - registry_reads += 1 - if registry_reads == 2: - terminal_failure.update( - {"outcome": "terminal-failure", "successor": successor_tag} - ) - return ( - [older_tag, successor_tag] - if terminal_failure - else [older_tag] - ) - - def lifecycle( - _client: mock.Mock, - tag: str, - _commit: str, - _plan: dict[str, object], - _preparation: None, - ) -> tuple[str, object | None]: - if tag == older_tag and terminal_failure: - return "superseded", { - "tag": successor_tag, - "sha256": self.recovery.manifest_digest(successor), - "plan": successor, - } - return "actionable", None - - with ( - mock.patch.object( - self.recovery, - "list_release_plan_tags", - side_effect=list_tags, - ), - mock.patch.object( - self.recovery, - "resolve_tag", - side_effect=lambda _client, _repository, tag: commits[tag], - ), - mock.patch.object( - self.recovery, - "read_plan_authority", - side_effect=lambda _client, tag, _commit: (plans[tag], None), - ), - mock.patch.object( - self.recovery, - "direct_plan_lifecycle", - side_effect=lifecycle, - ), - mock.patch.object( - self.recovery, - "immutable_plan_recorded_at", - side_effect=lambda _client, commit: recorded[commit], - ), - mock.patch.object( - self.recovery, - "accepted_continuity_supersession", - return_value=None, - ), - ): - selected = self.recovery.select_implicit_plan_authority(mock.Mock()) - - self.assertEqual(successor_tag, selected["tag"]) - self.assertEqual("actionable", selected["lifecycle"]) - self.assertEqual(4, registry_reads) - - def test_convergence_rechecks_nonselected_lifecycle_authority(self) -> None: - older = {"tag": "release-plan/older", "lifecycle": "completed"} - changed_older = {**older, "lifecycle": "superseded"} - latest = {"tag": "release-plan/latest", "lifecycle": "actionable"} - current_snapshot = [changed_older, latest] - - with mock.patch.object( - self.recovery, - "classify_implicit_plan_authority", - side_effect=[ - (latest, [older, latest]), - (latest, current_snapshot), - (latest, current_snapshot), - (latest, current_snapshot), - ], - ) as classify: - selected = self.recovery.select_implicit_plan_authority(mock.Mock()) - - self.assertEqual(4, classify.call_count) - self.assertEqual(current_snapshot, selected["authority_snapshot"]) - - def test_final_implicit_boundary_rejects_continuity_pause_activated_after_initial_read( - self, - ) -> None: - candidate = lifecycle_plan(self.recovery) - candidate_preparation = { - "components": { - "workflow": { - "release_notes": { - "release_date": "2026-07-23", - "sha256": "c" * 64, - "source": {}, - } - } - } - } - component = self.recovery.COMPONENTS["workflow"] - selected = {"tag": "release-plan/current", "lifecycle": "actionable"} - authority = {"authority_snapshot": [selected]} - continuity = mock.Mock( - side_effect=[ - None, - { - "accepted_tag": f"beta-continuity/{candidate['plan']}/accepted", - "accepted_commit": "b" * 40, - "resumed_tag": f"beta-continuity/{candidate['plan']}/resumed", - }, - ] - ) - publication_preflight = mock.Mock( - side_effect=self.recovery.NotFound("not published") - ) - - with ( - mock.patch.object(self.recovery, "verify_plan_authority", return_value=({}, {})), - mock.patch.object(self.recovery, "validate_release_preparation"), - mock.patch.object(self.recovery, "resolve_tag", return_value=None), - mock.patch.object( - self.recovery, - "classify_implicit_plan_authority", - return_value=(selected, [selected]), - ), - mock.patch.object( - self.recovery, - "scheduled_continuity_pause", - continuity, - ), - mock.patch.dict( - self.recovery.VERIFIERS, - {component.distribution: publication_preflight}, - ), - ): - self.assertIsNone(continuity(mock.Mock(), candidate)) - with self.assertRaisesRegex( - self.recovery.RecoveryError, - "continuity pause authority changed during component preflight", - ): - self.recovery.resolve_component( - mock.Mock(), - "workflow", - selected["tag"], - "a" * 40, - candidate, - candidate_preparation, - authority, - ) - - self.assertEqual(2, continuity.call_count) - self.assertEqual(1, publication_preflight.call_count) - - def test_final_implicit_boundary_rejects_stale_publish_but_explicit_actionable_recovery_does_not( - self, - ) -> None: - candidate = lifecycle_plan(self.recovery) - candidate_preparation = { - "components": { - "workflow": { - "release_notes": { - "release_date": "2026-07-23", - "sha256": "c" * 64, - "source": {}, - } - } - } - } - component = self.recovery.COMPONENTS["workflow"] - publication_preflight = mock.Mock( - side_effect=self.recovery.NotFound("not published") - ) - implicit_authority = { - "authority_snapshot": [ - {"tag": "release-plan/older", "lifecycle": "actionable"} - ] - } - current_snapshot = [ - {"tag": "release-plan/older", "lifecycle": "superseded"}, - {"tag": "release-plan/successor", "lifecycle": "actionable"}, - ] - explicit_authority = { - "selection": "explicit", - "tag": "release-plan/older", - "commit": "a" * 40, - "recorded_at": dt.datetime(2026, 7, 23, tzinfo=dt.UTC), - "plan": candidate, - "preparation": candidate_preparation, - "lifecycle": "actionable", - "successor": None, - } - current_explicit_authority = { - key: value - for key, value in explicit_authority.items() - if key != "selection" - } - - with ( - mock.patch.object(self.recovery, "verify_plan_authority", return_value=({}, {})), - mock.patch.object(self.recovery, "validate_release_preparation"), - mock.patch.object(self.recovery, "resolve_tag", return_value=None), - mock.patch.object( - self.recovery, - "classify_implicit_plan_authority", - return_value=(current_snapshot[-1], current_snapshot), - ) as classify, - mock.patch.object( - self.recovery, - "classify_plan_authorities", - return_value=[current_explicit_authority], - ) as classify_explicit, - mock.patch.dict( - self.recovery.VERIFIERS, - {component.distribution: publication_preflight}, - ), - ): - with self.assertRaisesRegex( - self.recovery.RecoveryError, - "refusing a stale recovery action", - ): - self.recovery.resolve_component( - mock.Mock(), - "workflow", - "release-plan/older", - "a" * 40, - candidate, - candidate_preparation, - implicit_authority, - ) - - for lifecycle in ("actionable", "interrupted"): - with self.subTest(explicit_lifecycle=lifecycle): - explicit_authority["lifecycle"] = lifecycle - current_explicit_authority["lifecycle"] = lifecycle - state, outputs = self.recovery.resolve_component( - mock.Mock(), - "workflow", - "release-plan/older", - "a" * 40, - candidate, - candidate_preparation, - explicit_authority, - ) - self.assertEqual("publish", outputs["action"]) - self.assertEqual("publication", state["phase"]) - - self.assertEqual(1, classify.call_count) - self.assertEqual(2, classify_explicit.call_count) - self.assertEqual(3, publication_preflight.call_count) - - def test_interrupted_plan_rejects_multiple_continuity_successors(self) -> None: - interrupted = lifecycle_plan(self.recovery) - interrupted["plan"] = "interrupted-plan" - first_successor = json.loads(json.dumps(interrupted)) - first_successor["plan"] = "first-successor" - second_successor = json.loads(json.dumps(interrupted)) - second_successor["plan"] = "second-successor" - tags = [ - f"release-plan/{interrupted['plan']}", - f"release-plan/{first_successor['plan']}", - f"release-plan/{second_successor['plan']}", - ] - commits = { - tags[0]: "a" * 40, - tags[1]: "b" * 40, - tags[2]: "c" * 40, - } - recorded = { - commits[tags[0]]: dt.datetime(2026, 7, 20, tzinfo=dt.UTC), - commits[tags[1]]: dt.datetime(2026, 7, 21, tzinfo=dt.UTC), - commits[tags[2]]: dt.datetime(2026, 7, 22, tzinfo=dt.UTC), - } - interruption_tag = f"{self.recovery.CONTINUITY_TAG_PREFIX}{interrupted['plan']}/interrupted" - interruption_commit = "d" * 40 - interruption_evidence = {"phase": "interrupted"} - superseded_interruption = { - "tag": interruption_tag, - "commit": interruption_commit, - "evidence_sha256": self.recovery.manifest_digest(interruption_evidence), - "plan_sha256": self.recovery.manifest_digest(interrupted), - "reason": self.recovery.CONTINUITY_SUPERSESSION_REASON, - } - - with ( - mock.patch.object( - self.recovery, - "list_release_plan_tags", - return_value=tags, - ), - mock.patch.object( - self.recovery, - "resolve_tag", - side_effect=lambda _client, _repository, tag: ( - interruption_commit if tag == interruption_tag else commits[tag] - ), - ), - mock.patch.object( - self.recovery, - "read_plan_authority", - side_effect=[ - (interrupted, None), - (first_successor, None), - (second_successor, None), - ], - ), - mock.patch.object( - self.recovery, - "direct_plan_lifecycle", - side_effect=[ - ("interrupted", interruption_tag), - ("completed", None), - ("completed", None), - ], - ), - mock.patch.object( - self.recovery, - "immutable_plan_recorded_at", - side_effect=lambda _client, commit: recorded[commit], - ), - mock.patch.object( - self.recovery, - "accepted_continuity_supersession", - side_effect=[ - None, - superseded_interruption, - superseded_interruption, - ], - ), - mock.patch.object( - self.recovery, - "list_continuity_resolution_tags", - return_value=[], - ), - mock.patch.object( - self.recovery, - "read_record", - return_value=interruption_evidence, - ), - self.assertRaisesRegex( - self.recovery.RecoveryError, - "multiple continuity successors", - ), - ): - self.recovery.select_implicit_plan_authority(mock.Mock()) - - def test_continuity_successor_fork_accepts_exact_digest_bound_resolution(self) -> None: - interrupted_plan = {"plan": "interrupted"} - interrupted = { - "tag": "release-plan/interrupted", - "commit": "a" * 40, - "plan": interrupted_plan, - } - interruption = { - "tag": "beta-continuity/interrupted/interrupted", - "commit": "b" * 40, - "evidence_sha256": "c" * 64, - } - successors = [] - for index, name in enumerate(("first-successor", "second-successor"), start=1): - successors.append( - { - "tag": f"release-plan/{name}", - "supersession": { - **interruption, - "continuity_claim": { - "plan": { - "tag": f"release-plan/{name}", - "commit": str(index) * 40, - "sha256": str(index + 2) * 64, - }, - "acceptance": { - "tag": f"beta-continuity/{name}/accepted", - "commit": str(index + 4) * 40, - "sha256": str(index + 6) * 64, - }, - }, - }, - } - ) - claims = [successor["supersession"]["continuity_claim"] for successor in successors] - resolution = { - "schema": self.recovery.CONTINUITY_RESOLUTION_SCHEMA, - "qualification": continuity_resolution_qualification(), - "interruption": { - "plan": { - "tag": interrupted["tag"], - "commit": interrupted["commit"], - "sha256": self.recovery.manifest_digest(interrupted_plan), - }, - "evidence": { - "tag": interruption["tag"], - "commit": interruption["commit"], - "sha256": interruption["evidence_sha256"], - }, - }, - "successor_claims": claims, - "selected_successor": claims[1]["plan"], - } - resolution_tag = ( - f"{self.recovery.CONTINUITY_RESOLUTION_TAG_PREFIX}interrupted/" - f"{self.recovery.manifest_digest(resolution)}" - ) - client = mock.Mock() - client.json.return_value = continuity_resolution_qualification_run() - with ( - mock.patch.object( - self.recovery, - "list_continuity_resolution_tags", - return_value=[resolution_tag], - ), - mock.patch.object(self.recovery, "resolve_tag", return_value="f" * 40), - mock.patch.object(self.recovery, "read_record", return_value=resolution), - ): - selected = self.recovery.resolve_continuity_successor_fork( - client, - interrupted, - successors, - ) - self.assertEqual("release-plan/second-successor", selected) - valid_run = continuity_resolution_qualification_run() - failures = ( - (None, "qualification is absent"), - ({**valid_run, "status": "in_progress", "conclusion": None}, "qualification is pending"), - ({**valid_run, "conclusion": "failure"}, "qualification failed"), - ({**valid_run, "conclusion": "cancelled"}, "qualification was cancelled"), - ({**valid_run, "head_sha": "8" * 40}, "another source revision"), - ({**valid_run, "path": ".github/workflows/untrusted.yml@main"}, "untrusted workflow"), - ) - with ( - mock.patch.object(self.recovery, "list_continuity_resolution_tags", return_value=[resolution_tag]), - mock.patch.object(self.recovery, "resolve_tag", return_value="f" * 40), - mock.patch.object(self.recovery, "read_record", return_value=resolution), - ): - for run, message in failures: - with self.subTest(qualification=message): - client.json.return_value = run - with self.assertRaisesRegex(self.recovery.RecoveryError, message): - self.recovery.resolve_continuity_successor_fork(client, interrupted, successors) - - def test_terminal_failure_successor_requires_exact_authorized_plan_identity(self) -> None: - failed = lifecycle_plan(self.recovery) - failed["plan"] = "failed-plan" - authorized_successor = json.loads(json.dumps(failed)) - authorized_successor["plan"] = "successor-plan" - authorized_successor["components"]["workflow"]["version"] = "2.0.0-alpha.2" - recorded_successor = json.loads(json.dumps(authorized_successor)) - recorded_successor["components"]["workflow"]["commit"] = "e" * 40 - failed_tag = f"release-plan/{failed['plan']}" - successor_tag = f"release-plan/{authorized_successor['plan']}" - failed_commit = "a" * 40 - successor_commit = "b" * 40 - failure_commit = "c" * 40 - failure = supersession_record( - self.recovery, - failed, - authorized_successor, - failed_commit, - ) - - with ( - mock.patch.object( - self.recovery, - "resolve_tag", - side_effect=[None, failure_commit], - ), - mock.patch.object( - self.recovery, - "read_record", - side_effect=[failure, authorized_successor], - ), - mock.patch.object(self.recovery, "revalidate_supersession_authority"), - ): - lifecycle, successor_identity = self.recovery.direct_plan_lifecycle( - mock.Mock(), - failed_tag, - failed_commit, - failed, - None, - ) - - self.assertEqual("superseded", lifecycle) - self.assertEqual( - { - "tag": successor_tag, - "sha256": self.recovery.manifest_digest(authorized_successor), - "plan": authorized_successor, - }, - successor_identity, - ) - - commits = {failed_tag: failed_commit, successor_tag: successor_commit} - recorded = { - failed_commit: dt.datetime(2026, 7, 20, tzinfo=dt.UTC), - successor_commit: dt.datetime(2026, 7, 21, tzinfo=dt.UTC), - } - with ( - mock.patch.object( - self.recovery, - "list_release_plan_tags", - return_value=[failed_tag, successor_tag], - ), - mock.patch.object( - self.recovery, - "resolve_tag", - side_effect=lambda _client, _repository, tag: commits[tag], - ), - mock.patch.object( - self.recovery, - "read_plan_authority", - side_effect=[(failed, None), (recorded_successor, None)], - ), - mock.patch.object( - self.recovery, - "direct_plan_lifecycle", - side_effect=[ - (lifecycle, successor_identity), - ("completed", None), - ], - ), - mock.patch.object( - self.recovery, - "immutable_plan_recorded_at", - side_effect=lambda _client, commit: recorded[commit], - ), - mock.patch.object( - self.recovery, - "accepted_continuity_supersession", - return_value=None, - ), - self.assertRaisesRegex( - self.recovery.RecoveryError, - "conflicting successor identity", - ), - ): - self.recovery.select_implicit_plan_authority(mock.Mock()) - - def test_terminal_failure_normalizes_captured_github_approval_shape(self) -> None: - failed = lifecycle_plan(self.recovery) - successor = json.loads(json.dumps(failed)) - successor["plan"] = "successor-plan" - successor["components"]["workflow"]["version"] = "2.0.0-alpha.2" - record = supersession_record(self.recovery, failed, successor, "a" * 40) - client = mock.Mock() - client.json.side_effect = captured_github_authority(self.recovery, record) - - self.recovery.revalidate_supersession_authority(record, client) - - self.assertEqual(4, client.json.call_count) - mutations = ( - ("run", "id", 999), - ("run", "run_attempt", 2), - ("environment", "id", 999), - ("history", "state", "rejected"), - ("reviewer", "id", 999), - ) - for target, field, value in mutations: - with self.subTest(target=target, field=field): - responses = captured_github_authority(self.recovery, record) - if target == "run": - responses[2][field] = value - elif target == "environment": - responses[0][field] = value - elif target == "history": - responses[3][0][field] = value - else: - responses[3][0]["user"][field] = value - client = mock.Mock() - client.json.side_effect = responses - with self.assertRaises(self.recovery.RecoveryError): - self.recovery.revalidate_supersession_authority(record, client) - - def test_terminal_failure_rejects_approval_history_for_a_rerun_attempt(self) -> None: - failed = lifecycle_plan(self.recovery) - successor = json.loads(json.dumps(failed)) - successor["plan"] = "successor-plan" - successor["components"]["workflow"]["version"] = "2.0.0-alpha.2" - record = supersession_record(self.recovery, failed, successor, "a" * 40) - authorization = record["authorization"] - authorization["run_attempt"] = 2 - authorization["environment_approval"]["run_attempt"] = 2 - client = mock.Mock() - client.json.side_effect = captured_github_authority(self.recovery, record) - - with self.assertRaisesRegex( - self.recovery.RecoveryError, - "approval history cannot bind.*rerun attempt", - ): - self.recovery.revalidate_supersession_authority(record, client) - - self.assertEqual(3, client.json.call_count) - - def test_terminal_failure_rejects_approver_outside_current_policy(self) -> None: - failed = lifecycle_plan(self.recovery) - successor = json.loads(json.dumps(failed)) - successor["plan"] = "successor-plan" - successor["components"]["workflow"]["version"] = "2.0.0-alpha.2" - record = supersession_record(self.recovery, failed, successor, "a" * 40) - responses = captured_github_authority(self.recovery, record) - responses[0]["protection_rules"][0]["reviewers"][0]["reviewer"].update( - { - "html_url": "https://github.com/different-reviewer", - "id": 77, - "login": "different-reviewer", - "node_id": "different-reviewer-node", - "url": "https://api.github.com/users/different-reviewer", - } - ) - client = mock.Mock() - client.json.side_effect = responses - - with self.assertRaisesRegex( - self.recovery.RecoveryError, - "approving user is not authorized by the current reviewer policy", - ): - self.recovery.revalidate_supersession_authority(record, client) - - self.assertEqual(4, client.json.call_count) - - def test_terminal_failure_rejects_incomplete_lifecycle_authority(self) -> None: - failed = lifecycle_plan(self.recovery) - failed["plan"] = "failed-plan" - successor = json.loads(json.dumps(failed)) - successor["plan"] = "successor-plan" - successor["components"]["workflow"]["version"] = "2.0.0-alpha.2" - failed_tag = f"release-plan/{failed['plan']}" - failed_commit = "a" * 40 - incomplete = { - "schema": "durable-workflow.release-plan-failure/v1", - "outcome": "terminal-failure", - "failed_plan": { - "tag": failed_tag, - "commit": failed_commit, - "sha256": self.recovery.manifest_digest(failed), - }, - "successor_plan": { - "tag": f"release-plan/{successor['plan']}", - "sha256": self.recovery.manifest_digest(successor), - }, - } - - with ( - mock.patch.object( - self.recovery, - "resolve_tag", - side_effect=[None, "c" * 40], - ), - mock.patch.object( - self.recovery, - "read_record", - side_effect=[incomplete, successor], - ), - self.assertRaisesRegex( - self.recovery.RecoveryError, - "record keys must be exactly", - ), - ): - self.recovery.direct_plan_lifecycle( - mock.Mock(), - failed_tag, - failed_commit, - failed, - None, - ) - - def test_terminal_failure_rejects_boolean_approval_run_identity(self) -> None: - failed = lifecycle_plan(self.recovery) - failed["plan"] = "failed-plan" - successor = json.loads(json.dumps(failed)) - successor["plan"] = "successor-plan" - successor["components"]["workflow"]["version"] = "2.0.0-alpha.2" - failed_tag = f"release-plan/{failed['plan']}" - failed_commit = "a" * 40 - - for field in ("run_id", "run_attempt"): - with self.subTest(field=field): - failure = supersession_record( - self.recovery, - failed, - successor, - failed_commit, - ) - authorization = failure["authorization"] - approval = authorization["environment_approval"] - authorization[field] = 1 - approval[field] = True - if field == "run_id": - authorization["run_url"] = "https://github.com/durable-workflow/.github/actions/runs/1" - - with ( - mock.patch.object( - self.recovery, - "resolve_tag", - side_effect=[None, "c" * 40], - ), - mock.patch.object( - self.recovery, - "read_record", - side_effect=[failure, successor], - ), - self.assertRaisesRegex( - self.recovery.RecoveryError, - "lacks an approved deployment bound to its workflow run", - ), - ): - self.recovery.direct_plan_lifecycle( - mock.Mock(), - failed_tag, - failed_commit, - failed, - None, - ) - - def test_terminal_failure_rejects_malformed_authorization_json_types(self) -> None: - failed = lifecycle_plan(self.recovery) - failed["plan"] = "failed-plan" - successor = json.loads(json.dumps(failed)) - successor["plan"] = "successor-plan" - successor["components"]["workflow"]["version"] = "2.0.0-alpha.2" - failed_commit = "a" * 40 - valid_failure = supersession_record( - self.recovery, - failed, - successor, - failed_commit, - ) - valid_failure["authorization"]["run_id"] = 1 - valid_failure["authorization"]["run_url"] = "https://github.com/durable-workflow/.github/actions/runs/1" - valid_failure["authorization"]["environment_approval"]["run_id"] = 1 - self.recovery.validate_supersession_record( - valid_failure, - failed, - failed_commit, - successor, - ) - mutations = ( - (("authorization", "actor"), True), - (("authorization", "workflow_commit"), int("1" * 40)), - (("authorization", "environment_approval", "run_id"), True), - (("authorization", "environment_approval", "run_attempt"), True), - ( - ( - "authorization", - "environment_protection", - "deployment_branch_policy", - "custom_branch_policies", - ), - 1, - ), - ( - ( - "authorization", - "environment_protection", - "deployment_branch_policy", - "protected_branches", - ), - 0, - ), - ) - - for path, value in mutations: - with self.subTest(field=".".join(path)): - malformed = json.loads(json.dumps(valid_failure)) - target = malformed - for key in path[:-1]: - target = target[key] - target[path[-1]] = value - - with self.assertRaises(self.recovery.RecoveryError): - self.recovery.validate_supersession_record( - malformed, - failed, - failed_commit, - successor, - ) - - def test_terminal_failure_rejects_numeric_commit_references(self) -> None: - failed = lifecycle_plan(self.recovery) - failed["plan"] = "failed-plan" - successor = json.loads(json.dumps(failed)) - successor["plan"] = "successor-plan" - successor["components"]["workflow"]["version"] = "2.0.0-alpha.2" - failed_commit = "a" * 40 - failure = supersession_record( - self.recovery, - failed, - successor, - failed_commit, - ) - numeric_commit = int("1" * 40) - conflict = failure["conflicts"][0] - conflict["observed_commit"] = numeric_commit - conflict["distribution"]["source_reference"] = numeric_commit - conflict["distribution"]["dist_reference"] = numeric_commit - - with self.assertRaisesRegex( - self.recovery.RecoveryError, - "does not prove a different public source identity", - ): - self.recovery.validate_supersession_record( - failure, - failed, - failed_commit, - successor, - ) - - def test_authority_records_reject_coercible_commit_digest_and_tag_object_types(self) -> None: - beta_plan = lifecycle_plan(self.recovery, "beta") - beta_plan["beta_authorization"]["commit"] = int("1" * 40) - with self.assertRaisesRegex( - self.recovery.RecoveryError, - "beta and release-candidate plans require immutable beta qualification", - ): - self.recovery.validate_plan(beta_plan) - - plan = lifecycle_plan(self.recovery) - identity = plan["components"]["sdk-python"] - specification = self.recovery.SOURCE_MANIFESTS["sdk-python"] - source_manifest = { - "declared_version": identity["version"], - "package": specification["package"], - "path": specification["path"], - "sha256": int("2" * 64), - "source_commit": identity["commit"], - "url": ( - "https://github.com/durable-workflow/sdk-python/blob/" - f"{identity['commit']}/{specification['path']}" - ), - } - with self.assertRaisesRegex( - self.recovery.RecoveryError, - "invalid source-manifest evidence", - ): - self.recovery.validate_source_manifest_evidence( - source_manifest, - "sdk-python", - identity, - must_match_version=True, - ) - - github_release, distribution = self.recovery.publication_absence_locations( - "sdk-python", - identity["version"], - ) - occupied_conflict = { - "source_tag": { - "commit": identity["commit"], - "repository": "durable-workflow/sdk-python", - "tag": identity["version"], - "tag_object": int("3" * 40), - "url": f"https://github.com/durable-workflow/sdk-python/tree/{identity['version']}", - }, - "github_release": github_release, - "distribution": distribution, - } - with self.assertRaisesRegex( - self.recovery.RecoveryError, - "occupied planned source tag", - ): - self.recovery.validate_occupied_source_manifest_evidence( - occupied_conflict, - "sdk-python", - identity, - ) - - client = mock.Mock() - client.json.return_value = { - "object": { - "type": "commit", - "sha": int("4" * 40), - } - } - with self.assertRaisesRegex( - self.recovery.RecoveryError, - "does not resolve to a commit", - ): - self.recovery.resolve_tag(client, "durable-workflow/sdk-python", identity["version"]) - - def test_continuity_authority_rejects_coercible_commit_and_digest_types(self) -> None: - plan = lifecycle_plan(self.recovery) - tag = f"release-plan/{plan['plan']}" - digest = self.recovery.manifest_digest(plan) - superseded = { - "commit": "a" * 40, - "evidence_sha256": "b" * 64, - "plan_sha256": "c" * 64, - "reason": self.recovery.CONTINUITY_SUPERSESSION_REASON, - "tag": f"{self.recovery.CONTINUITY_TAG_PREFIX}{plan['plan']}/interrupted", - } - authority = {"tag": tag, "plan": plan} - - for field, value in ( - ("commit", int("5" * 40)), - ("evidence_sha256", int("6" * 64)), - ("plan_sha256", int("7" * 64)), - ): - with self.subTest(field=field): - malformed_superseded = dict(superseded) - malformed_superseded[field] = value - evidence = { - "schema": self.recovery.CONTINUITY_EVIDENCE_SCHEMA, - "phase": "accepted", - "outcome": "accepted", - "release_plan": {"tag": tag, "sha256": digest}, - "candidate_identity": { - "components": plan["components"], - "plan_sha256": digest, - }, - "superseded_interruption": malformed_superseded, - } - with ( - mock.patch.object(self.recovery, "resolve_tag", return_value="d" * 40), - mock.patch.object(self.recovery, "read_record", side_effect=[evidence, plan]), - self.assertRaisesRegex( - self.recovery.RecoveryError, - "invalid superseded interruption identity", - ), - ): - self.recovery.accepted_continuity_supersession(mock.Mock(), authority) - - def assert_explicit_terminal_recovery_rejected( - self, - *, - requested_tag: str, - plans: dict[str, dict[str, object]], - commits: dict[str, str], - recorded_at: dict[str, dt.datetime], - references: dict[str, str], - records: dict[tuple[str, str, str], dict[str, object]], - github_responses: list[object], - ) -> None: - candidate = plans[requested_tag] - preparation = { - "components": { - "sdk-python": { - "release_notes": { - "release_date": "2026-07-23", - "sha256": "c" * 64, - "source": {}, - } - } - } - } - component = self.recovery.COMPONENTS["sdk-python"] - publication_preflight = mock.Mock(side_effect=self.recovery.NotFound("Python package is absent")) - client = mock.Mock() - client.json.side_effect = [{"tag_name": requested_tag}, *github_responses] - - def resolve_reference( - _client: mock.Mock, - repository: str, - tag: str, - ) -> str | None: - if repository == self.recovery.CONTROL_REPOSITORY: - return references.get(tag) - if repository == component.repository: - self.assertEqual(candidate["components"]["sdk-python"]["version"], tag) - return None - raise AssertionError(f"unexpected tag lookup for {repository}@{tag}") - - def read_plan( - _client: mock.Mock, - tag: str, - commit: str, - ) -> tuple[dict[str, object], dict[str, object]]: - self.assertEqual(commits[tag], commit) - return plans[tag], preparation - - def read_lifecycle_record( - _client: mock.Mock, - tag: str, - commit: str, - filename: str, - ) -> dict[str, object]: - return records[(tag, commit, filename)] - - with tempfile.TemporaryDirectory(prefix="explicit-terminal-recovery-") as temporary: - root = Path(temporary) - plan_output = root / "release-plan.json" - preparation_output = root / "release-preparation.json" - evidence_output = root / "release-recovery-evidence.json" - github_output = root / "github-output" - argv = [ - "component-release-recovery.py", - "resolve", - "--component", - "sdk-python", - "--plan-tag", - requested_tag, - "--plan-output", - str(plan_output), - "--preparation-output", - str(preparation_output), - "--evidence", - str(evidence_output), - "--github-output", - str(github_output), - ] - - with ( - mock.patch.object(self.recovery, "PublicClient", return_value=client), - mock.patch.object(self.recovery.sys, "argv", argv), - mock.patch.object( - self.recovery, - "list_release_plan_tags", - return_value=list(plans), - ), - mock.patch.object( - self.recovery, - "resolve_tag", - side_effect=resolve_reference, - ), - mock.patch.object( - self.recovery, - "read_plan_authority", - side_effect=read_plan, - ), - mock.patch.object( - self.recovery, - "read_record", - side_effect=read_lifecycle_record, - ), - mock.patch.object( - self.recovery, - "immutable_plan_recorded_at", - side_effect=lambda _client, commit: recorded_at[commit], - ), - mock.patch.object(self.recovery, "validate_release_mirrors"), - mock.patch.object( - self.recovery, - "verify_plan_authority", - return_value=({}, {}), - ), - mock.patch.object(self.recovery, "validate_release_preparation"), - mock.patch.object( - self.recovery, - "verify_component", - return_value={"status": "present"}, - ), - mock.patch.object( - self.recovery, - "require_python_source_manifest_version", - return_value=None, - ), - mock.patch.dict( - self.recovery.VERIFIERS, - {component.distribution: publication_preflight}, - ), - mock.patch.object(self.recovery.sys, "stderr", io.StringIO()), - ): - exit_code = self.recovery.main() - - self.assertEqual(1, exit_code) - self.assertFalse(plan_output.exists()) - self.assertFalse(preparation_output.exists()) - self.assertFalse(github_output.exists()) - failure = json.loads(evidence_output.read_bytes()) - self.assertEqual("plan-discovery", failure["phase"]) - self.assertEqual("failed", failure["outcome"]) - self.assertIn( - "terminally superseded and cannot be recovered", - failure["reason"], - ) - - publication_preflight.assert_not_called() - - def test_explicit_terminal_failure_with_absent_artifact_has_no_publish_handoff( - self, - ) -> None: - failed = lifecycle_plan(self.recovery) - failed["plan"] = "failed-plan" - successor = json.loads(json.dumps(failed)) - successor["plan"] = "successor-plan" - successor["components"]["workflow"]["version"] = "2.0.0-alpha.2" - failed_tag = f"release-plan/{failed['plan']}" - successor_tag = f"release-plan/{successor['plan']}" - failed_commit = "a" * 40 - successor_commit = "b" * 40 - failure_commit = "c" * 40 - failure_tag = f"{self.recovery.FAILURE_TAG_PREFIX}{failed['plan']}" - failure = supersession_record( - self.recovery, - failed, - successor, - failed_commit, - ) - - self.assert_explicit_terminal_recovery_rejected( - requested_tag=failed_tag, - plans={failed_tag: failed, successor_tag: successor}, - commits={ - failed_tag: failed_commit, - successor_tag: successor_commit, - }, - recorded_at={ - failed_commit: dt.datetime(2026, 7, 20, tzinfo=dt.UTC), - successor_commit: dt.datetime(2026, 7, 21, tzinfo=dt.UTC), - }, - references={ - failed_tag: failed_commit, - successor_tag: successor_commit, - failure_tag: failure_commit, - }, - records={ - ( - failure_tag, - failure_commit, - "release-plan-failure.json", - ): failure, - ( - failure_tag, - failure_commit, - "successor-release-plan.json", - ): successor, - }, - github_responses=captured_github_authority(self.recovery, failure), - ) - - def test_explicit_continuity_supersession_with_absent_artifact_has_no_publish_handoff( - self, - ) -> None: - interrupted = lifecycle_plan(self.recovery) - interrupted["plan"] = "interrupted-plan" - successor = json.loads(json.dumps(interrupted)) - successor["plan"] = "continuity-successor" - successor["components"]["workflow"]["version"] = "2.0.0-alpha.2" - interrupted_tag = f"release-plan/{interrupted['plan']}" - successor_tag = f"release-plan/{successor['plan']}" - interrupted_commit = "a" * 40 - successor_commit = "b" * 40 - interruption_tag = f"{self.recovery.CONTINUITY_TAG_PREFIX}{interrupted['plan']}/interrupted" - interruption_commit = "c" * 40 - accepted_tag = f"{self.recovery.CONTINUITY_TAG_PREFIX}{successor['plan']}/accepted" - accepted_commit = "d" * 40 - interrupted_digest = self.recovery.manifest_digest(interrupted) - successor_digest = self.recovery.manifest_digest(successor) - interruption_evidence = { - "schema": self.recovery.CONTINUITY_EVIDENCE_SCHEMA, - "phase": "interrupted", - "outcome": "intentionally-interrupted", - "release_plan": { - "tag": interrupted_tag, - "sha256": interrupted_digest, - }, - "plan_record": { - "tag": interrupted_tag, - "commit": interrupted_commit, - "sha256": interrupted_digest, - }, - } - accepted_evidence = { - "schema": self.recovery.CONTINUITY_EVIDENCE_SCHEMA, - "phase": "accepted", - "outcome": "accepted", - "release_plan": { - "tag": successor_tag, - "sha256": successor_digest, - }, - "candidate_identity": { - "components": successor["components"], - "plan_sha256": successor_digest, - }, - "superseded_interruption": { - "tag": interruption_tag, - "commit": interruption_commit, - "evidence_sha256": self.recovery.manifest_digest(interruption_evidence), - "plan_sha256": interrupted_digest, - "reason": self.recovery.CONTINUITY_SUPERSESSION_REASON, - }, - } - - self.assert_explicit_terminal_recovery_rejected( - requested_tag=interrupted_tag, - plans={ - interrupted_tag: interrupted, - successor_tag: successor, - }, - commits={ - interrupted_tag: interrupted_commit, - successor_tag: successor_commit, - }, - recorded_at={ - interrupted_commit: dt.datetime(2026, 7, 20, tzinfo=dt.UTC), - successor_commit: dt.datetime(2026, 7, 21, tzinfo=dt.UTC), - }, - references={ - interrupted_tag: interrupted_commit, - successor_tag: successor_commit, - interruption_tag: interruption_commit, - accepted_tag: accepted_commit, - }, - records={ - ( - interruption_tag, - interruption_commit, - "continuity-evidence.json", - ): interruption_evidence, - ( - interruption_tag, - interruption_commit, - "release-plan.json", - ): interrupted, - ( - accepted_tag, - accepted_commit, - "continuity-evidence.json", - ): accepted_evidence, - ( - accepted_tag, - accepted_commit, - "release-plan.json", - ): successor, - }, - github_responses=[], - ) - - def test_explicit_terminal_transition_during_preflight_cannot_publish(self) -> None: - candidate = lifecycle_plan(self.recovery) - preparation = { - "components": { - "workflow": { - "release_notes": { - "release_date": "2026-07-23", - "sha256": "c" * 64, - "source": {}, - } - } - } - } - tag = f"release-plan/{candidate['plan']}" - commit = "a" * 40 - component = self.recovery.COMPONENTS["workflow"] - authority = { - "selection": "explicit", - "tag": tag, - "commit": commit, - "recorded_at": dt.datetime(2026, 7, 23, tzinfo=dt.UTC), - "plan": candidate, - "preparation": preparation, - "lifecycle": "actionable", - "successor": None, - } - superseded = { - **authority, - "lifecycle": "superseded", - "successor": { - "tag": "release-plan/successor", - "sha256": "d" * 64, - "plan": {"plan": "successor"}, - }, - } - superseded.pop("selection") - publication_preflight = mock.Mock( - side_effect=self.recovery.NotFound("not published") - ) - with ( - mock.patch.object( - self.recovery, "verify_plan_authority", return_value=({}, {}) - ), - mock.patch.object(self.recovery, "validate_release_preparation"), - mock.patch.object(self.recovery, "resolve_tag", return_value=None), - mock.patch.object( - self.recovery, "classify_plan_authorities", return_value=[superseded] - ), - mock.patch.dict( - self.recovery.VERIFIERS, - {component.distribution: publication_preflight}, - ), - self.assertRaisesRegex( - self.recovery.RecoveryError, - "became terminally superseded during component preflight", - ), - ): - self.recovery.resolve_component( - mock.Mock(), - "workflow", - tag, - commit, - candidate, - preparation, - authority, - ) - self.assertEqual(1, publication_preflight.call_count) - - -class ReleasePreparationRecoveryTest(unittest.TestCase): - @classmethod - def setUpClass(cls) -> None: - cls.recovery = load_recovery_module() - - def candidate(self) -> dict[str, object]: - return { - "plan": "missing-preparation", - "channel": "alpha", - "components": {"workflow": {"version": "2.0.0-alpha.1", "commit": "a" * 40}}, - } - - def test_discovery_rejects_missing_preparation_for_an_incomplete_release(self) -> None: - candidate = self.candidate() - tag = "release-plan/missing-preparation" - record_commit = "b" * 40 - client = mock.Mock() - client.json.return_value = { - "tag_name": tag, - "draft": False, - "assets": [ - { - "name": "release-plan.json", - "browser_download_url": "https://example.invalid/release-plan.json", - } - ], - } - client.bytes.return_value = self.recovery.canonical_json(candidate) - with ( - mock.patch.object(self.recovery, "validate_plan"), - mock.patch.object(self.recovery, "resolve_tag", return_value=record_commit), - mock.patch.object( - self.recovery, - "select_explicit_plan_authority", - return_value={"selection": "explicit"}, - ), - mock.patch.object( - self.recovery, - "read_record", - side_effect=[candidate, self.recovery.NotFound("missing preparation")], - ), - mock.patch.object( - self.recovery, - "verify_component", - side_effect=self.recovery.NotFound("release is incomplete"), - ), - self.assertRaisesRegex(self.recovery.RecoveryError, "only completed legacy releases"), - ): - self.recovery.discover_plan(client, tag, "workflow") - - def test_missing_preparation_cannot_resolve_to_publish(self) -> None: - candidate = self.candidate() - with ( - mock.patch.object(self.recovery, "verify_plan_authority", return_value=({}, {})), - mock.patch.object(self.recovery, "resolve_tag", return_value=None), - self.assertRaisesRegex( - self.recovery.RecoveryError, - "release preparation required before publishing workflow", - ), - ): - self.recovery.resolve_component( - mock.Mock(), - "workflow", - "release-plan/missing-preparation", - "b" * 40, - candidate, - None, - ) - - def test_explicit_completed_release_still_resolves_to_skip(self) -> None: - candidate = self.candidate() - identity = candidate["components"]["workflow"] - public_evidence = {"version": identity["version"], "commit": identity["commit"]} - authority = { - "selection": "explicit", - "tag": "release-plan/missing-preparation", - "commit": "b" * 40, - "plan": candidate, - "preparation": None, - "lifecycle": "completed", - "successor": None, - } - with ( - mock.patch.object(self.recovery, "verify_plan_authority", return_value=({}, {})), - mock.patch.object(self.recovery, "resolve_tag", return_value=identity["commit"]), - mock.patch.object(self.recovery, "verify_component", return_value=public_evidence), - mock.patch.object( - self.recovery, - "classify_plan_authorities", - return_value=[ - {key: value for key, value in authority.items() if key != "selection"} - ], - ), - ): - state, outputs = self.recovery.resolve_component( - mock.Mock(), - "workflow", - "release-plan/missing-preparation", - "b" * 40, - candidate, - None, - authority, - ) - - self.assertEqual("skip", outputs["action"]) - self.assertEqual("complete", state["phase"]) - self.assertNotIn("release_preparation", state) - - -class RecoveryWorkflowSourceTest(unittest.TestCase): - @classmethod - def setUpClass(cls) -> None: - cls.recovery = load_recovery_module() - - def assert_rejected(self, source: str) -> None: - with self.assertRaises(self.recovery.RecoveryError) as caught: - self.recovery.verify_recovery_workflow_source( - "sdk-rust", - source, - hashlib.sha256(CURRENT_RUST_RECOVERY_WORKFLOW.encode("utf-8")).hexdigest(), - ) - self.assertEqual(caught.exception.phase, "default-branch-preflight") - - def test_accepts_only_the_current_protected_rust_workflow_identity(self) -> None: - digest = hashlib.sha256(CURRENT_RUST_RECOVERY_WORKFLOW.encode("utf-8")).hexdigest() - self.recovery.verify_recovery_workflow_source("sdk-rust", CURRENT_RUST_RECOVERY_WORKFLOW, digest) - self.recovery.verify_recovery_workflow_source( - "sdk-rust", - CURRENT_RUST_RECOVERY_WORKFLOW.replace("\n", "\r\n"), - digest, - ) - - def test_rejects_shell_semantic_bypasses_and_any_source_mutation(self) -> None: - source = CURRENT_RUST_RECOVERY_WORKFLOW - variants = { - "one-byte mutation": source.replace("timeout-minutes: 30", "timeout-minutes: 31", 1), - "one-line mutation": source + "\n", - "readarray release tag mutation": source.replace( - " select_publication_run() {", - " readarray -t release_identity < <(printf '%s\\n' mutable)\n" - ' RELEASE_TAG="${release_identity[0]}"\n\n' - " select_publication_run() {", - 1, - ), - "successful early exit": source.replace( - " python scripts/ci/publish-planned-tag.py \\", - " exit 0\n python scripts/ci/publish-planned-tag.py \\", - 1, - ), - "shadowed gh command": source.replace( - " set -euo pipefail", - " set -euo pipefail\n gh() { printf 'shadowed\\n'; }", - 1, - ), - } - - for label, variant in variants.items(): - with self.subTest(label): - self.assertNotEqual(variant, source) - self.assert_rejected(variant) - - def test_rejects_skipped_nonblocking_or_decoy_scoped_steps(self) -> None: - source = CURRENT_RUST_RECOVERY_WORKFLOW - tag_step = " - name: Create or verify the exact planned source tag" - publication_step = " - name: Start or resume repository-owned publication" - completion_step = " - name: Verify crates.io source identity and the GitHub Release" - exact_bindings = """ RELEASE_TAG: ${{ needs.discover.outputs.version }} - RELEASE_COMMIT: ${{ needs.discover.outputs.commit }}""" - decoy_step = f""" - name: Unrelated release identity - env: -{exact_bindings} - run: echo "release identity is not consumed here" - -""" - mutable_tag_bindings = source.replace( - exact_bindings, - """ RELEASE_TAG: ${{ github.ref_name }} - RELEASE_COMMIT: ${{ github.sha }}""", - 1, - ).replace(tag_step, decoy_step + tag_step, 1) - publication_env = """ env: - GH_TOKEN: ${{ github.token }} - PLAN_TAG: ${{ needs.discover.outputs.plan_tag }} - RELEASE_TAG: ${{ needs.discover.outputs.version }} - RELEASE_COMMIT: ${{ needs.discover.outputs.commit }}""" - mutable_selector_bindings = source.replace( - publication_env, - """ env: - GH_TOKEN: ${{ github.token }} - PLAN_TAG: ${{ needs.discover.outputs.plan_tag }} - RELEASE_TAG: ${{ github.ref_name }} - RELEASE_COMMIT: ${{ github.sha }}""", - 1, - ).replace( - " - name: Start or resume repository-owned publication", - decoy_step + " - name: Start or resume repository-owned publication", - 1, - ) - variants = { - "tag publication skipped": source.replace( - tag_step, - tag_step + "\n if: ${{ false }}", - 1, - ), - "tag publication nonblocking even when false": source.replace( - tag_step, - tag_step + "\n continue-on-error: false", - 1, - ), - "tag publication expression-enabled nonblocking": source.replace( - tag_step, - tag_step + "\n continue-on-error: ${{ github.ref_name != '' }}", - 1, - ), - "tag publication uses a nonblocking shell": source.replace( - tag_step, - tag_step + "\n shell: bash {0} || true", - 1, - ), - "publication selection skipped": source.replace( - publication_step, - publication_step + "\n if: ${{ false }}", - 1, - ), - "completion verification skipped": source.replace( - completion_step, - completion_step + "\n if: ${{ false }}", - 1, - ), - "completion verification nonblocking": source.replace( - completion_step, - completion_step + "\n continue-on-error: true", - 1, - ), - "completion verification expression-enabled nonblocking": source.replace( - completion_step, - completion_step + "\n continue-on-error: ${{ failure() }}", - 1, - ), - "tag bindings moved to an unrelated step": mutable_tag_bindings, - "selector bindings moved to an unrelated step": mutable_selector_bindings, - "checkout adds repository-token authority": source.replace( - " fetch-depth: 0", - " fetch-depth: 0\n token: ${{ github.token }}", - 1, - ), - "run identity includes an unapproved field": source.replace( - "databaseId,event,displayTitle,headBranch,headSha,status,conclusion", - "databaseId,event,displayTitle,headBranch,headSha,status,conclusion,actor", - 1, - ), - } - - for label, variant in variants.items(): - with self.subTest(label): - self.assertNotEqual(variant, source) - self.assert_rejected(variant) - - def test_rejects_weakened_or_mismatched_rust_publication_shapes(self) -> None: - source = CURRENT_RUST_RECOVERY_WORKFLOW - publisher = r""" python scripts/ci/publish-planned-tag.py \ - --tag "$RELEASE_TAG" --commit "$RELEASE_COMMIT" --plan-tag "$PLAN_TAG" \ - --evidence release-tag-publication-evidence.json""" - deferred_publisher = source.replace(publisher, " echo tag-publication-deferred", 1).replace( - " - name: Verify crates.io source identity and the GitHub Release", - " - name: Deferred source tag publication\n" - " run: |\n" - f"{publisher}\n\n" - " - name: Verify crates.io source identity and the GitHub Release", - 1, - ) - repository_token_creation = source.replace( - " python scripts/ci/publish-planned-tag.py", - ' gh api --method POST "repos/$GITHUB_REPOSITORY/git/refs"\n' - " python scripts/ci/publish-planned-tag.py", - 1, - ) - misplaced_deploy_key = source.replace( - " ssh-key: ${{ secrets.RELEASE_PLAN_DEPLOY_KEY }}", - " env:\n UNUSED_DEPLOY_KEY: ${{ secrets.RELEASE_PLAN_DEPLOY_KEY }}", - 1, - ) - dormant_publisher = source.replace( - publisher, - " publish_planned_tag() {\n" - + "\n".join(f" {line}" for line in publisher.splitlines()) - + "\n }", - 1, - ) - reassigned_tag = source.replace( - " python scripts/ci/publish-planned-tag.py", - ' RELEASE_TAG="$GITHUB_REF_NAME"\n python scripts/ci/publish-planned-tag.py', - 1, - ) - nonblocking_verification = source.replace( - "--attempts 6 --sleep 10 --evidence release-completion-evidence.json", - "--attempts 6 --sleep 10 --evidence release-completion-evidence.json || true", - 1, - ) - variants = { - "missing protected environment": source.replace( - "environment: release-plan-publication", "environment: unprotected", 1 - ), - "missing deploy key": source.replace("secrets.RELEASE_PLAN_DEPLOY_KEY", "secrets.UNPROTECTED_KEY", 1), - "deploy key only in unrelated env": misplaced_deploy_key, - "tag publisher defined but not executed": dormant_publisher, - "release tag reassigned before publication": reassigned_tag, - "public verification made nonblocking": nonblocking_verification, - "tag publication after dispatch": deferred_publisher, - "mutable tag publisher argument": source.replace('--tag "$RELEASE_TAG"', '--tag "$GITHUB_REF_NAME"', 1), - "mismatched tag publisher commit": source.replace( - '--commit "$RELEASE_COMMIT"', '--commit "$GITHUB_SHA"', 1 - ), - "mutable planned tag binding": source.replace("needs.discover.outputs.version", "github.ref_name"), - "mutable planned commit binding": source.replace("needs.discover.outputs.commit", "github.sha"), - "different selected workflow": source.replace( - "gh run list --workflow release.yml", "gh run list --workflow nightly.yml", 1 - ), - "different dispatched workflow": source.replace( - "gh workflow run release.yml", "gh workflow run nightly.yml", 1 - ), - "incomplete run identity": source.replace("headBranch,headSha,status", "headBranch,status", 1), - "mismatched selector tag": source.replace( - '--release-tag "$RELEASE_TAG"', '--release-tag "$GITHUB_REF_NAME"', 1 - ), - "mismatched selector commit": source.replace( - '--release-commit "$RELEASE_COMMIT"', '--release-commit "$GITHUB_SHA"', 1 - ), - "mismatched dispatch tag": source.replace( - '-f release_tag="$RELEASE_TAG"', '-f release_tag="$GITHUB_REF_NAME"', 1 - ), - "missing completed release verification": source.replace( - "--component sdk-rust --plan recovery-input/release-plan.json", - "--component sdk-rust --plan mutable-release-plan.json", - 1, - ), - "broad contents permission": source.replace("contents: read", "contents: write", 1), - "repository token tag creation": repository_token_creation, - } - - for label, variant in variants.items(): - with self.subTest(label): - self.assertNotEqual(variant, source) - self.assert_rejected(variant) - - def test_other_components_keep_the_contents_api_contract(self) -> None: - expected_sha256 = hashlib.sha256(GENERIC_RECOVERY_WORKFLOW.encode("utf-8")).hexdigest() - self.recovery.verify_recovery_workflow_source("server", GENERIC_RECOVERY_WORKFLOW, expected_sha256) - - protected_only = GENERIC_RECOVERY_WORKFLOW.replace( - '-f ref="refs/tags/$RELEASE_TAG" -f sha="$RELEASE_COMMIT"', - 'python scripts/ci/publish-planned-tag.py --tag "$RELEASE_TAG" --commit "$RELEASE_COMMIT"', - ) - with self.assertRaises(self.recovery.RecoveryError): - self.recovery.verify_recovery_workflow_source("server", protected_only, expected_sha256) - - def test_python_recovery_dispatches_publication_from_protected_main(self) -> None: - recovery_source = RECOVERY_WORKFLOW.read_text() - publish_source = PUBLISH_WORKFLOW.read_text() - expected_sha256 = hashlib.sha256(recovery_source.encode("utf-8")).hexdigest() - discover_job = recovery_source[recovery_source.index(" discover:") : recovery_source.index(" publish:")] - publication_job = recovery_source[recovery_source.index(" publish:") :] - - self.recovery.verify_recovery_workflow_source("sdk-python", recovery_source, expected_sha256) - self.assertIn("contents: read", discover_job) - self.assertNotIn("actions: write", discover_job) - self.assertNotIn("contents: write", discover_job) - self.assertNotIn("GH_TOKEN:", discover_job) - self.assertNotIn("gh workflow run", discover_job) - self.assertIn("needs: discover", publication_job) - self.assertIn("needs.discover.outputs.action == 'publish'", publication_job) - self.assertIn("actions: write", publication_job) - self.assertIn("contents: write", publication_job) - self.assertEqual(1, recovery_source.count("actions: write")) - self.assertEqual(1, recovery_source.count("contents: write")) - self.assertIn("gh workflow run publish.yml --ref main", publication_job) - self.assertNotIn('gh workflow run publish.yml --ref "$RELEASE_TAG"', publication_job) - self.assertIn('-f release_tag="$RELEASE_TAG"', publication_job) - self.assertIn('-f release_commit="$RELEASE_COMMIT"', publication_job) - self.assertIn('--release-plan "$PLAN_TAG"', publication_job) - self.assertIn("&& 'Publish' || 'Build'", publish_source) - self.assertIn("inputs.release_commit || github.sha", publish_source) - self.assertIn("github.ref == 'refs/heads/main' && inputs.publish", publish_source) - self.assertIn("format('refs/tags/{0}', inputs.release_tag)", publish_source) - self.assertIn('if [ "$REQUESTED_TAG" != "$package_version" ]', publish_source) - self.assertIn('tag_commit="$(git rev-list -n 1 "$REQUESTED_TAG")"', publish_source) - self.assertIn('if [ "$tag_commit" != "$REQUESTED_COMMIT" ]', publish_source) - - invalid_recovery_sources = ( - recovery_source.replace("--ref main", '--ref "$RELEASE_TAG"', 1), - recovery_source.replace('-f release_tag="$RELEASE_TAG"', '-f release_tag="$GITHUB_REF_NAME"', 1), - recovery_source.replace('--release-plan "$PLAN_TAG"', '--release-plan "$RELEASE_TAG"', 1), - recovery_source.replace('-f release_commit="$RELEASE_COMMIT"', '-f release_commit="$GITHUB_SHA"', 1), - recovery_source.replace("-f publish=true", "-f publish=false", 1), - ) - for invalid_source in invalid_recovery_sources: - with self.assertRaises(self.recovery.RecoveryError): - self.recovery.verify_recovery_workflow_source("sdk-python", invalid_source, expected_sha256) - - -class PublicationRunSelectionTest(unittest.TestCase): - RELEASE_TAG = "1.2.3" - RELEASE_COMMIT = "1" * 40 - PLAN_TAG = "release-plan/continuity-plan-b" - - @classmethod - def setUpClass(cls) -> None: - cls.recovery = load_recovery_module() - - def run_metadata( - self, - run_id: int, - *, - status: str, - conclusion: str | None, - head_sha: str | None = None, - plan: str | None = None, - release_tag: str | None = None, - release_commit: str | None = None, - publish: bool = True, - legacy_title: bool = False, - ) -> dict[str, object]: - exact_tag = release_tag or self.RELEASE_TAG - exact_commit = release_commit or self.RELEASE_COMMIT - exact_plan = plan or self.PLAN_TAG - if legacy_title: - display_title = f"Release {exact_tag} for {exact_plan}" - else: - action = "Publish" if publish else "Build" - display_title = f"{action} {exact_tag}@{exact_commit} from {exact_plan}" - return { - "databaseId": run_id, - "displayTitle": display_title, - "headBranch": "main", - "headSha": head_sha or "2" * 40, - "status": status, - "conclusion": conclusion, - } - - def select(self, runs: list[dict[str, object]]) -> dict[str, object]: - return self.recovery.select_publication_run( - self.RELEASE_TAG, - self.RELEASE_COMMIT, - self.PLAN_TAG, - runs, - ) - - def test_failed_plan_a_dispatches_current_plan_b_once(self) -> None: - selection = self.select( - [ - self.run_metadata( - 1, - status="completed", - conclusion="failure", - plan="release-plan/continuity-plan-a", - ) - ] - ) - - self.assertEqual( - selection, - {"action": "dispatch", "run_id": None, "status": None, "conclusion": None}, - ) - workflow = RECOVERY_WORKFLOW.read_text() - self.assertEqual(workflow.count("gh workflow run publish.yml"), 1) - self.assertIn("gh workflow run publish.yml --ref main", workflow) - self.assertIn( - "gh run list --workflow publish.yml --event workflow_dispatch --branch main", - workflow, - ) - self.assertIn('-f release_tag="$RELEASE_TAG" -f release_commit="$RELEASE_COMMIT"', workflow) - self.assertIn('-f release_plan="$PLAN_TAG" -f publish=true', workflow) - self.assertNotIn("gh run rerun", workflow) - - def test_active_exact_run_waits_and_successful_exact_run_completes(self) -> None: - active = self.run_metadata(2, status="in_progress", conclusion=None) - failed = self.run_metadata(1, status="completed", conclusion="failure") - self.assertEqual(self.select([failed, active])["action"], "wait") - - successful = self.run_metadata(3, status="completed", conclusion="success") - self.assertEqual(self.select([failed, successful])["action"], "complete") - - def test_successful_publish_false_run_does_not_complete_publication(self) -> None: - build_only = self.run_metadata(4, status="completed", conclusion="success", publish=False) - legacy_build_only = self.run_metadata( - 3, - status="completed", - conclusion="success", - publish=False, - legacy_title=True, - ) - - self.assertEqual(self.select([legacy_build_only, build_only])["action"], "dispatch") - - def test_non_main_or_different_release_identity_runs_are_ignored(self) -> None: - tag_context = self.run_metadata(1, status="completed", conclusion="success") - tag_context["headBranch"] = self.RELEASE_TAG - different_tag = self.run_metadata( - 2, - status="completed", - conclusion="success", - release_tag="9.9.9", - ) - different_commit = self.run_metadata( - 3, - status="completed", - conclusion="success", - release_commit="9" * 40, - ) - different_plan = self.run_metadata( - 4, - status="completed", - conclusion="success", - plan="release-plan/continuity-plan-a", - ) - - self.assertEqual( - self.select([tag_context, different_tag, different_commit, different_plan])["action"], - "dispatch", - ) - - def test_publication_selection_rejects_boolean_run_identity(self) -> None: - malformed = self.run_metadata(True, status="completed", conclusion="success") - - with self.assertRaisesRegex( - self.recovery.RecoveryError, - "publication run metadata is incomplete", - ): - self.select([malformed]) - - def test_publication_selection_rejects_non_string_commit_identity(self) -> None: - with self.assertRaisesRegex( - self.recovery.RecoveryError, - "publication run selection requires an exact release identity", - ): - self.recovery.select_publication_run( - self.RELEASE_TAG, - int(self.RELEASE_COMMIT), - self.PLAN_TAG, - [], - ) - - def test_fresh_dispatch_keeps_partial_publication_idempotent(self) -> None: - workflow = PUBLISH_WORKFLOW.read_text() - - self.assertIn("skip-existing: true", workflow) - self.assertIn('if ! gh release view "$RELEASE_TAG"', workflow) - - -class PublishedReleaseRecoveryTest(unittest.TestCase): - RELEASE_TAG = "0.4.101" - RELEASE_COMMIT = "8aa0e86fe51edc1e7aba3d97ddf3dfda8009ee23" - PLAN_TAG = "release-plan/alpha-continuity" - - @classmethod - def setUpClass(cls) -> None: - cls.recovery = load_recovery_module() - - def test_publication_authority_uses_exact_tag_distribution_and_release(self) -> None: - client = mock.Mock(spec=self.recovery.PublicClient) - identity = {"version": self.RELEASE_TAG, "commit": self.RELEASE_COMMIT} - distribution_evidence = {"kind": "pypi", "source_files_compared": 12} - release_evidence = {"tag_name": self.RELEASE_TAG} - distribution_verifier = mock.Mock(return_value=distribution_evidence) - - with ( - mock.patch.object(self.recovery, "require_source_tag") as source_tag_verifier, - mock.patch.object( - self.recovery, - "verify_github_release", - return_value=release_evidence, - ) as release_verifier, - mock.patch.dict(self.recovery.VERIFIERS, {"pypi": distribution_verifier}), - ): - evidence = self.recovery.verify_component(client, "sdk-python", identity) - - source_tag_verifier.assert_called_once_with(client, "sdk-python", identity) - distribution_verifier.assert_called_once_with( - client, - self.recovery.COMPONENTS["sdk-python"], - self.RELEASE_TAG, - self.RELEASE_COMMIT, - ) - release_verifier.assert_called_once_with(client, "sdk-python", self.RELEASE_TAG) - self.assertEqual(evidence["version"], self.RELEASE_TAG) - self.assertEqual(evidence["commit"], self.RELEASE_COMMIT) - self.assertEqual(evidence["distribution"], distribution_evidence) - self.assertEqual(evidence["github_release"], release_evidence) - - def test_exact_public_release_completes_without_publication_dispatch(self) -> None: - client = mock.Mock(spec=self.recovery.PublicClient) - plan = { - "plan": "alpha-continuity", - "channel": "alpha", - "components": { - "server": {"version": "0.2.700", "commit": "2" * 40}, - "sdk-python": {"version": self.RELEASE_TAG, "commit": self.RELEASE_COMMIT}, - }, - } - package_evidence = { - "version": self.RELEASE_TAG, - "commit": self.RELEASE_COMMIT, - "distribution": {"kind": "pypi", "source_files_compared": 12}, - "github_release": {"tag_name": self.RELEASE_TAG}, - } - preparation = { - "components": { - "sdk-python": { - "release_notes": { - "release_date": "2026-07-19", - "sha256": "a" * 64, - "source": {"kind": "changelog-unreleased"}, - } - } - } - } - - def verify_public_component(_client: object, component: str, _identity: dict[str, str]) -> dict[str, object]: - if component == "sdk-python": - return package_evidence - return {"version": plan["components"][component]["version"]} - - with ( - mock.patch.object(self.recovery, "verify_plan_authority", return_value=({}, {})), - mock.patch.object(self.recovery, "validate_release_preparation"), - mock.patch.object(self.recovery, "verify_component", side_effect=verify_public_component), - mock.patch.object(self.recovery, "resolve_tag", return_value=self.RELEASE_COMMIT), - mock.patch.object( - self.recovery, - "require_python_source_manifest_version", - return_value={"declared_version": self.RELEASE_TAG, "source_commit": self.RELEASE_COMMIT}, - ), - ): - state, outputs = self.recovery.resolve_component( - client, - "sdk-python", - self.PLAN_TAG, - "3" * 40, - plan, - preparation, - ) - - self.assertEqual(outputs["action"], "skip") - self.assertEqual(state["phase"], "complete") - self.assertEqual(state["outcome"], "verified") - self.assertEqual(state["public_evidence"], package_evidence) - - workflow = RECOVERY_WORKFLOW.read_text() - publication_job = workflow[workflow.index(" publish:") :] - self.assertIn("needs.discover.outputs.action == 'publish'", publication_job) - - -class PythonSourceManifestPreflightTest(unittest.TestCase): - RELEASE_TAG = "0.4.100" - RELEASE_COMMIT = "2018400368cf4251c58b24b3d53a99f0ca3512e3" - PLAN_TAG = "release-plan/continuity-plan-b" - - @classmethod - def setUpClass(cls) -> None: - cls.recovery = load_recovery_module() - - def test_manifest_version_mismatch_hands_off_occupied_tag_before_publication(self) -> None: - client = mock.Mock(spec=self.recovery.PublicClient) - client.bytes.return_value = b'[project]\nname = "durable-workflow"\nversion = "0.4.99"\n' - plan = { - "plan": "continuity-plan-b", - "channel": "alpha", - "components": { - "server": {"version": "0.2.666", "commit": "2" * 40}, - "sdk-python": {"version": self.RELEASE_TAG, "commit": self.RELEASE_COMMIT}, - }, - } - publication_verifier = mock.Mock() - preparation = { - "components": { - "sdk-python": { - "release_notes": { - "release_date": "2026-07-19", - "sha256": "a" * 64, - "source": {"kind": "changelog-unreleased"}, - } - } - } - } - - with ( - mock.patch.object(self.recovery, "verify_plan_authority", return_value=({}, {})), - mock.patch.object(self.recovery, "validate_release_preparation"), - mock.patch.object(self.recovery, "verify_component", return_value={}), - mock.patch.object(self.recovery, "resolve_tag", return_value=self.RELEASE_COMMIT), - mock.patch.dict(self.recovery.VERIFIERS, {"pypi": publication_verifier}), - self.assertRaises(self.recovery.RecoveryError) as caught, - ): - self.recovery.resolve_component( - client, - "sdk-python", - self.PLAN_TAG, - "3" * 40, - plan, - preparation, - ) - - error = caught.exception - self.assertEqual(error.phase, "source-manifest-preflight") - self.assertEqual(error.evidence["classification"], "source-manifest-version-conflict") - self.assertEqual(error.evidence["source_manifest"]["declared_version"], "0.4.99") - self.assertEqual( - error.evidence["source_tag"], - {"tag": self.RELEASE_TAG, "status": "present", "commit": self.RELEASE_COMMIT}, - ) - client.bytes.assert_called_once_with( - f"https://api.github.com/repos/durable-workflow/sdk-python/contents/pyproject.toml" - f"?ref={self.RELEASE_COMMIT}", - accept="application/vnd.github.raw+json", - ) - publication_verifier.assert_not_called() - - failure = self.recovery.resolution_failure_state( - "sdk-python", - self.PLAN_TAG, - "3" * 40, - plan, - error, - ) - self.assertEqual(failure["durable_evidence"]["failure"], error.evidence) - self.assertIn("control-plane", failure["resume_action"]) - self.assertIn("successor allocation", failure["resume_action"]) - self.assertIn("immutable durable-workflow/sdk-python@0.4.100", failure["resume_action"]) - self.assertNotIn("Run durable-workflow/sdk-python Actions workflow", failure["resume_action"]) - - -class ReleaseCandidateChannelTest(unittest.TestCase): - def setUp(self) -> None: - self.recovery = load_recovery_module() - - def test_rc_plan_retains_coherent_beta_qualification(self) -> None: - candidate = lifecycle_plan(self.recovery, "rc") - for identity in candidate["components"].values(): - identity["version"] = "2.0.0-rc.5" - self.recovery.validate_plan(candidate) - beta = lifecycle_plan(self.recovery, "beta") - record = { - "schema": "durable-workflow.beta-authorization/v1", - "channel": "beta", - "candidate": beta["plan"], - "components": beta["components"], - } - for identity in record["components"].values(): - identity["version"] = "2.0.0-beta.21" - self.assertTrue( - self.recovery.beta_authorization_matches_plan( - candidate, - candidate["beta_authorization"], - record, - ) - ) - - -if __name__ == "__main__": - unittest.main()