diff --git a/.github/workflows/autopilot-auto-merge.yml b/.github/workflows/autopilot-auto-merge.yml index 9a8e54d..938b7da 100644 --- a/.github/workflows/autopilot-auto-merge.yml +++ b/.github/workflows/autopilot-auto-merge.yml @@ -21,7 +21,7 @@ concurrency: jobs: auto-merge: name: Autopilot auto-merge - runs-on: [self-hosted, evoo-ci, Linux] + runs-on: [self-hosted, evoo-ci] steps: - name: Resolve pull request id: pr @@ -71,7 +71,10 @@ jobs: HEAD_SHA: ${{ steps.pr.outputs.head_sha }} run: | set -euo pipefail - mapfile -t conclusions < <( + conclusions=() + while IFS= read -r conclusion; do + [[ -n "$conclusion" ]] && conclusions+=("$conclusion") + done < <( gh api "repos/${GITHUB_REPOSITORY}/commits/${HEAD_SHA}/check-runs" --paginate \ --jq '.check_runs[] | select(.name == "Product CI") | .conclusion' ) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7a519fa..db204d9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,10 +29,13 @@ concurrency: group: ci-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} +# Jobs that only need Node/pnpm/bash use [self-hosted, evoo-ci] so macOS runners (air, mini) +# can share load. Linux-only work keeps the Linux label (Docker, xvfb, local Postgres :5433). + jobs: changes: name: Detect product changes - runs-on: [self-hosted, evoo-ci, Linux] + runs-on: [self-hosted, evoo-ci] outputs: product: ${{ steps.filter.outputs.product }} steps: @@ -63,7 +66,7 @@ jobs: name: Lint needs: changes if: needs.changes.outputs.product == 'true' - runs-on: [self-hosted, evoo-ci, Linux] + runs-on: [self-hosted, evoo-ci] steps: - uses: actions/checkout@v5 with: @@ -99,7 +102,7 @@ jobs: name: Typecheck needs: changes if: needs.changes.outputs.product == 'true' - runs-on: [self-hosted, evoo-ci, Linux] + runs-on: [self-hosted, evoo-ci] steps: - uses: actions/checkout@v5 with: @@ -190,7 +193,7 @@ jobs: name: Unit tests needs: changes if: needs.changes.outputs.product == 'true' - runs-on: [self-hosted, evoo-ci, Linux] + runs-on: [self-hosted, evoo-ci] steps: - uses: actions/checkout@v5 with: @@ -274,7 +277,7 @@ jobs: name: Product CI if: always() needs: [changes, lint, check, build, test, test-integration, test-e2e] - runs-on: [self-hosted, evoo-ci, Linux] + runs-on: [self-hosted, evoo-ci] steps: - name: Verify product checks env: diff --git a/.github/workflows/publish-server-image.yml b/.github/workflows/publish-server-image.yml index 27477ba..96efacf 100644 --- a/.github/workflows/publish-server-image.yml +++ b/.github/workflows/publish-server-image.yml @@ -5,8 +5,9 @@ name: publish-server-image # cache, so untrusted code can validate the images without receiving package-publish authority. # Self-hosted runners on one host often share ~/.docker; build/publish set DOCKER_CONFIG under # RUNNER_TEMP so docker/login-action post-logout cannot evict a concurrent job's GHCR session. -# `ci---` handoff tags are registry labels on the per-arch digest; publish -# removes them via the distribution API (tag only, not the digest edge/sha tags reference). +# `ci---` handoff tags are extra aliases on the per-arch digest; GHCR does not +# expose a safe tag-only delete API (DELETE /v2/.../manifests/ is unsupported or digest-scoped), +# so they are left in place and do not affect edge/sha-* manifests assembled in publish. on: push: branches: [main] @@ -52,7 +53,7 @@ concurrency: jobs: changes: name: Detect image publish inputs - runs-on: [self-hosted, evoo-ci, Linux] + runs-on: [self-hosted, evoo-ci] outputs: publish: ${{ steps.decide.outputs.publish }} steps: @@ -355,22 +356,3 @@ jobs: subject-name: ghcr.io/${{ steps.ns.outputs.repo }}/${{ matrix.name }} subject-digest: ${{ steps.manifest.outputs.digest }} push-to-registry: true - - name: Delete CI handoff tags - env: - GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }} - IMAGE: ghcr.io/${{ steps.ns.outputs.repo }}/${{ matrix.name }} - IMAGE_NAME: ${{ matrix.name }} - run: | - set -euo pipefail - repository="${IMAGE#ghcr.io/}" - token="$(curl -fsSL -u "${GITHUB_ACTOR}:${GHCR_TOKEN}" \ - "https://ghcr.io/token?service=ghcr.io&scope=repository:${repository}:pull,push" \ - | jq -er .token)" - for arch in amd64; do - tag="ci-${GITHUB_RUN_ID}-${IMAGE_NAME}-${arch}" - curl -fsSL -o /dev/null -X DELETE \ - -H "Authorization: Bearer ${token}" \ - -H "Accept: application/vnd.docker.distribution.manifest.v2+json, application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.oci.image.index.v1+json" \ - "https://ghcr.io/v2/${repository}/manifests/${tag}" - echo "Removed handoff tag ${tag} from ${repository}." - done diff --git a/infra/updater/src/publish-workflow.test.ts b/infra/updater/src/publish-workflow.test.ts index 9bdaea7..cc75541 100644 --- a/infra/updater/src/publish-workflow.test.ts +++ b/infra/updater/src/publish-workflow.test.ts @@ -62,12 +62,12 @@ describe("server image publish workflow", () => { expect(workflowText).toContain("ci-${GITHUB_RUN_ID}-"); }); - it("isolates Docker registry auth per job and deletes handoff tags without imagetools rm", () => { + it("isolates Docker registry auth per job and does not delete handoff tags via the registry API", () => { expect(workflowText).toContain('echo "DOCKER_CONFIG=${cfg}" >> "$GITHUB_ENV"'); expect(workflowText).toContain("Prepare isolated Docker config directory"); expect(workflowText).not.toContain("imagetools rm"); - expect(workflowText).toContain("Delete CI handoff tags"); - expect(workflowText).toContain("/v2/${repository}/manifests/${tag}"); + expect(workflowText).not.toContain("Delete CI handoff tags"); + expect(workflowText).toContain("safe tag-only delete API"); }); it("names every image under the lowercase repository namespace", () => {