From 2e1e854fa8a27057b769401278f563062b6bf226 Mon Sep 17 00:00:00 2001 From: Tianyu Zhou Date: Sat, 12 Sep 2026 10:06:00 +0800 Subject: [PATCH] ci: separate Docker Hub publishing from standalone E2E Move Docker Hub login and publication into a dependent job so the CI view separates E2E results from publishing. Transfer the tested image through a compressed Actions artifact and verify its image ID before pushing, without rebuilding or publishing intermediate registry tags. Retain the publication lock and main-head checks to prevent older runs from replacing a newer latest image. Keep artifacts for one day and skip both image transfer and publication for PRs and forks. Signed-off-by: Tianyu Zhou --- .github/workflows/ci.yml | 85 +++++++++++++++++++++++++++++++++++----- AGENTS.md | 20 +++++++--- 2 files changed, 90 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9d10c6c..f08f6c5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -120,12 +120,9 @@ jobs: - deployment-script-syntax runs-on: ubuntu-22.04 timeout-minutes: 60 - # Keep testing and publishing the same local image. Serialize main jobs - # through the latest push, including reruns, without replacing queued jobs. - concurrency: - group: ci-standalone-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: false - queue: max + outputs: + image-id: ${{ steps.export-image.outputs.image-id }} + artifact-id: ${{ steps.upload-image.outputs.artifact-id }} steps: - name: Check out repository @@ -290,19 +287,87 @@ jobs: docker rm -f akernel-traefik akernel-node >/dev/null 2>&1 || true fi - - name: Log in to Docker Hub + - name: Export tested all-in-one image + id: export-image + if: >- + github.repository == 'inclusionAI/AKernel' && + github.event_name == 'push' && github.ref == 'refs/heads/main' + shell: bash + run: | + source_image="akernel-ci/all-in-one:${GITHUB_SHA}" + image_id="$(docker image inspect "${source_image}" --format '{{.Id}}')" + echo "image-id=${image_id}" >> "${GITHUB_OUTPUT}" + docker save "${source_image}" | gzip -1 > "${RUNNER_TEMP}/all-in-one.tar.gz" + + - name: Upload tested image for publication + id: upload-image if: >- github.repository == 'inclusionAI/AKernel' && github.event_name == 'push' && github.ref == 'refs/heads/main' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: tested-all-in-one-${{ github.run_attempt }} + path: ${{ runner.temp }}/all-in-one.tar.gz + if-no-files-found: error + compression-level: 0 + retention-days: 1 + + publish-dockerhub: + name: Publish to Docker Hub + needs: standalone-e2e + if: >- + github.repository == 'inclusionAI/AKernel' && + github.event_name == 'push' && github.ref == 'refs/heads/main' + runs-on: ubuntu-22.04 + timeout-minutes: 30 + concurrency: + # Retain the previous workflow's lock key while older runs finish. + group: ci-standalone-${{ github.ref }} + cancel-in-progress: false + queue: max + + steps: + - name: Check whether main still needs this image + id: current-main + env: + GH_TOKEN: ${{ github.token }} + run: | + main_sha="$(gh api "repos/${GITHUB_REPOSITORY}/commits/main" --jq .sha)" + if [[ "${main_sha}" == "${GITHUB_SHA}" ]]; then + echo "publish=true" >> "${GITHUB_OUTPUT}" + else + echo "Skipped latest: main has advanced to ${main_sha}." \ + >> "${GITHUB_STEP_SUMMARY}" + fi + + - name: Download tested image + if: steps.current-main.outputs.publish == 'true' + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + artifact-ids: ${{ needs.standalone-e2e.outputs.artifact-id }} + path: ${{ runner.temp }} + merge-multiple: true + + - name: Load and verify tested image + if: steps.current-main.outputs.publish == 'true' + env: + EXPECTED_IMAGE_ID: ${{ needs.standalone-e2e.outputs.image-id }} + run: | + docker load --input "${RUNNER_TEMP}/all-in-one.tar.gz" + actual_image_id="$(docker image inspect "akernel-ci/all-in-one:${GITHUB_SHA}" \ + --format '{{.Id}}')" + test -n "${EXPECTED_IMAGE_ID}" + test "${actual_image_id}" = "${EXPECTED_IMAGE_ID}" + + - name: Log in to Docker Hub + if: steps.current-main.outputs.publish == 'true' uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4 with: username: ${{ vars.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Publish tested all-in-one image - if: >- - github.repository == 'inclusionAI/AKernel' && - github.event_name == 'push' && github.ref == 'refs/heads/main' + if: steps.current-main.outputs.publish == 'true' env: GH_TOKEN: ${{ github.token }} IMAGE_REPOSITORY: akerneldev/all-in-one diff --git a/AGENTS.md b/AGENTS.md index 7ba109c..0bf8999 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -543,11 +543,21 @@ independent of external package mirrors. Configure the repository Actions variable `DOCKERHUB_USERNAME` and secret `DOCKERHUB_TOKEN` with Docker Hub credentials that can push to `akerneldev/all-in-one`. Keep credentials out of source and logs. Main CI runs -are not canceled by subsequent pushes. The standalone job serializes builds, -tests, and publication using `queue: max` (up to 100 pending jobs), so a slow -or rerun job cannot overwrite a newer published `latest`. Do not remove the -main-head check or the publication lock. A failed check or push leaves the -run failed and can be retried using GitHub Actions' rerun controls. +are not canceled by subsequent pushes. After E2E and teardown succeed on +upstream main, the standalone job exports the tested image as a compressed +Actions artifact retained for one day. The separate `publish-dockerhub` job +downloads that exact artifact by ID, loads it, and verifies its image ID +against the E2E job output before publishing. PRs and forks skip image +transfer and publication. Do not rebuild the image in the publishing job. + +The publishing job serializes publication using `queue: max` (up to 100 +pending jobs), so a slow or rerun job cannot overwrite a newer published +`latest`. Keep its existing lock key to coordinate with older workflow runs. +Check main before downloading and again immediately before pushing while +holding this lock. Do not remove the main-head check or the publication lock. +A failed check or push leaves the run failed. Rerun the failed publishing job +while the artifact is available; after it expires, rerun all jobs to rebuild +and retest the image. Python SDK releases use stable `vX.Y.Z` tags or release-candidate `vX.Y.ZrcN` tags. The tag version must match the version in