Skip to content

Decouple :latest publish from release promotion + fork-safe CI [PLA-828] #2

Decouple :latest publish from release promotion + fork-safe CI [PLA-828]

Decouple :latest publish from release promotion + fork-safe CI [PLA-828] #2

Workflow file for this run

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