|
1 | 1 | name: CI/CD Pipeline |
2 | 2 |
|
| 3 | +# CI/CD workflow for building, publishing, mirroring, signing container images and building release binaries. |
| 4 | +# Actions are pinned to specific SHAs to reduce supply-chain risk. This workflow triggers on tag push events. |
| 5 | + |
3 | 6 | permissions: |
4 | 7 | contents: read |
| 8 | + packages: write # for GHCR push |
| 9 | + id-token: write # for Cosign Keyless (OIDC) Signing |
| 10 | + |
| 11 | +# Required secrets: |
| 12 | +# - DOCKER_HUB_USERNAME / DOCKER_HUB_ACCESS_TOKEN: push to Docker Hub |
| 13 | +# - GITHUB_TOKEN: used for GHCR login and OIDC keyless signing |
| 14 | +# - COSIGN_PRIVATE_KEY / COSIGN_PASSWORD / COSIGN_PUBLIC_KEY: for key-based signing |
5 | 15 |
|
6 | 16 | on: |
7 | | - push: |
8 | | - tags: |
9 | | - - "*" |
| 17 | + push: |
| 18 | + tags: |
| 19 | + - "*" |
| 20 | + |
| 21 | +concurrency: |
| 22 | + group: ${{ github.ref }} |
| 23 | + cancel-in-progress: true |
10 | 24 |
|
11 | 25 | jobs: |
12 | | - release: |
13 | | - name: Build and Release |
14 | | - runs-on: amd64-runner |
15 | | - |
16 | | - steps: |
17 | | - - name: Checkout code |
18 | | - uses: actions/checkout@v5 |
19 | | - |
20 | | - - name: Set up QEMU |
21 | | - uses: docker/setup-qemu-action@v3 |
22 | | - |
23 | | - - name: Set up Docker Buildx |
24 | | - uses: docker/setup-buildx-action@v3 |
25 | | - |
26 | | - - name: Log in to Docker Hub |
27 | | - uses: docker/login-action@v3 |
28 | | - with: |
29 | | - username: ${{ secrets.DOCKER_HUB_USERNAME }} |
30 | | - password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} |
31 | | - |
32 | | - - name: Extract tag name |
33 | | - id: get-tag |
34 | | - run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV |
35 | | - |
36 | | - - name: Install Go |
37 | | - uses: actions/setup-go@v6 |
38 | | - with: |
39 | | - go-version: 1.25 |
40 | | - |
41 | | - - name: Update version in main.go |
42 | | - run: | |
43 | | - TAG=${{ env.TAG }} |
44 | | - if [ -f main.go ]; then |
45 | | - sed -i 's/version_replaceme/'"$TAG"'/' main.go |
46 | | - echo "Updated main.go with version $TAG" |
47 | | - else |
48 | | - echo "main.go not found" |
49 | | - fi |
50 | | -
|
51 | | - - name: Build and push Docker images |
52 | | - run: | |
53 | | - TAG=${{ env.TAG }} |
54 | | - make docker-build-release tag=$TAG |
55 | | -
|
56 | | - - name: Build binaries |
57 | | - run: | |
58 | | - make go-build-release |
59 | | -
|
60 | | - - name: Upload artifacts from /bin |
61 | | - uses: actions/upload-artifact@v4 |
62 | | - with: |
63 | | - name: binaries |
64 | | - path: bin/ |
| 26 | + release: |
| 27 | + name: Build and Release |
| 28 | + runs-on: amd64-runner |
| 29 | + # Job-level timeout to avoid runaway or stuck runs |
| 30 | + timeout-minutes: 120 |
| 31 | + env: |
| 32 | + # Target images |
| 33 | + DOCKERHUB_IMAGE: docker.io/${{ secrets.DOCKER_HUB_USERNAME }}/${{ github.event.repository.name }} |
| 34 | + GHCR_IMAGE: ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }} |
| 35 | + |
| 36 | + steps: |
| 37 | + - name: Checkout code |
| 38 | + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 |
| 39 | + |
| 40 | + - name: Set up QEMU |
| 41 | + uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 |
| 42 | + |
| 43 | + - name: Set up Docker Buildx |
| 44 | + uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 |
| 45 | + |
| 46 | + - name: Log in to Docker Hub |
| 47 | + uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 |
| 48 | + with: |
| 49 | + registry: docker.io |
| 50 | + username: ${{ secrets.DOCKER_HUB_USERNAME }} |
| 51 | + password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} |
| 52 | + |
| 53 | + - name: Extract tag name |
| 54 | + id: get-tag |
| 55 | + run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV |
| 56 | + shell: bash |
| 57 | + |
| 58 | + - name: Install Go |
| 59 | + uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 |
| 60 | + with: |
| 61 | + go-version: 1.25 |
| 62 | + |
| 63 | + - name: Update version in main.go |
| 64 | + run: | |
| 65 | + TAG=${{ env.TAG }} |
| 66 | + if [ -f main.go ]; then |
| 67 | + sed -i 's/version_replaceme/'"$TAG"'/' main.go |
| 68 | + echo "Updated main.go with version $TAG" |
| 69 | + else |
| 70 | + echo "main.go not found" |
| 71 | + fi |
| 72 | + shell: bash |
| 73 | + |
| 74 | + - name: Build and push Docker images (Docker Hub) |
| 75 | + run: | |
| 76 | + TAG=${{ env.TAG }} |
| 77 | + make docker-build-release tag=$TAG |
| 78 | + echo "Built & pushed to: ${{ env.DOCKERHUB_IMAGE }}:${TAG}" |
| 79 | + shell: bash |
| 80 | + |
| 81 | + - name: Login in to GHCR |
| 82 | + uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 |
| 83 | + with: |
| 84 | + registry: ghcr.io |
| 85 | + username: ${{ github.actor }} |
| 86 | + password: ${{ secrets.GITHUB_TOKEN }} |
| 87 | + |
| 88 | + - name: Install skopeo + jq |
| 89 | + # skopeo: copy/inspect images between registries |
| 90 | + # jq: JSON parsing tool used to extract digest values |
| 91 | + run: | |
| 92 | + sudo apt-get update -y |
| 93 | + sudo apt-get install -y skopeo jq |
| 94 | + skopeo --version |
| 95 | + shell: bash |
| 96 | + |
| 97 | + - name: Copy tag from Docker Hub to GHCR |
| 98 | + # Mirror the already-built image (all architectures) to GHCR so we can sign it |
| 99 | + run: | |
| 100 | + set -euo pipefail |
| 101 | + TAG=${{ env.TAG }} |
| 102 | + echo "Copying ${{ env.DOCKERHUB_IMAGE }}:${TAG} -> ${{ env.GHCR_IMAGE }}:${TAG}" |
| 103 | + skopeo copy --all --retry-times 3 \ |
| 104 | + docker://$DOCKERHUB_IMAGE:$TAG \ |
| 105 | + docker://$GHCR_IMAGE:$TAG |
| 106 | + shell: bash |
| 107 | + |
| 108 | + - name: Install cosign |
| 109 | + # cosign is used to sign and verify container images (key and keyless) |
| 110 | + uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0 |
| 111 | + |
| 112 | + - name: Dual-sign and verify (GHCR & Docker Hub) |
| 113 | + # Sign each image by digest using keyless (OIDC) and key-based signing, |
| 114 | + # then verify both the public key signature and the keyless OIDC signature. |
| 115 | + env: |
| 116 | + TAG: ${{ env.TAG }} |
| 117 | + COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }} |
| 118 | + COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }} |
| 119 | + COSIGN_PUBLIC_KEY: ${{ secrets.COSIGN_PUBLIC_KEY }} |
| 120 | + COSIGN_YES: "true" |
| 121 | + run: | |
| 122 | + set -euo pipefail |
| 123 | +
|
| 124 | + issuer="https://token.actions.githubusercontent.com" |
| 125 | + id_regex="^https://github.com/${{ github.repository }}/.+" # accept this repo (all workflows/refs) |
| 126 | +
|
| 127 | + for IMAGE in "${GHCR_IMAGE}" "${DOCKERHUB_IMAGE}"; do |
| 128 | + echo "Processing ${IMAGE}:${TAG}" |
| 129 | +
|
| 130 | + DIGEST="$(skopeo inspect --retry-times 3 docker://${IMAGE}:${TAG} | jq -r '.Digest')" |
| 131 | + REF="${IMAGE}@${DIGEST}" |
| 132 | + echo "Resolved digest: ${REF}" |
| 133 | +
|
| 134 | + echo "==> cosign sign (keyless) --recursive ${REF}" |
| 135 | + cosign sign --recursive "${REF}" |
| 136 | +
|
| 137 | + echo "==> cosign sign (key) --recursive ${REF}" |
| 138 | + cosign sign --key env://COSIGN_PRIVATE_KEY --recursive "${REF}" |
| 139 | +
|
| 140 | + echo "==> cosign verify (public key) ${REF}" |
| 141 | + cosign verify --key env://COSIGN_PUBLIC_KEY "${REF}" -o text |
| 142 | +
|
| 143 | + echo "==> cosign verify (keyless policy) ${REF}" |
| 144 | + cosign verify \ |
| 145 | + --certificate-oidc-issuer "${issuer}" \ |
| 146 | + --certificate-identity-regexp "${id_regex}" \ |
| 147 | + "${REF}" -o text |
| 148 | + done |
| 149 | + shell: bash |
| 150 | + |
| 151 | + - name: Build binaries |
| 152 | + run: | |
| 153 | + make go-build-release |
| 154 | + shell: bash |
| 155 | + |
| 156 | + - name: Upload artifacts from /bin |
| 157 | + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 |
| 158 | + with: |
| 159 | + name: binaries |
| 160 | + path: bin/ |
0 commit comments