From 0c91c75a057f6aeb9e4cc5f3dcfc670bc3c6507b Mon Sep 17 00:00:00 2001 From: claude Date: Tue, 21 Jul 2026 05:27:24 +0000 Subject: [PATCH] fix(ci): use COVERAGE_FILE env var for reliable artifact paths; add --reuse-published to infra build MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit coverage run's --data-file flag can be overridden or ignored in some runner environments (Nix Python, older act-based runners). Switching to the COVERAGE_FILE env var with an absolute ${{ github.workspace }} path ensures coverage.py writes to a known location in every runner context, so upload-artifact can find the file. Also adds --reuse-published to the infra build step: if the artifact for this content hash already exists in the registry, download it instead of running the full docker build → mke2fs → gzip pipeline. --- .gitea/workflows/test.yml | 18 +++++++--- .../backend/firecracker/publish_infra.py | 33 +++++++++++++++++++ 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml index 5da227c..40da087 100644 --- a/.gitea/workflows/test.yml +++ b/.gitea/workflows/test.yml @@ -57,10 +57,14 @@ jobs: run: python3 -m pip install --break-system-packages -r requirements-dev.txt - name: Run unit tests with coverage - run: python3 -m coverage run --data-file=.coverage.unit -m unittest discover -t . -s tests/unit -v + env: + COVERAGE_FILE: ${{ github.workspace }}/.coverage.unit + run: python3 -m coverage run -m unittest discover -t . -s tests/unit -v - name: Report unit coverage - run: python3 -m coverage report --data-file=.coverage.unit -m + env: + COVERAGE_FILE: ${{ github.workspace }}/.coverage.unit + run: python3 -m coverage report -m - name: Upload unit coverage artifact uses: actions/upload-artifact@v3 @@ -91,7 +95,8 @@ jobs: - name: Run integration tests (docker) with coverage env: BOT_BOTTLE_BACKEND: docker - run: python3 -m coverage run --data-file=.coverage.docker -m unittest discover -t . -s tests/integration -v + COVERAGE_FILE: ${{ github.workspace }}/.coverage.docker + run: python3 -m coverage run -m unittest discover -t . -s tests/integration -v - name: Upload docker coverage artifact uses: actions/upload-artifact@v3 @@ -139,7 +144,7 @@ jobs: - name: Build infra candidate from this checkout env: BOT_BOTTLE_FC_DROPBEAR: /var/cache/bot-bottle-fc/dropbear - run: python3 -m bot_bottle.backend.firecracker.publish_infra --output infra-candidate + run: python3 -m bot_bottle.backend.firecracker.publish_infra --output infra-candidate --reuse-published - name: Replace the persistent infra VM with the candidate run: python3 -c 'from bot_bottle.backend.firecracker import infra_vm; infra_vm.stop()' @@ -151,7 +156,8 @@ jobs: env: BOT_BOTTLE_BACKEND: firecracker BOT_BOTTLE_INFRA_ARTIFACT_DIR: ${{ github.workspace }}/infra-candidate - run: python3 -m coverage run --data-file=.coverage.firecracker -m unittest discover -t . -s tests/integration -v + COVERAGE_FILE: ${{ github.workspace }}/.coverage.firecracker + run: python3 -m coverage run -m unittest discover -t . -s tests/integration -v - name: Upload firecracker coverage artifact uses: actions/upload-artifact@v3 @@ -181,6 +187,8 @@ jobs: # # Runs on ubuntu-latest — no KVM needed, no test reruns. Coverage files use # relative_files = True (.coveragerc) so they combine cleanly across runners. + # Each test job sets COVERAGE_FILE to an absolute path so coverage.py writes + # to a known location that upload-artifact can find regardless of runner env. # # Restricted to the same events as integration-firecracker: it depends on # that job's coverage artifact and skips for fork PRs alongside it. diff --git a/bot_bottle/backend/firecracker/publish_infra.py b/bot_bottle/backend/firecracker/publish_infra.py index c4c0d1f..f295e86 100644 --- a/bot_bottle/backend/firecracker/publish_infra.py +++ b/bot_bottle/backend/firecracker/publish_infra.py @@ -131,6 +131,29 @@ def build_artifact(out_dir: Path) -> tuple[str, Path, Path]: return version, gz, sha +def _try_download_published(out_dir: Path) -> tuple[str, Path, Path] | None: + """If this version's artifact is already in the registry, download the gz + and sha to out_dir and return (version, gz_path, sha_path). Returns None + when not yet published.""" + version = infra_artifact.infra_artifact_version(infra_vm._infra_init()) + sha_url = infra_artifact.artifact_url(version, "rootfs.ext4.gz.sha256") + try: + with urllib.request.urlopen(infra_artifact._open(sha_url)): + pass + except urllib.error.HTTPError as e: + if e.code == 404: + return None + raise SystemExit(f"registry check failed (HTTP {e.code}): {sha_url}") + except urllib.error.URLError as e: + raise SystemExit(f"registry unreachable: {sha_url} ({e.reason})") + print(f"infra rootfs {version} already published — downloading instead of building") + gz = out_dir / "rootfs.ext4.gz" + sha = out_dir / "rootfs.ext4.gz.sha256" + infra_artifact._download(infra_artifact.artifact_url(version, "rootfs.ext4.gz"), gz) + infra_artifact._download(infra_artifact.artifact_url(version, "rootfs.ext4.gz.sha256"), sha) + return version, gz, sha + + def _publish_bundle(root: Path, token: str) -> str: version_file = root / "version.txt" # Guard the read so a missing version.txt is a clean error, not a raw @@ -187,6 +210,8 @@ def main(argv: list[str] | None = None) -> int: help="build a candidate bundle in DIR without publishing") mode.add_argument("--publish-dir", type=Path, help="publish an already-built and tested candidate bundle") + parser.add_argument("--reuse-published", action="store_true", + help="with --output: download from registry if already published instead of building") args = parser.parse_args(argv) _, _, token = infra_artifact._config() @@ -197,6 +222,14 @@ def main(argv: list[str] | None = None) -> int: if args.output is not None: args.output.mkdir(parents=True, exist_ok=True) + reused = None + if args.reuse_published: + reused = _try_download_published(args.output) + if reused is not None: + version, _, _ = reused + (args.output / "version.txt").write_text(version + "\n", encoding="utf-8") + print(f"reused published infra rootfs candidate {version}") + return 0 version, _gz, _sha = build_artifact(args.output) (args.output / "version.txt").write_text(version + "\n", encoding="utf-8") print(f"built infra rootfs candidate {version}")