diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml index 433d880..155da5c 100644 --- a/.gitea/workflows/test.yml +++ b/.gitea/workflows/test.yml @@ -42,6 +42,15 @@ on: jobs: stage-firecracker-inputs: runs-on: [self-hosted, kvm] + # Same guard as the other KVM-runner jobs: don't spin the privileged + # runner for fork PRs (this only copies a non-secret static binary, but + # keep the posture consistent — build-infra/integration/coverage all + # depend on it, so gating here gates the whole Firecracker chain). + if: >- + github.event_name == 'push' || + github.event_name == 'workflow_dispatch' || + (github.event_name == 'pull_request' && + github.event.pull_request.head.repo.full_name == github.repository) steps: - name: Stage the provisioned static dropbear run: | @@ -234,7 +243,7 @@ jobs: python3 scripts/diff_coverage.py --base origin/main --min 90 publish-infra: - needs: [build-infra, unit, integration-docker, integration-firecracker, coverage] + needs: [stage-firecracker-inputs, build-infra, unit, integration-docker, integration-firecracker, coverage] runs-on: ubuntu-latest if: github.event_name == 'push' && github.ref == 'refs/heads/main' steps: @@ -247,7 +256,18 @@ jobs: name: infra-candidate path: infra-candidate + # publish_infra re-derives the version from the checkout to confirm the + # bundle matches before uploading, and the version hashes the dropbear + # bytes. Stage the SAME dropbear build-infra used, or the recheck + # computes a ""-dropbear version and rejects the candidate. + - name: Download the staged dropbear (matches build-infra's version) + uses: actions/download-artifact@v3 + with: + name: firecracker-inputs + path: firecracker-inputs + - name: Publish the tested candidate env: BOT_BOTTLE_INFRA_ARTIFACT_TOKEN: ${{ secrets.BOT_BOTTLE_INFRA_ARTIFACT_TOKEN }} + BOT_BOTTLE_FC_DROPBEAR: ${{ github.workspace }}/firecracker-inputs/dropbear run: python3 -m bot_bottle.backend.firecracker.publish_infra --publish-dir infra-candidate diff --git a/bot_bottle/backend/firecracker/infra_artifact.py b/bot_bottle/backend/firecracker/infra_artifact.py index b4f5cf6..26d02da 100644 --- a/bot_bottle/backend/firecracker/infra_artifact.py +++ b/bot_bottle/backend/firecracker/infra_artifact.py @@ -169,7 +169,12 @@ def ensure_artifact_gz(version: str) -> Path: candidate_dir = os.environ.get(_CANDIDATE_DIR_ENV, "").strip() if candidate_dir: root = Path(candidate_dir) - declared = (root / "version.txt").read_text(encoding="utf-8").strip() + version_file = root / "version.txt" + # Guard the read so a missing version.txt is a clean error, not a raw + # FileNotFoundError. + if not version_file.is_file(): + die(f"infra candidate bundle is incomplete: {root}") + declared = version_file.read_text(encoding="utf-8").strip() if declared != version: die( f"infra candidate version mismatch: expected {version}, " diff --git a/bot_bottle/backend/firecracker/publish_infra.py b/bot_bottle/backend/firecracker/publish_infra.py index 631ec81..c4c0d1f 100644 --- a/bot_bottle/backend/firecracker/publish_infra.py +++ b/bot_bottle/backend/firecracker/publish_infra.py @@ -132,7 +132,12 @@ def build_artifact(out_dir: Path) -> tuple[str, Path, Path]: def _publish_bundle(root: Path, token: str) -> str: - version = (root / "version.txt").read_text(encoding="utf-8").strip() + version_file = root / "version.txt" + # Guard the read so a missing version.txt is a clean error, not a raw + # FileNotFoundError. + if not version_file.is_file(): + raise SystemExit(f"incomplete artifact bundle: {root}") + version = version_file.read_text(encoding="utf-8").strip() expected = infra_artifact.infra_artifact_version(infra_vm._infra_init()) if version != expected: raise SystemExit( diff --git a/tests/unit/test_infra_artifact.py b/tests/unit/test_infra_artifact.py index 51aac5f..4fccf80 100644 --- a/tests/unit/test_infra_artifact.py +++ b/tests/unit/test_infra_artifact.py @@ -50,7 +50,12 @@ class _CacheMixin(unittest.TestCase): self._env = mock.patch.dict( os.environ, {"BOT_BOTTLE_FC_CACHE": self._tmp.name, - "BOT_BOTTLE_INFRA_ARTIFACT_TOKEN": ""}, + "BOT_BOTTLE_INFRA_ARTIFACT_TOKEN": "", + # Pin the candidate-dir override off: the coverage CI job exports a + # candidate dir for the integration suite, and an ambient value + # would send these registry-pull tests down the local-bundle path. + # Cases that exercise the candidate path set it explicitly. + "BOT_BOTTLE_INFRA_ARTIFACT_DIR": ""}, clear=False, ) self._env.start()