From 2bd90a2be31c608849df5b21899b4171fee7ee67 Mon Sep 17 00:00:00 2001 From: JOY <5027251+JOY@users.noreply.github.com> Date: Wed, 5 Aug 2026 10:20:48 +0700 Subject: [PATCH] ci: tolerate registry cache export failures --- .../scripts/tests/test_publish_workflow.py | 50 ++++++++++ .github/workflows/dependency-build.yml | 1 + ...publish-regular-docker-image-on-demand.yml | 2 +- ...6-08-05-publish-cache-export-resilience.md | 95 +++++++++++++++++++ 4 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 .github/scripts/tests/test_publish_workflow.py create mode 100644 docs/superpowers/plans/2026-08-05-publish-cache-export-resilience.md diff --git a/.github/scripts/tests/test_publish_workflow.py b/.github/scripts/tests/test_publish_workflow.py new file mode 100644 index 000000000000..2010c54c1dca --- /dev/null +++ b/.github/scripts/tests/test_publish_workflow.py @@ -0,0 +1,50 @@ +import unittest +from pathlib import Path + + +REPOSITORY_ROOT = Path(__file__).resolve().parents[3] +PUBLISH_WORKFLOW = REPOSITORY_ROOT / ".github/workflows/publish-regular-docker-image-on-demand.yml" +DEPENDENCY_WORKFLOW = REPOSITORY_ROOT / ".github/workflows/dependency-build.yml" +BUILD_STEP_NAME = "Build and push Docker image (indexer + API)" + + +def build_step_inputs(workflow: str) -> dict[str, str]: + step_start = workflow.index(f"- name: {BUILD_STEP_NAME}") + step = workflow[step_start:].split("\n - name:", 1)[0] + with_mapping = step.split("\n with:\n", 1)[1] + + return { + key.strip(): value.strip() + for line in with_mapping.splitlines() + if line.startswith(" ") and ": " in line + for key, value in [line.strip().split(": ", 1)] + } + + +def parse_cache_options(cache_to: str) -> dict[str, str]: + return dict(option.split("=", 1) for option in cache_to.split(",")) + + +class PublishWorkflowTests(unittest.TestCase): + def test_registry_cache_export_does_not_fail_the_image_push(self) -> None: + build_inputs = build_step_inputs(PUBLISH_WORKFLOW.read_text(encoding="utf-8")) + cache_options = parse_cache_options(build_inputs["cache-to"]) + + self.assertEqual(build_inputs["push"], "true") + self.assertEqual(cache_options["type"], "registry") + self.assertEqual(cache_options["ref"], "ghcr.io/dos/doscan:buildcache") + self.assertEqual(cache_options["mode"], "max") + self.assertIn("ignore-error", cache_options) + self.assertEqual(cache_options["ignore-error"], "true") + + def test_dependency_build_runs_for_the_publish_workflow(self) -> None: + dependency_workflow = DEPENDENCY_WORKFLOW.read_text(encoding="utf-8") + + self.assertIn( + ' - ".github/workflows/publish-regular-docker-image-on-demand.yml"', + dependency_workflow.splitlines(), + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/.github/workflows/dependency-build.yml b/.github/workflows/dependency-build.yml index 44c742df99fd..6890bcfc9a82 100644 --- a/.github/workflows/dependency-build.yml +++ b/.github/workflows/dependency-build.yml @@ -27,6 +27,7 @@ on: - "docs/reports/doscan-frontend-env-audit.vi.html" - ".github/workflows/deploy-config.yml" - ".github/workflows/dependency-build.yml" + - ".github/workflows/publish-regular-docker-image-on-demand.yml" - ".github/workflows/sync-upstream.yml" - ".github/scripts/check-upstream-sync.py" - ".github/scripts/validate-docs-production-status.py" diff --git a/.github/workflows/publish-regular-docker-image-on-demand.yml b/.github/workflows/publish-regular-docker-image-on-demand.yml index 6f02868b001f..8ee3bb220a64 100644 --- a/.github/workflows/publish-regular-docker-image-on-demand.yml +++ b/.github/workflows/publish-regular-docker-image-on-demand.yml @@ -65,7 +65,7 @@ jobs: file: ./docker/Dockerfile push: true cache-from: type=registry,ref=ghcr.io/dos/doscan:buildcache - cache-to: type=registry,ref=ghcr.io/dos/doscan:buildcache,mode=max + cache-to: type=registry,ref=ghcr.io/dos/doscan:buildcache,mode=max,ignore-error=true tags: ghcr.io/dos/doscan:${{ steps.version.outputs.release_version }}.commit.${{ env.SHORT_SHA }} labels: ${{ steps.setup.outputs.docker-labels }} platforms: linux/amd64 diff --git a/docs/superpowers/plans/2026-08-05-publish-cache-export-resilience.md b/docs/superpowers/plans/2026-08-05-publish-cache-export-resilience.md new file mode 100644 index 000000000000..60ebe56c9c82 --- /dev/null +++ b/docs/superpowers/plans/2026-08-05-publish-cache-export-resilience.md @@ -0,0 +1,95 @@ +# Publish Cache Export Resilience Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Keep a successful production image publish successful when the optional GHCR registry cache export fails. + +**Architecture:** Preserve the existing combined BuildKit image push and cache export step. Configure only the registry cache exporter to ignore export errors, while keeping image build and registry push failures fatal. Add a repository test that parses the real workflow step and a trigger-path test that keeps the guard active when the publish workflow changes. + +**Tech Stack:** GitHub Actions YAML, Docker Buildx, Python `unittest`, `actionlint` + +## Global Constraints + +- Keep `push: true`; image build and image push failures must remain fatal. +- Add `ignore-error=true` only to the `cache-to` registry exporter for the combined indexer and API image. +- Preserve the existing cache reference `ghcr.io/dos/doscan:buildcache` and `mode=max`. +- Ensure Dependency build runs when `.github/workflows/publish-regular-docker-image-on-demand.yml` changes. +- Use English for repository artifacts and avoid Unicode dash characters. + +--- + +### Task 1: Make registry cache export best effort + +**Files:** +- Create: `.github/scripts/tests/test_publish_workflow.py` +- Modify: `.github/workflows/publish-regular-docker-image-on-demand.yml:60-69` +- Modify: `.github/workflows/dependency-build.yml:28-33` + +**Interfaces:** +- Consumes: The existing `docker/build-push-action` step named `Build and push Docker image (indexer + API)`. +- Produces: A best-effort registry cache export that cannot mask a successful image push, plus CI coverage for the policy and trigger path. + +- [ ] **Step 1: Write the failing tests** + +Create a `unittest` module that reads the real workflows, isolates the named build step, parses its `with` mapping, and verifies these observable configuration contracts: + +```python +self.assertEqual(build_inputs["push"], "true") +self.assertEqual(cache_options["type"], "registry") +self.assertEqual(cache_options["ref"], "ghcr.io/dos/doscan:buildcache") +self.assertEqual(cache_options["mode"], "max") +self.assertEqual(cache_options["ignore-error"], "true") +``` + +Add a separate test that verifies Dependency build includes this exact push path: + +```python +self.assertIn( + ' - ".github/workflows/publish-regular-docker-image-on-demand.yml"', + dependency_workflow.splitlines(), +) +``` + +- [ ] **Step 2: Run the focused test and verify RED** + +Run: + +```powershell +python -m unittest .github/scripts/tests/test_publish_workflow.py -v +``` + +Expected: two assertion failures, one for missing `ignore-error` and one for the missing Dependency build push path. + +- [ ] **Step 3: Implement the minimal workflow changes** + +Change the cache exporter to: + +```yaml +cache-to: type=registry,ref=ghcr.io/dos/doscan:buildcache,mode=max,ignore-error=true +``` + +Add this path to Dependency build push paths: + +```yaml +- ".github/workflows/publish-regular-docker-image-on-demand.yml" +``` + +- [ ] **Step 4: Verify GREEN and workflow syntax** + +Run: + +```powershell +python -m unittest .github/scripts/tests/test_publish_workflow.py -v +python -m unittest discover -s .github/scripts/tests -p 'test_*.py' -v +actionlint .github/workflows/publish-regular-docker-image-on-demand.yml .github/workflows/dependency-build.yml +git diff --check +``` + +Expected: focused tests pass, full suite passes with 0 failures, `actionlint` exits 0, and `git diff --check` exits 0. + +- [ ] **Step 5: Commit** + +```powershell +git add .github/scripts/tests/test_publish_workflow.py .github/workflows/publish-regular-docker-image-on-demand.yml .github/workflows/dependency-build.yml docs/superpowers/plans/2026-08-05-publish-cache-export-resilience.md +git commit -m "ci: tolerate registry cache export failures" +```