Repository navigation
chore(deps): run e2e on Kubernetes 1.37, move go-git v6 to beta.1, and unblock the 0.52.0 release lint #1784
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Validation pipeline: everything needed to trust a commit, nothing that | |
| # publishes. Runs directly for pull requests (fork-safe: no secrets, no | |
| # registry writes) and is called as a reusable workflow by release.yml for | |
| # pushes to main. See docs/ci-overview.md for the full CI/CD design. | |
| # | |
| # Trust model in one rule: untrusted (PR) code is built and tested, but never | |
| # meets a write token. On PRs images move between jobs as artifacts; on | |
| # trusted runs they are pushed to ghcr.io so later jobs (and the release | |
| # workflow) can pull them. GitHub enforces the fork side of this: fork PRs | |
| # always get a read-only GITHUB_TOKEN, whatever permissions are declared here. | |
| name: CI | |
| on: | |
| # Every pull request, whatever its base — deliberately NOT filtered to `branches: [main]`. | |
| # A stacked PR (PR N based on PR N-1's branch) never matches a main-only filter, so it | |
| # merged with no lint, no unit tests, and no e2e: PR #255 ran only the external review | |
| # bots. Retargeting does not rescue it either — when the base branch merges, GitHub | |
| # retargets the open PR onto main and fires `pull_request` with action `edited`, which | |
| # is not in the default trigger set (opened/synchronize/reopened), so nothing re-runs. | |
| # Validating every base costs a little runner time and is what makes a stack reviewable | |
| # one PR at a time. | |
| pull_request: | |
| workflow_call: | |
| outputs: | |
| ci-image: | |
| description: CI base container image (pushed to ghcr.io on trusted runs) | |
| value: ${{ jobs.ci-container.outputs.image }} | |
| workflow_dispatch: | |
| env: | |
| REGISTRY: ghcr.io | |
| IMAGE_NAME: configbutler/gitops-reverser | |
| IMAGE_TAG: ci-${{ github.sha }} | |
| CI_WORKDIR: /workspaces/${{ github.event.repository.name }} | |
| permissions: | |
| contents: read | |
| # PR runs dedupe per PR (a new push cancels the previous run). When called | |
| # from release.yml there is no PR number; github.run_id keeps the group unique | |
| # so the called run never interferes with the caller's own concurrency group. | |
| concurrency: | |
| group: ci-${{ github.event.pull_request.number || github.run_id }} | |
| cancel-in-progress: true | |
| jobs: | |
| ci-container: | |
| name: CI base container | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| outputs: | |
| image: ${{ steps.image.outputs.name }} | |
| permissions: | |
| contents: read | |
| # Only trusted (non-PR) runs push. Fork PRs get a read-only token from | |
| # GitHub regardless of this declaration; same-repo PR authors already | |
| # have repo write access, so nothing is escalated for them either. | |
| packages: write | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0 | |
| - name: Login to GitHub Container Registry | |
| if: github.event_name != 'pull_request' | |
| uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Set image name | |
| id: image | |
| run: | | |
| IMAGE="${REGISTRY}/configbutler/gitops-reverser-ci:${{ github.sha }}" | |
| echo "Building CI base container: ${IMAGE}" | |
| echo "name=${IMAGE}" >> "$GITHUB_OUTPUT" | |
| - name: Build CI base container | |
| # PRs build and load locally only (their token cannot publish, and | |
| # untrusted code must not publish anyway); trusted runs also push. | |
| # :latest is only moved from main so a workflow_dispatch on a topic | |
| # branch can't repoint it. | |
| uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0 | |
| with: | |
| context: . | |
| file: .devcontainer/Dockerfile | |
| target: ci | |
| push: ${{ github.event_name != 'pull_request' }} | |
| load: true # So the validate step below can run without pulling | |
| tags: | | |
| ${{ steps.image.outputs.name }} | |
| ${{ github.ref == 'refs/heads/main' && format('{0}/configbutler/gitops-reverser-ci:latest', env.REGISTRY) || '' }} | |
| cache-from: type=gha,scope=ci-container | |
| cache-to: ${{ github.event_name != 'pull_request' && 'type=gha,mode=max,scope=ci-container' || '' }} | |
| - name: Validate CI container tools | |
| run: | | |
| docker run --rm ${{ steps.image.outputs.name }} bash -lc " | |
| set -e | |
| echo '=== Validating CI Container Tools ===' | |
| go version | |
| kubectl version --client | |
| kustomize version | |
| helm version | |
| flux-operator version --client | |
| golangci-lint version | |
| actionlint --version | |
| hadolint --version | |
| markdownlint-cli2 --version | |
| vale --version | |
| controller-gen --version | |
| k3d version | |
| docker --version | |
| cosign version | |
| echo '✅ All CI container tools verified' | |
| " | |
| - name: Save CI container image (PR) | |
| # PR builds aren't pushed; hand the image to later jobs as an artifact. | |
| if: github.event_name == 'pull_request' | |
| run: docker save "${{ steps.image.outputs.name }}" -o ci-container-image.tar | |
| - name: Upload CI container image artifact (PR) | |
| if: github.event_name == 'pull_request' | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: ci-container-image | |
| path: ci-container-image.tar | |
| if-no-files-found: error | |
| retention-days: 1 | |
| build: | |
| name: Project image (instrumented, artifact-only) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| outputs: | |
| image: ${{ steps.image.outputs.name }} | |
| permissions: | |
| # No push on any run: this image is never published, so no write token. | |
| contents: read | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Set full image name and build metadata | |
| id: image | |
| run: | | |
| FULL_IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }}" | |
| echo "name=${FULL_IMAGE}" >> "$GITHUB_OUTPUT" | |
| echo "git_commit=${GITHUB_SHA:0:7}" >> "$GITHUB_OUTPUT" | |
| echo "build_date=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT" | |
| echo "Building image: ${FULL_IMAGE}" | |
| - name: Build Docker image | |
| # Coverage-instrumented (GOCOVER=1), amd64-only. Never pushed on any | |
| # run: delivered to later jobs (e2e) as a docker-save artifact and | |
| # imported into k3d via IMAGE_DELIVERY_MODE=load — the fork-safe path | |
| # every PR already exercises, now the only path. The registry-served | |
| # bytes are the release-grade build-release-* images, so no | |
| # instrumented digest can ever be mistaken for promotable. Keeps the | |
| # ci-<sha> tag name only as a local Docker/k3d reference. | |
| uses: ./.github/actions/docker-build | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| login: false | |
| platforms: linux/amd64 | |
| tags: ${{ steps.image.outputs.name }} | |
| push: false | |
| load: true | |
| # Own cache scope so the instrumented build and the release-grade | |
| # build-linux/amd64 scope no longer invalidate each other. | |
| cache-scope: build-instrumented-linux/amd64 | |
| cache-to: ${{ github.event_name != 'pull_request' && 'type=gha,mode=max,scope=build-instrumented-linux/amd64' || '' }} | |
| build-args: | | |
| VERSION=${{ env.IMAGE_TAG }} | |
| GIT_COMMIT=${{ steps.image.outputs.git_commit }} | |
| GIT_DIRTY=0 | |
| BUILD_DATE=${{ steps.image.outputs.build_date }} | |
| GOCOVER=1 | |
| - name: Save project image | |
| # Delivered as an artifact on every run (PR and main): e2e loads it and | |
| # imports it into k3d. | |
| run: docker save "${{ steps.image.outputs.name }}" -o project-image.tar | |
| - name: Upload project image artifact | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: project-image | |
| path: project-image.tar | |
| if-no-files-found: error | |
| # ~7 days (was 1): a failed main e2e can still be reproduced from the | |
| # exact instrumented image that ran, now that it is never pushed. | |
| retention-days: 7 | |
| # Release-grade images: clean (no coverage), semver-stamped, pushed by digest | |
| # on every push to main so the release tail can retag instead of rebuild. | |
| # Two named jobs, not a matrix: publish-manifest needs each per-arch digest | |
| # individually, and matrix jobs share one output set (last writer wins). | |
| # | |
| # Gated on `push` + `refs/heads/main` (not `!= 'pull_request'`, which would | |
| # also fire release builds from a workflow_dispatch on any branch). Note: | |
| # gating on `event_name == 'workflow_call'` would never match — inside a | |
| # called workflow the github context belongs to the *caller*, so a release.yml | |
| # push shows up here as event_name == 'push'. | |
| build-release-amd64: | |
| name: Release image linux/amd64 | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/main' | |
| outputs: | |
| digest: ${{ steps.build.outputs.digest }} | |
| permissions: | |
| contents: read | |
| packages: write | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Compute release build metadata | |
| id: meta | |
| run: | | |
| # On the release-bump commit this is the new semver — release-please | |
| # bumped .release-please-manifest.json in the PR before merge. On | |
| # ordinary main pushes it is the *previous* released version; that is | |
| # fine because only the bump commit's digest is ever promoted to a | |
| # semver tag, and GIT_COMMIT disambiguates never-promoted candidates. | |
| VERSION="$(jq -r '."."' .release-please-manifest.json)" | |
| echo "version=${VERSION}" >> "$GITHUB_OUTPUT" | |
| echo "git_commit=${GITHUB_SHA:0:7}" >> "$GITHUB_OUTPUT" | |
| echo "build_date=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT" | |
| - name: Extract OCI labels | |
| id: docker_meta | |
| uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 | |
| with: | |
| images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} | |
| - name: Build and push release image by digest | |
| id: build | |
| uses: ./.github/actions/docker-build | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| platforms: linux/amd64 | |
| labels: ${{ steps.docker_meta.outputs.labels }} | |
| outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true | |
| # Reuses the release cache scope: same build args at release time, so | |
| # every main push keeps this cache warm. | |
| cache-scope: build-linux/amd64 | |
| cache-to: type=gha,mode=max,scope=build-linux/amd64 | |
| build-args: | | |
| VERSION=${{ steps.meta.outputs.version }} | |
| GIT_COMMIT=${{ steps.meta.outputs.git_commit }} | |
| GIT_DIRTY=0 | |
| BUILD_DATE=${{ steps.meta.outputs.build_date }} | |
| - name: Export digest | |
| run: | | |
| mkdir -p /tmp/digests | |
| digest="${{ steps.build.outputs.digest }}" | |
| touch "/tmp/digests/${digest#sha256:}" | |
| - name: Upload digest | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: digests-amd64 | |
| path: /tmp/digests/* | |
| if-no-files-found: error | |
| retention-days: 1 | |
| build-release-arm64: | |
| name: Release image linux/arm64 | |
| runs-on: ubuntu-24.04-arm | |
| timeout-minutes: 30 | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/main' | |
| outputs: | |
| digest: ${{ steps.build.outputs.digest }} | |
| permissions: | |
| contents: read | |
| packages: write | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Compute release build metadata | |
| id: meta | |
| run: | | |
| # See build-release-amd64: previous version on non-bump commits is | |
| # expected and harmless. | |
| VERSION="$(jq -r '."."' .release-please-manifest.json)" | |
| echo "version=${VERSION}" >> "$GITHUB_OUTPUT" | |
| echo "git_commit=${GITHUB_SHA:0:7}" >> "$GITHUB_OUTPUT" | |
| echo "build_date=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT" | |
| - name: Extract OCI labels | |
| id: docker_meta | |
| uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 | |
| with: | |
| images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} | |
| - name: Build and push release image by digest | |
| id: build | |
| uses: ./.github/actions/docker-build | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| platforms: linux/arm64 | |
| labels: ${{ steps.docker_meta.outputs.labels }} | |
| outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true | |
| cache-scope: build-linux/arm64 | |
| cache-to: type=gha,mode=max,scope=build-linux/arm64 | |
| build-args: | | |
| VERSION=${{ steps.meta.outputs.version }} | |
| GIT_COMMIT=${{ steps.meta.outputs.git_commit }} | |
| GIT_DIRTY=0 | |
| BUILD_DATE=${{ steps.meta.outputs.build_date }} | |
| - name: Export digest | |
| run: | | |
| mkdir -p /tmp/digests | |
| digest="${{ steps.build.outputs.digest }}" | |
| touch "/tmp/digests/${digest#sha256:}" | |
| - name: Upload digest | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: digests-arm64 | |
| path: /tmp/digests/* | |
| if-no-files-found: error | |
| retention-days: 1 | |
| validate-devcontainer: | |
| name: Validate Dev Container | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| needs: ci-container | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0 | |
| - name: Build dev container (validation only) | |
| uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0 | |
| with: | |
| context: . | |
| file: .devcontainer/Dockerfile | |
| target: dev | |
| push: false | |
| load: true | |
| tags: gitops-reverser-devcontainer:test | |
| cache-from: type=gha,scope=devcontainer | |
| cache-to: ${{ github.event_name == 'pull_request' && '' || 'type=gha,scope=devcontainer,mode=max' }} | |
| build-args: | | |
| BUILDKIT_CONTEXT_KEEP_GIT_DIR=1 | |
| - name: Validate dev container tools | |
| run: | | |
| docker run --rm gitops-reverser-devcontainer:test bash -lc " | |
| set -e | |
| export PATH=/usr/local/go/bin:/go/bin:\$PATH | |
| echo '=== Validating Dev Container Tools ===' | |
| go version | |
| kubectl version --client | |
| kustomize version | |
| helm version | |
| flux-operator version --client | |
| golangci-lint version | |
| actionlint --version | |
| markdownlint-cli2 --version | |
| vale --version | |
| k3d version | |
| task --version | |
| bash -ic 'complete -p task >/dev/null' | |
| docker --version | |
| echo '✅ All dev container tools verified' | |
| " | |
| lint-helm: | |
| name: Build Helm chart and installer bundle | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| needs: ci-container | |
| permissions: | |
| contents: read | |
| packages: read | |
| env: | |
| CI_CONTAINER: ${{ needs.ci-container.outputs.image }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Login to Docker registry | |
| if: github.event_name != 'pull_request' | |
| run: | | |
| echo "${{ secrets.GITHUB_TOKEN }}" | docker login ${{ env.REGISTRY }} -u ${{ github.actor }} --password-stdin | |
| - name: Download CI container image (PR) | |
| if: github.event_name == 'pull_request' | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: ci-container-image | |
| path: . | |
| - name: Load CI container image (PR) | |
| if: github.event_name == 'pull_request' | |
| # rm after load: the multi-GB tarball otherwise stays on disk for the whole | |
| # job — e2e runners have been observed within 1.5GB of the kubelet eviction | |
| # threshold (run 28623232380). | |
| run: docker load -i ci-container-image.tar && rm -f ci-container-image.tar | |
| - name: Lint and package Helm chart | |
| run: | | |
| docker run --rm \ | |
| -v "${GITHUB_WORKSPACE}:${{ env.CI_WORKDIR }}" \ | |
| -w "${{ env.CI_WORKDIR }}" \ | |
| "${CI_CONTAINER}" \ | |
| bash -lc ' | |
| set -e | |
| git config --global --add safe.directory "$PWD" | |
| task helm-sync | |
| helm lint charts/gitops-reverser | |
| helm template gitops-reverser charts/gitops-reverser \ | |
| --namespace gitops-reverser-system \ | |
| --set image.repository=test/image \ | |
| --set image.tag=test | |
| task dist-install | |
| helm package charts/gitops-reverser --destination . | |
| mv gitops-reverser-*.tgz gitops-reverser.tgz | |
| ' | |
| - name: Upload release bundle artifact | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: release-bundle | |
| path: | | |
| dist/crds.yaml | |
| dist/install.yaml | |
| gitops-reverser.tgz | |
| if-no-files-found: error | |
| lint: | |
| name: Lint | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| needs: ci-container | |
| permissions: | |
| contents: read | |
| packages: read | |
| env: | |
| CI_CONTAINER: ${{ needs.ci-container.outputs.image }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Login to Docker registry | |
| if: github.event_name != 'pull_request' | |
| run: | | |
| echo "${{ secrets.GITHUB_TOKEN }}" | docker login ${{ env.REGISTRY }} -u ${{ github.actor }} --password-stdin | |
| - name: Download CI container image (PR) | |
| if: github.event_name == 'pull_request' | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: ci-container-image | |
| path: . | |
| - name: Load CI container image (PR) | |
| if: github.event_name == 'pull_request' | |
| # rm after load: the multi-GB tarball otherwise stays on disk for the whole | |
| # job — e2e runners have been observed within 1.5GB of the kubelet eviction | |
| # threshold (run 28623232380). | |
| run: docker load -i ci-container-image.tar && rm -f ci-container-image.tar | |
| - name: Run lint tasks | |
| run: | | |
| docker run --rm \ | |
| -v "${GITHUB_WORKSPACE}:${{ env.CI_WORKDIR }}" \ | |
| -w "${{ env.CI_WORKDIR }}" \ | |
| "${CI_CONTAINER}" \ | |
| bash -lc ' | |
| set -e | |
| git config --global --add safe.directory "$PWD" | |
| task lint | |
| cache_log="$(mktemp)" | |
| task --dry --verbose lint 2>&1 | tee "${cache_log}" | |
| for task_name in \ | |
| generate \ | |
| manifests \ | |
| helm-sync \ | |
| lint-golang \ | |
| lint-dockerfiles \ | |
| lint-actions \ | |
| lint-helm | |
| do | |
| if ! grep -Fq "Task \"${task_name}\" is up to date" "${cache_log}"; then | |
| echo "Expected ${task_name} to be cached after task lint" >&2 | |
| exit 1 | |
| fi | |
| done | |
| # The five docs tasks are exempt on purpose, and all for one reason: none | |
| # of them carries `sources:` (see Taskfile-build.yml). Their real input is | |
| # "every tracked file" — every .md for lint-markdown and lint-prose, plus | |
| # .go, .yml and .sh for lint-doc-links and lint-metric-names — which Task | |
| # cannot express: an unrooted `**` glob walks external-sources/ and has | |
| # OOMed the host, and a hand-maintained file list silently drifts out of | |
| # sync with what the checkers scan. A stale "up to date" means the check | |
| # never runs, and lint-metric-names exists precisely because a check that | |
| # does not run reads as a confident zero. All four tools together cost a few | |
| # seconds over the whole tree, so they always run. lint-docs itself is the | |
| # aggregate and has no cmds of its own. | |
| # | |
| # Adding a task to `task lint` without `sources:` and without adding it | |
| # here fails this step. That is deliberate: it forces the choice to be made | |
| # rather than silently skipped. | |
| # | |
| # awk, not `grep -qv`: the CI image ships ugrep as grep, whose -q -v returns | |
| # 1 even when -c reports a selected line. That would make this assertion | |
| # silently pass whatever it was handed. | |
| exempt="lint-docs|lint-doc-links|lint-markdown|lint-prose|lint-metric-names" | |
| uncached="$(awk -v ex="^task: \\\\[(${exempt})\\\\]" "/^task: \\[/ && \$0 !~ ex" "${cache_log}")" | |
| if [ -n "${uncached}" ]; then | |
| echo "Expected second task lint dry-run to be fully cached apart from the docs tasks, but these would run:" >&2 | |
| printf "%s\n" "${uncached}" >&2 | |
| exit 1 | |
| fi | |
| ' | |
| - name: GitOps layout baseline is current | |
| # test/fixtures/gitops-layouts/support-today.md is generated from a live | |
| # manifest-analyzer scan of the corpus. Regenerating it here and failing on a | |
| # diff is what makes a change to the acceptance boundary show up in review as | |
| # "exactly which fixtures moved, and in which direction". Without this check the | |
| # baseline silently goes stale, and the corpus stops being evidence of anything. | |
| # | |
| # A failure is not necessarily a bug: if the boundary moved on purpose, run | |
| # `task gitops-layouts-baseline` and commit the regenerated file. Review the diff. | |
| # | |
| # Kept out of `task lint` deliberately: the lint step above asserts that a second | |
| # dry-run is fully cached, and this task's fingerprint would have to be threaded | |
| # through that assertion for no benefit. | |
| run: | | |
| docker run --rm \ | |
| -v "${GITHUB_WORKSPACE}:${{ env.CI_WORKDIR }}" \ | |
| -w "${{ env.CI_WORKDIR }}" \ | |
| "${CI_CONTAINER}" \ | |
| bash -lc ' | |
| set -e | |
| git config --global --add safe.directory "$PWD" | |
| task gitops-layouts-baseline --force | |
| if ! git diff --quiet -- test/fixtures/gitops-layouts/support-today.md; then | |
| # No backticks in the message below. It is a double-quoted string inside a | |
| # bash -lc block, where backticks are command substitution: the echo would | |
| # RUN the task a second time and splice its stdout into the annotation. | |
| echo "::error file=test/fixtures/gitops-layouts/support-today.md::The GitOps layout baseline is stale: the analyzer now reports something different for the corpus. If the acceptance boundary moved on purpose, run task gitops-layouts-baseline and commit the result." | |
| git --no-pager diff -- test/fixtures/gitops-layouts/support-today.md | |
| exit 1 | |
| fi | |
| echo "baseline is current" | |
| ' | |
| test: | |
| name: Unit tests | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| needs: ci-container | |
| permissions: | |
| contents: read | |
| packages: read | |
| id-token: write | |
| env: | |
| CI_CONTAINER: ${{ needs.ci-container.outputs.image }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Login to Docker registry | |
| if: github.event_name != 'pull_request' | |
| run: | | |
| echo "${{ secrets.GITHUB_TOKEN }}" | docker login ${{ env.REGISTRY }} -u ${{ github.actor }} --password-stdin | |
| - name: Download CI container image (PR) | |
| if: github.event_name == 'pull_request' | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: ci-container-image | |
| path: . | |
| - name: Load CI container image (PR) | |
| if: github.event_name == 'pull_request' | |
| # rm after load: the multi-GB tarball otherwise stays on disk for the whole | |
| # job — e2e runners have been observed within 1.5GB of the kubelet eviction | |
| # threshold (run 28623232380). | |
| run: docker load -i ci-container-image.tar && rm -f ci-container-image.tar | |
| - name: Run tests | |
| run: | | |
| docker run --rm \ | |
| -v "${GITHUB_WORKSPACE}:${{ env.CI_WORKDIR }}" \ | |
| -w "${{ env.CI_WORKDIR }}" \ | |
| "${CI_CONTAINER}" \ | |
| bash -lc ' | |
| set -e | |
| git config --global --add safe.directory "$PWD" | |
| task test | |
| ' | |
| - name: Make test artifacts readable by the runner | |
| if: always() | |
| run: sudo chmod -R a+rX cover.out .coverage-baseline config/crd/bases config/rbac config/webhook 2>/dev/null || true | |
| - name: Upload unit coverage to Codecov | |
| uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1 | |
| with: | |
| files: cover.out | |
| flags: unit | |
| use_oidc: true | |
| fail_ci_if_error: false | |
| image-scan: | |
| name: Scan project image | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| needs: [build, ci-container] | |
| # PRs only: scans the instrumented image the PR built (delivered as an | |
| # artifact). On main the shipped bytes are the release-grade digests, which | |
| # image-scan-release scans instead — so this scan reports to the job log | |
| # and never needs a write token. | |
| if: github.event_name == 'pull_request' | |
| permissions: | |
| contents: read | |
| packages: read | |
| env: | |
| PROJECT_IMAGE: ${{ needs.build.outputs.image }} | |
| CI_CONTAINER: ${{ needs.ci-container.outputs.image }} | |
| # Inside the container this is a workspace path, so the runner-side cache step | |
| # below can persist it between runs. Without a cache Trivy re-downloads its whole | |
| # vulnerability DB on every job, which is slow and rate-limited at the source. | |
| TRIVY_CACHE_DIR: ${{ github.workspace }}/.stamps/trivy-cache | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Download project image | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: project-image | |
| path: . | |
| - name: Download CI container image | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: ci-container-image | |
| path: . | |
| - name: Load CI container image | |
| # rm after load: the tarball is multi-GB and otherwise occupies the runner | |
| # for the rest of the job. | |
| run: docker load -i ci-container-image.tar && rm -f ci-container-image.tar | |
| - name: Restore Trivy vulnerability DB | |
| uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 | |
| with: | |
| path: .stamps/trivy-cache | |
| key: trivy-db-${{ github.run_id }} | |
| restore-keys: trivy-db- | |
| - name: Scan project image | |
| # The project image is scanned straight from its artifact tarball, so this | |
| # needs neither a `docker load` of it nor a registry pull. What the task does | |
| # — report everything, then gate on CRITICAL-with-a-fix — is documented where | |
| # it is defined; keeping it there is what lets a maintainer reproduce this | |
| # exact gate with `task scan-image SCAN_ARCHIVE=project-image.tar`. | |
| run: | | |
| docker run --rm \ | |
| -v "${GITHUB_WORKSPACE}:${{ env.CI_WORKDIR }}" \ | |
| -w "${{ env.CI_WORKDIR }}" \ | |
| -e TRIVY_CACHE_DIR="${{ env.CI_WORKDIR }}/.stamps/trivy-cache" \ | |
| "${CI_CONTAINER}" \ | |
| bash -lc ' | |
| set -e | |
| git config --global --add safe.directory "$PWD" | |
| task scan-image SCAN_ARCHIVE=project-image.tar | |
| ' | |
| - name: Make the Trivy cache readable by the runner | |
| # The container writes it as root; actions/cache runs as the runner user. | |
| if: always() | |
| run: sudo chown -R "$(id -u):$(id -g)" .stamps/trivy-cache || true | |
| image-scan-release: | |
| name: Scan release image (${{ matrix.arch }}) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| # Rides its build jobs' gate: when build-release-* are skipped (PRs, | |
| # dispatch) this skips transitively — no skip-tolerant `if:` needed. The | |
| # release ships amd64 *and* arm64, so both digests are scanned; Trivy pulls | |
| # each by digest and analyzes the filesystem cross-arch. | |
| needs: [ci-container, build-release-amd64, build-release-arm64] | |
| permissions: | |
| contents: read | |
| packages: read | |
| security-events: write | |
| env: | |
| CI_CONTAINER: ${{ needs.ci-container.outputs.image }} | |
| TRIVY_CACHE_DIR: ${{ github.workspace }}/.stamps/trivy-cache | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - arch: amd64 | |
| digest: ${{ needs.build-release-amd64.outputs.digest }} | |
| - arch: arm64 | |
| digest: ${{ needs.build-release-arm64.outputs.digest }} | |
| steps: | |
| - name: Checkout code | |
| # This job scans a REMOTE image by digest and needs nothing from the repository | |
| # except the Taskfile and .trivyignore.yaml the scan below reads. Trivy treats a | |
| # missing --ignorefile as a fatal error, so a lost checkout fails loudly here | |
| # rather than quietly scanning without the suppressions. | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - name: Log in to registry | |
| # Each build-release-* job pushes by digest, and buildx wraps the | |
| # single-platform image in an OCI *index* (it attaches a provenance | |
| # attestation manifest). Trivy resolves the digest remotely but, for an | |
| # index ref, selects the child matching the *runner's* platform — so on | |
| # this amd64 runner the arm64 leg fails with "no child with platform | |
| # linux/amd64 in index". The Trivy steps below pin TRIVY_PLATFORM to the | |
| # matrix arch to select the right child. The login just covers | |
| # authenticated pulls; the package is public anyway. | |
| run: | | |
| echo "${{ secrets.GITHUB_TOKEN }}" | docker login ${{ env.REGISTRY }} -u ${{ github.actor }} --password-stdin | |
| - name: Restore Trivy vulnerability DB | |
| uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 | |
| with: | |
| path: .stamps/trivy-cache | |
| key: trivy-db-${{ github.run_id }} | |
| restore-keys: trivy-db- | |
| - name: Scan release image | |
| # The same `task scan-image` the PR gate runs, so the release and the PR are | |
| # held to one definition of what blocks. It writes SARIF for code scanning and | |
| # then gates; the upload below runs even when the gate fails, so a blocking | |
| # finding is still reported rather than only logged. | |
| # | |
| # Trivy pulls the image with its OWN registry client rather than through the | |
| # docker daemon, so the runner's `docker login` above does not reach it: the | |
| # credentials live in the runner's home, and this container sees only the | |
| # workspace. Mounting that config read-only and pointing DOCKER_CONFIG at it is | |
| # what keeps this working if the package ever stops being public. Read-only, and | |
| # outside the workspace, so the credential is never somewhere an artifact upload | |
| # could pick it up. DOCKER_CONFIG rather than a mount onto ~/.docker because it | |
| # does not care which user the image runs as. | |
| run: | | |
| docker run --rm \ | |
| -v "${GITHUB_WORKSPACE}:${{ env.CI_WORKDIR }}" \ | |
| -v "${HOME}/.docker:/tmp/docker-config:ro" \ | |
| -w "${{ env.CI_WORKDIR }}" \ | |
| -e DOCKER_CONFIG=/tmp/docker-config \ | |
| -e TRIVY_CACHE_DIR="${{ env.CI_WORKDIR }}/.stamps/trivy-cache" \ | |
| -e TRIVY_PLATFORM="linux/${{ matrix.arch }}" \ | |
| "${CI_CONTAINER}" \ | |
| bash -lc ' | |
| set -e | |
| git config --global --add safe.directory "$PWD" | |
| task scan-image \ | |
| SCAN_IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ matrix.digest }} \ | |
| SCAN_SARIF=trivy-image.sarif | |
| ' | |
| - name: Make scan output readable by the runner | |
| # The container writes both as root; the upload action and actions/cache run | |
| # as the runner user. | |
| if: always() | |
| run: sudo chown -R "$(id -u):$(id -g)" .stamps/trivy-cache trivy-image.sarif || true | |
| - name: Upload scan results to GitHub code scanning | |
| # always(): the SARIF is written by the report pass BEFORE the gate runs, so a | |
| # gate failure must not swallow the report that explains it. | |
| if: always() | |
| uses: github/codeql-action/upload-sarif@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2 | |
| with: | |
| sarif_file: trivy-image.sarif | |
| # Distinct category so the amd64 and arm64 legs don't overwrite each | |
| # other's code-scanning results. | |
| category: trivy-release-${{ matrix.arch }} | |
| e2e: | |
| name: E2E (${{ matrix.name }}) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 45 | |
| needs: [ci-container, build, lint-helm] | |
| permissions: | |
| contents: read | |
| packages: read | |
| id-token: write | |
| strategy: | |
| # Don't let one lane's flake cancel the other mid-run — a cancelled lane | |
| # loses its signal *and* fails its report upload with "No files found". | |
| fail-fast: false | |
| matrix: | |
| include: | |
| # Functional suite, sharded across two runners (was the single `full` | |
| # lane). The two label filters are exact complements within the old | |
| # `!image-refresh` filter — `manager` ∪ `!manager && !image-refresh` | |
| # equals what `full` ran — so no spec is added or dropped, the work is | |
| # just split across two clusters. The membership is a provisional | |
| # first cut; rebalance from the e2e-ginkgo-reports-* per-spec timings | |
| # (docs/finished/e2e-ci-runner-sharding-plan.md, "balance from real data"). | |
| # Each shard runs config-dir installs (GOCOVERDIR overlay) and collects | |
| # coverage; Codecov unions same-flag `e2e` uploads, so merged coverage | |
| # is the union of both shards — the non-regression ratchet still holds. | |
| - name: full-manager | |
| script: "task test-e2e" | |
| e2e_label_filter: "manager" | |
| e2e_report_name: "full-manager" | |
| needs_artifact: false | |
| coverage: "1" | |
| # Bumped 1 -> 4: the full suite was serialized to avoid the | |
| # cross-target rule-change snapshot coupling, now fixed (per-target | |
| # effective watch plan — docs/spec/gittarget-isolation-on-rule-change.md). | |
| e2e_ginkgo_procs: "4" | |
| - name: full-core | |
| script: "task test-e2e" | |
| # `bi-directional` and `source-cluster` moved out to their own legs (they | |
| # install Argo CD / kcp that `task prepare-e2e` does not perform), so this | |
| # filter is now a strict subset of what it used to run. | |
| e2e_label_filter: "!manager && !image-refresh && !bi-directional && !source-cluster" | |
| e2e_report_name: "full-core" | |
| needs_artifact: false | |
| coverage: "1" | |
| e2e_ginkgo_procs: "4" | |
| # The bi-directional corner: the only leg that installs Argo CD, and the | |
| # only place gitops-reverser shares a path with a foreign GitOps engine. | |
| # Its own runner (hence its own cluster) is what keeps the Argo CD | |
| # install off the other three legs. | |
| # docs/spec/e2e-bi-directional-corner.md. | |
| # | |
| # `task test-e2e-bi-directional` pins its own label filter and Ginkgo | |
| # procs (BI_DIRECTIONAL_GINKGO_PROCS), so e2e_label_filter and | |
| # e2e_ginkgo_procs are deliberately absent here rather than set to values | |
| # the task would ignore. It uses the default config-dir install, so it | |
| # carries the GOCOVERDIR overlay and its coverage unions with the others'. | |
| - name: bi-directional | |
| script: "task test-e2e-bi-directional" | |
| e2e_report_name: "bi-directional" | |
| needs_artifact: false | |
| coverage: "1" | |
| # The source-cluster corner: the only leg that installs kcp, and the only | |
| # place gitops-reverser mirrors REMOTE clusters (GitTarget.spec.kubeConfig). | |
| # kcp workspaces are cheap logical clusters, so its own runner installs a | |
| # small kcp control plane and provisions three workspaces as source clusters. | |
| # docs/finished/config-plane-split.md. | |
| # | |
| # `task test-e2e-source-cluster` pins its own label filter and Ginkgo procs | |
| # (SOURCE_CLUSTER_GINKGO_PROCS=1 — the specs share one kcp port-forward), so | |
| # e2e_label_filter and e2e_ginkgo_procs are deliberately absent here. It uses | |
| # the default config-dir install, so it carries the GOCOVERDIR overlay and its | |
| # coverage unions with the others'. | |
| - name: source-cluster | |
| script: "task test-e2e-source-cluster" | |
| e2e_report_name: "source-cluster" | |
| needs_artifact: false | |
| coverage: "1" | |
| # Quickstart chain, sharded across two runners (was the single | |
| # `quickstart` lane). quickstart-install runs the two install-mode | |
| # validations on one cluster (cleanup-installs.sh resets the namespace | |
| # between them, since install stamps don't invalidate on an image-ref | |
| # change). image-refresh gets its own runner: it validates the local | |
| # build → k3d load → rollout restart chain, so it must run against a | |
| # locally built image — unset PROJECT_IMAGE and force load delivery. | |
| # Fork-safe: neither leg pushes to a registry. Helm/manifest installs | |
| # don't carry the GOCOVERDIR overlay, so these legs collect no coverage. | |
| - name: quickstart-install | |
| script: >- | |
| export INSTALL_MODE=helm HELM_CHART_SOURCE=./gitops-reverser.tgz | |
| && task test-e2e-quickstart-helm | |
| && bash hack/cleanup-installs.sh k3d-gitops-reverser-test-e2e | |
| && export INSTALL_MODE=plain-manifests-file | |
| && task test-e2e-quickstart-manifest | |
| needs_artifact: true | |
| coverage: "" | |
| e2e_ginkgo_procs: "2" | |
| - name: image-refresh | |
| script: >- | |
| export INSTALL_MODE=helm HELM_CHART_SOURCE=./gitops-reverser.tgz | |
| && env -u PROJECT_IMAGE IMAGE_DELIVERY_MODE=load task test-image-refresh | |
| needs_artifact: true | |
| coverage: "" | |
| e2e_ginkgo_procs: "2" | |
| env: | |
| PROJECT_IMAGE: ${{ needs.build.outputs.image }} | |
| CI_CONTAINER: ${{ needs.ci-container.outputs.image }} | |
| # The instrumented project image is never pushed (PR *or* main), so it is | |
| # always delivered via `load`: the artifact is docker-loaded below and | |
| # imported into k3d. The CI container is still pulled from GHCR on main | |
| # (publish-helm needs a registry ref), so that delivery stays branched. | |
| IMAGE_DELIVERY_MODE: load | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Free runner disk | |
| # The e2e stack (CI container, project image, k3d node volumes, local | |
| # rebuilds) needs tens of GB; runners have been observed dipping below | |
| # the k3d node's kubelet eviction threshold mid-run (28624646437). | |
| # Drop the biggest preinstalled toolchains we never use. | |
| run: | | |
| df -h / | tail -1 | |
| sudo rm -rf /usr/local/lib/android /opt/hostedtoolcache/CodeQL /usr/share/dotnet & | |
| RM_PID=$! | |
| docker system prune -f >/dev/null 2>&1 || true | |
| wait "${RM_PID}" | |
| df -h / | tail -1 | |
| - name: Download release bundle artifact | |
| if: matrix.needs_artifact | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: release-bundle | |
| path: . | |
| - name: Download CI container image (PR) | |
| if: github.event_name == 'pull_request' | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: ci-container-image | |
| path: . | |
| - name: Load CI container image (PR) | |
| if: github.event_name == 'pull_request' | |
| # rm after load: the multi-GB tarball otherwise stays on disk for the whole | |
| # job — e2e runners have been observed within 1.5GB of the kubelet eviction | |
| # threshold (run 28623232380). | |
| run: docker load -i ci-container-image.tar && rm -f ci-container-image.tar | |
| - name: Login to Docker registry | |
| if: github.event_name != 'pull_request' | |
| run: | | |
| echo "${{ secrets.GITHUB_TOKEN }}" | docker login ${{ env.REGISTRY }} -u ${{ github.actor }} --password-stdin | |
| - name: Download project image | |
| # The project image is never pushed; pull the tarball built by the | |
| # build job (on PR and main alike). | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: project-image | |
| path: . | |
| - name: Load project image into Docker | |
| # Make PROJECT_IMAGE available in the runner's Docker daemon so the k3d | |
| # image import (load delivery) finds it locally instead of pulling. | |
| run: docker load -i project-image.tar && rm -f project-image.tar | |
| - name: Start disk usage sampler | |
| run: | | |
| # Sample free space on / every 10s for the whole e2e run so we can see | |
| # the peak (not just the post-run value). /var/lib/docker — k3d node | |
| # volumes, pulled images, build cache — lives on this filesystem. | |
| ( while true; do | |
| printf '%s avail=%s\n' "$(date -u +%H:%M:%S)" \ | |
| "$(df --output=avail -BM / | tail -1 | tr -d ' ')" | |
| sleep 10 | |
| done ) > "df-samples-${{ matrix.name }}.log" 2>/dev/null & | |
| echo "SAMPLER_PID=$!" >> "$GITHUB_ENV" | |
| - name: Bring up cluster + Flux services (explicit, streaming logs) | |
| run: | | |
| # Install-mode-/image-independent slice of prepare-e2e. Runs the | |
| # slow, hang-prone cluster + Flux + services bring-up here so its | |
| # logs stream live, instead of being buffered inside Ginkgo's | |
| # SynchronizedBeforeSuite (where a stall just looks like a silent | |
| # 15m hang ending in a goroutine dump). The in-suite prepare-e2e | |
| # no-ops this part because the stamps already exist. | |
| docker run --rm \ | |
| --network host \ | |
| -v "${GITHUB_WORKSPACE}:${{ env.CI_WORKDIR }}" \ | |
| -v /var/run/docker.sock:/var/run/docker.sock \ | |
| -w "${{ env.CI_WORKDIR }}" \ | |
| -e IMAGE_DELIVERY_MODE=${{ env.IMAGE_DELIVERY_MODE }} \ | |
| -e HOST_PROJECT_PATH=${{ github.workspace }} \ | |
| -e KUBECONFIG=${{ env.CI_WORKDIR }}/.stamps/cluster/k3d-gitops-reverser-test-e2e/.kube/config \ | |
| ${{ env.CI_CONTAINER }} \ | |
| bash -c " | |
| git config --global --add safe.directory ${{ env.CI_WORKDIR }} | |
| # The cluster is created here but reused by the test container | |
| # below. start-cluster.sh writes the k3d context into KUBECONFIG; | |
| # point it at a file on the mounted workspace so the next | |
| # container inherits the context (otherwise it is lost with this | |
| # ephemeral container and kubectl reports 'context does not exist'). | |
| # Living under .stamps/cluster/<ctx>/.kube keeps it tied to the | |
| # cluster lifecycle: cleanup-installs.sh (per-namespace) leaves it | |
| # alone, while clean-cluster (rm -rf .stamps/cluster/<ctx>) clears | |
| # it together with the cluster it points at. | |
| mkdir -p .stamps/cluster/k3d-gitops-reverser-test-e2e/.kube | |
| task _services-ready | |
| " | |
| - name: Run E2E tests in CI container | |
| # E2E_LABEL_FILTER / E2E_REPORT_NAME are passed through by name (not | |
| # value) so the shell never sees the filter's spaces or `&&` — GitHub | |
| # sets them as process env vars and `docker run -e VAR` forwards them. | |
| # They're only consumed by `task test-e2e` (the functional shards); the | |
| # quickstart legs leave them empty and ignore them. | |
| env: | |
| E2E_LABEL_FILTER: ${{ matrix.e2e_label_filter }} | |
| E2E_REPORT_NAME: ${{ matrix.e2e_report_name }} | |
| run: | | |
| docker run --rm \ | |
| --network host \ | |
| -v "${GITHUB_WORKSPACE}:${{ env.CI_WORKDIR }}" \ | |
| -v /var/run/docker.sock:/var/run/docker.sock \ | |
| -w "${{ env.CI_WORKDIR }}" \ | |
| -e PROJECT_IMAGE=${{ env.PROJECT_IMAGE }} \ | |
| -e IMAGE_DELIVERY_MODE=${{ env.IMAGE_DELIVERY_MODE }} \ | |
| -e E2E_GINKGO_PROCS=${{ matrix.e2e_ginkgo_procs }} \ | |
| -e E2E_LABEL_FILTER \ | |
| -e E2E_REPORT_NAME \ | |
| -e HOST_PROJECT_PATH=${{ github.workspace }} \ | |
| -e KUBECONFIG=${{ env.CI_WORKDIR }}/.stamps/cluster/k3d-gitops-reverser-test-e2e/.kube/config \ | |
| ${{ env.CI_CONTAINER }} \ | |
| bash -c " | |
| git config --global --add safe.directory ${{ env.CI_WORKDIR }} | |
| ${{ matrix.script }} | |
| " | |
| - name: Collect E2E coverage | |
| if: matrix.coverage == '1' | |
| # Fail-safe: a coverage hiccup must never fail the e2e gate. | |
| run: | | |
| docker run --rm \ | |
| --network host \ | |
| -v "${GITHUB_WORKSPACE}:${{ env.CI_WORKDIR }}" \ | |
| -v /var/run/docker.sock:/var/run/docker.sock \ | |
| -w "${{ env.CI_WORKDIR }}" \ | |
| -e KUBECONFIG=${{ env.CI_WORKDIR }}/.stamps/cluster/k3d-gitops-reverser-test-e2e/.kube/config \ | |
| ${{ env.CI_CONTAINER }} \ | |
| bash -c " | |
| git config --global --add safe.directory ${{ env.CI_WORKDIR }} | |
| task e2e-coverage-collect | |
| " || echo "e2e coverage collection failed (non-fatal)" | |
| - name: Upload E2E coverage to Codecov | |
| if: matrix.coverage == '1' | |
| uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1 | |
| with: | |
| files: e2e-cover.out | |
| flags: e2e | |
| use_oidc: true | |
| fail_ci_if_error: false | |
| - name: Report disk usage (peak + final) | |
| if: always() | |
| run: | | |
| kill "${SAMPLER_PID}" 2>/dev/null || true | |
| samples="df-samples-${{ matrix.name }}.log" | |
| { | |
| echo "### Disk usage (${{ matrix.name }})" | |
| echo | |
| echo "Lowest free space on \`/\` during the run:" | |
| echo '```' | |
| sort -t= -k2 -n "${samples}" 2>/dev/null | head -1 || echo "(no samples captured)" | |
| echo '```' | |
| echo "Final state:" | |
| echo '```' | |
| df -h / | |
| echo | |
| docker system df | |
| echo '```' | |
| echo "<details><summary>Full df sample timeline</summary>" | |
| echo | |
| echo '```' | |
| cat "${samples}" 2>/dev/null || echo "(no samples captured)" | |
| echo '```' | |
| echo "</details>" | |
| } | tee -a "$GITHUB_STEP_SUMMARY" | |
| - name: Make E2E artifacts readable by the runner | |
| if: always() | |
| # The E2E suite runs in a root container that mounts the workspace, so | |
| # everything it writes under .stamps/ (e.g. e2e-repo-artifacts/) is | |
| # root-owned with restrictive perms. The upload step below runs on the | |
| # host as the non-root runner user and fails with EACCES when its glob | |
| # tries to scandir those dirs. Relax perms so the runner can traverse | |
| # and read them. a+rX adds read for files and traverse for dirs only. | |
| run: sudo chmod -R a+rX .stamps || true | |
| - name: Upload E2E raw Ginkgo JSON reports | |
| if: always() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: e2e-ginkgo-reports-${{ matrix.name }} | |
| path: .stamps/**/ginkgo-report-*.json | |
| # Reports live under the dot-dir .stamps/, which upload-artifact treats | |
| # as hidden and excludes by default — without this the upload silently | |
| # finds no files. See the "No files were found" gotcha in upload-artifact v4+. | |
| include-hidden-files: true | |
| if-no-files-found: error | |
| retention-days: 60 |