Decouple :latest publish from release promotion + fork-safe CI [PLA-828] #2
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
| name: ci | |
| on: | |
| push: | |
| branches: ["develop"] | |
| pull_request: | |
| branches: ["develop"] | |
| types: [opened, reopened, synchronize] | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| jobs: | |
| test: | |
| name: unit tests | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: "[preparation] checkout the current branch" | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: "[preparation] set up golang" | |
| uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 | |
| with: | |
| go-version-file: go.mod | |
| cache-dependency-path: go.sum | |
| # `make test` depends on `deps`, which downloads the embedded binaries that | |
| # internal/core/embedded references via //go:embed. Without them the test | |
| # binary fails to compile. gcc (for CGO/SQLite) is preinstalled on the runner. | |
| - name: "[execution] run unit tests with coverage" | |
| run: make test GOOS=linux GOARCH=amd64 | |
| - name: "[reporting] coverage summary" | |
| if: always() | |
| run: | | |
| if [[ -f coverage.out ]]; then | |
| echo "### Coverage" >> "$GITHUB_STEP_SUMMARY" | |
| echo '```' >> "$GITHUB_STEP_SUMMARY" | |
| go tool cover -func=coverage.out | tail -n 1 >> "$GITHUB_STEP_SUMMARY" | |
| echo '```' >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| - name: "[reporting] upload coverage artifact" | |
| if: always() | |
| uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 | |
| with: | |
| name: coverage | |
| path: coverage.out | |
| retention-days: 30 | |
| if-no-files-found: ignore | |
| lint: | |
| name: lint | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: "[preparation] checkout the current branch" | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: "[preparation] set up golang" | |
| uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 | |
| with: | |
| go-version-file: go.mod | |
| cache-dependency-path: go.sum | |
| # golangci-lint type-checks the packages it analyses, so the embedded | |
| # binaries must exist first (same //go:embed reason as the test job). | |
| - name: "[preparation] download embedded deps" | |
| run: make deps GOOS=linux GOARCH=amd64 | |
| # golangci-lint-action v9 pairs with golangci-lint v2 (matches the | |
| # `version: "2"` schema in .golangci.yml). Pinned to a commit SHA like the | |
| # repo's other actions; golangci-lint itself pinned for reproducible CI. | |
| - name: "[execution] run golangci-lint" | |
| uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9.3.0 | |
| with: | |
| version: v2.12.2 | |
| build: | |
| # online: standard binary that pulls OCI images at runtime. | |
| # offline: air-gapped binary with all OCI images embedded (built via the | |
| # `-offline` make targets, which mirror release.yaml). Both glibc and musl | |
| # are built for amd64/arm64 so every published variant is exercised on PRs. | |
| runs-on: ubuntu-latest | |
| strategy: | |
| matrix: | |
| goos: [linux] | |
| goarch: [amd64, arm64] | |
| libc: [glibc, musl] | |
| variant: [online, offline] | |
| steps: | |
| - name: "[preparation] checkout the current branch" | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: "[preparation] set up golang" | |
| uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 | |
| with: | |
| go-version-file: go.mod | |
| cache-dependency-path: go.sum | |
| - name: "[preparation] install cross-compilation toolchains" | |
| run: sudo make install-cross-compilers${{ matrix.libc == 'musl' && ' install-musl-cross-compilers' || '' }} | |
| - name: "[preparation] resolve build target" | |
| id: target | |
| run: | | |
| TARGET="build" | |
| if [[ "${{ matrix.libc }}" == "musl" ]]; then TARGET="build-musl"; fi | |
| if [[ "${{ matrix.variant }}" == "offline" ]]; then TARGET="${TARGET}-offline"; fi | |
| echo "build_target=${TARGET}" >> "$GITHUB_OUTPUT" | |
| - name: "[execution] build the binary for ${{ matrix.goos }}/${{ matrix.goarch }} (${{ matrix.libc }}, ${{ matrix.variant }})" | |
| run: make ${{ steps.target.outputs.build_target }} GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} | |
| - name: "[execution] upload artifact" | |
| uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 | |
| with: | |
| name: kubesolo-${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.libc == 'musl' && '-musl' || '' }}${{ matrix.variant == 'offline' && '-offline' || '' }} | |
| path: ./dist/kubesolo | |
| retention-days: 30 | |
| build-kubesoloctl: | |
| name: build kubesoloctl-${{ matrix.goos }}-${{ matrix.goarch }} | |
| runs-on: ubuntu-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - goos: linux | |
| goarch: amd64 | |
| - goos: linux | |
| goarch: arm64 | |
| - goos: darwin | |
| goarch: amd64 | |
| - goos: darwin | |
| goarch: arm64 | |
| steps: | |
| - name: "[preparation] checkout the current branch" | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: "[preparation] set up golang" | |
| uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 | |
| with: | |
| go-version-file: go.mod | |
| cache-dependency-path: go.sum | |
| - name: "[execution] build kubesoloctl for ${{ matrix.goos }}/${{ matrix.goarch }}" | |
| run: | | |
| make build-kubesoloctl \ | |
| GOOS=${{ matrix.goos }} \ | |
| GOARCH=${{ matrix.goarch }} \ | |
| KUBESOLOCTL_OUTPUT=./dist/kubesoloctl-${{ matrix.goos }}-${{ matrix.goarch }} | |
| - name: "[execution] upload artifact" | |
| uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 | |
| with: | |
| name: kubesoloctl-${{ matrix.goos }}-${{ matrix.goarch }} | |
| path: ./dist/kubesoloctl-${{ matrix.goos }}-${{ matrix.goarch }} | |
| retention-days: 30 | |
| # Uploads the PR number as a build-independent artifact so ci-publish.yml can | |
| # validate it strictly, rather than trusting workflow_run.pull_requests[0] | |
| # (unreliable for fork-originated runs). | |
| resolve-tag: | |
| name: resolve-tag | |
| if: github.event_name == 'pull_request' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: "[preparation] resolve pr image tag" | |
| run: echo "pr-${{ github.event.pull_request.number }}" > /tmp/image-tag.txt | |
| - name: "[post] upload image tag artifact" | |
| uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 | |
| with: | |
| name: pr-image-tag | |
| path: /tmp/image-tag.txt | |
| retention-days: 1 | |
| docker-build: | |
| runs-on: ubuntu-latest | |
| strategy: | |
| matrix: | |
| goarch: [amd64, arm64] | |
| steps: | |
| - name: "[preparation] checkout the current branch" | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: "[preparation] set up golang" | |
| uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 | |
| with: | |
| go-version-file: go.mod | |
| cache-dependency-path: go.sum | |
| - name: "[preparation] install cross-compilation toolchains" | |
| run: sudo make install-cross-compilers install-musl-cross-compilers | |
| - name: "[execution] build linux/${{ matrix.goarch }} musl binary" | |
| run: make build-musl GOOS=linux GOARCH=${{ matrix.goarch }} | |
| - name: "[preparation] resolve image tag" | |
| id: image | |
| run: | | |
| if [[ "${{ github.event_name }}" == "pull_request" ]]; then | |
| echo "tag=pr-${{ github.event.pull_request.number }}" >> "$GITHUB_OUTPUT" | |
| elif [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" == "refs/heads/develop" ]]; then | |
| echo "push=true" >> "$GITHUB_OUTPUT" | |
| echo "tags=portainerci/kubesolo:develop-linux-${{ matrix.goarch }}" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "push=false" >> "$GITHUB_OUTPUT" | |
| echo "tags=portainerci/kubesolo:ci-linux-${{ matrix.goarch }}" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: "[preparation] set up QEMU for cross-platform Docker builds" | |
| if: matrix.goarch == 'arm64' | |
| uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0 | |
| with: | |
| platforms: linux/arm64 | |
| - name: "[preparation] set up docker buildx" | |
| uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 | |
| # No secrets on pull_request — untrusted PR code must never see them; ci-publish.yml pushes afterwards. | |
| - name: "[execution] build linux/${{ matrix.goarch }} image and export as oci tarball" | |
| if: github.event_name == 'pull_request' | |
| uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0 | |
| with: | |
| context: . | |
| platforms: linux/${{ matrix.goarch }} | |
| push: false | |
| tags: kubesolo:${{ steps.image.outputs.tag }}-linux-${{ matrix.goarch }} | |
| outputs: type=oci,dest=/tmp/image-${{ matrix.goarch }}.tar | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| - name: "[post] upload image artifact" | |
| if: github.event_name == 'pull_request' | |
| uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 | |
| with: | |
| name: pr-image-linux-${{ matrix.goarch }} | |
| path: /tmp/image-${{ matrix.goarch }}.tar | |
| retention-days: 1 | |
| - name: "[preparation] log in to docker hub" | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/develop' | |
| uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 | |
| with: | |
| registry: docker.io | |
| username: ${{ secrets.DOCKER_HUB_USERNAME }} | |
| password: ${{ secrets.DOCKER_HUB_PASSWORD }} | |
| - name: "[execution] build docker image for linux/${{ matrix.goarch }}" | |
| if: github.event_name != 'pull_request' | |
| uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0 | |
| with: | |
| context: . | |
| platforms: linux/${{ matrix.goarch }} | |
| push: ${{ steps.image.outputs.push }} | |
| tags: ${{ steps.image.outputs.tags }} | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| docker-manifest: | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/develop' | |
| runs-on: ubuntu-latest | |
| needs: [docker-build] | |
| steps: | |
| - name: "[preparation] log in to docker hub" | |
| uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 | |
| with: | |
| registry: docker.io | |
| username: ${{ secrets.DOCKER_HUB_USERNAME }} | |
| password: ${{ secrets.DOCKER_HUB_PASSWORD }} | |
| - name: "[preparation] set up docker buildx" | |
| uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 | |
| - name: "[execution] create and push multi-arch manifest" | |
| run: | | |
| docker buildx imagetools create \ | |
| -t portainerci/kubesolo:develop \ | |
| portainerci/kubesolo:develop-linux-amd64 \ | |
| portainerci/kubesolo:develop-linux-arm64 | |
| # ── End-to-end smoke (develop pushes only) ────────────────────────────────── | |
| # Boots the just-built image as a container on a NATIVE runner (no QEMU — a | |
| # full Kubernetes boot under emulation is far too slow) and runs the smoke | |
| # suite via kubesoloctl. PR-time e2e (amd64 only, to keep arm64 minutes down) | |
| # now runs from ci-publish.yml instead, after the PR image is safely | |
| # published — see that file's e2e-amd64 job. | |
| e2e-amd64: | |
| name: e2e (amd64) | |
| needs: [docker-manifest] | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/develop' | |
| runs-on: ubuntu-24.04 | |
| steps: | |
| - name: "[preparation] checkout the current branch" | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: "[preparation] set up golang" | |
| uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 | |
| with: | |
| go-version-file: go.mod | |
| cache-dependency-path: go.sum | |
| - name: "[preparation] build kubesoloctl" | |
| run: make build-kubesoloctl GOOS=linux GOARCH=amd64 KUBESOLOCTL_OUTPUT=./dist/kubesoloctl | |
| - name: "[preparation] install kubectl" | |
| run: | | |
| curl -fsSLo /usr/local/bin/kubectl https://dl.k8s.io/release/v1.34.8/bin/linux/amd64/kubectl | |
| chmod +x /usr/local/bin/kubectl | |
| - name: "[execution] run e2e smoke suite" | |
| run: IMAGE=portainerci/kubesolo:develop KUBESOLOCTL=./dist/kubesoloctl test/e2e/run.sh | |
| e2e-arm64: | |
| name: e2e (arm64) | |
| needs: [docker-manifest] | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/develop' | |
| runs-on: ubuntu-24.04-arm | |
| steps: | |
| - name: "[preparation] checkout the current branch" | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: "[preparation] set up golang" | |
| uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 | |
| with: | |
| go-version-file: go.mod | |
| cache-dependency-path: go.sum | |
| - name: "[preparation] build kubesoloctl" | |
| run: make build-kubesoloctl GOOS=linux GOARCH=arm64 KUBESOLOCTL_OUTPUT=./dist/kubesoloctl | |
| - name: "[preparation] install kubectl" | |
| run: | | |
| curl -fsSLo /usr/local/bin/kubectl https://dl.k8s.io/release/v1.34.8/bin/linux/arm64/kubectl | |
| chmod +x /usr/local/bin/kubectl | |
| - name: "[execution] run e2e smoke suite" | |
| run: IMAGE=portainerci/kubesolo:develop KUBESOLOCTL=./dist/kubesoloctl test/e2e/run.sh |