From dc5d90f387415cca279027fbab2e73d353d0fc92 Mon Sep 17 00:00:00 2001 From: Owen Date: Tue, 17 Mar 2026 16:58:20 -0700 Subject: [PATCH 1/5] Clarify default --- cmd/up/client/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/up/client/client.go b/cmd/up/client/client.go index 6c9cddd..f497abb 100644 --- a/cmd/up/client/client.go +++ b/cmd/up/client/client.go @@ -120,7 +120,7 @@ func ClientUpCmd() *cobra.Command { cmd.Flags().StringVar(&opts.OrgID, "org", "", "Organization ID (default: selected organization if logged in)") cmd.Flags().StringVar(&opts.Endpoint, "endpoint", "", "Client endpoint (required if not logged in)") cmd.Flags().IntVar(&opts.MTU, "mtu", 1280, "Maximum transmission unit") - cmd.Flags().StringVar(&opts.DNS, "netstack-dns", defaultDNSServer, "DNS `server` to use for Netstack") + cmd.Flags().StringVar(&opts.DNS, "netstack-dns", defaultDNSServer, "DNS `server` to use for Netstack. This handles DNS resolution outside of the upstream servers.") cmd.Flags().StringVar(&opts.InterfaceName, "interface-name", "pangolin", "Interface `name`") cmd.Flags().StringVar(&opts.LogLevel, "log-level", "info", "Log level") cmd.Flags().StringVar(&opts.HTTPAddr, "http-addr", "", "HTTP address for API server") From 351c51a16fe12455a3fd915b7e6b0cb4a35b1b39 Mon Sep 17 00:00:00 2001 From: Owen Date: Tue, 17 Mar 2026 17:00:25 -0700 Subject: [PATCH 2/5] Bump newt --- go.mod | 18 +++++++++--------- go.sum | 32 ++++++++++++++++---------------- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/go.mod b/go.mod index 7724558..d1f726a 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/fosrl/cli -go 1.25 +go 1.25.0 require ( github.com/Masterminds/semver/v3 v3.4.0 @@ -8,15 +8,15 @@ require ( github.com/charmbracelet/huh v0.8.0 github.com/charmbracelet/lipgloss v1.1.0 github.com/creack/pty v1.1.24 - github.com/fosrl/newt v1.10.1 + github.com/fosrl/newt v1.10.3 github.com/fosrl/olm v1.4.3 github.com/mattn/go-isatty v0.0.20 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/spf13/cobra v1.10.2 github.com/spf13/viper v1.21.0 - golang.org/x/crypto v0.46.0 - golang.org/x/sys v0.40.0 - golang.org/x/term v0.38.0 + golang.org/x/crypto v0.48.0 + golang.org/x/sys v0.41.0 + golang.org/x/term v0.40.0 ) require ( @@ -61,12 +61,12 @@ require ( github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 // indirect - golang.org/x/mod v0.31.0 // indirect - golang.org/x/net v0.48.0 // indirect + golang.org/x/mod v0.32.0 // indirect + golang.org/x/net v0.51.0 // indirect golang.org/x/sync v0.19.0 // indirect - golang.org/x/text v0.32.0 // indirect + golang.org/x/text v0.34.0 // indirect golang.org/x/time v0.12.0 // indirect - golang.org/x/tools v0.40.0 // indirect + golang.org/x/tools v0.41.0 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 // indirect diff --git a/go.sum b/go.sum index 4dc5be1..b53e7df 100644 --- a/go.sum +++ b/go.sum @@ -50,8 +50,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= -github.com/fosrl/newt v1.10.1 h1:3RZqYhJDDBWZHViVeDl4hWwWVJFtvFJB+wL+AF99neA= -github.com/fosrl/newt v1.10.1/go.mod h1:d1+yYMnKqg4oLqAM9zdbjthjj2FQEVouiACjqU468ck= +github.com/fosrl/newt v1.10.3 h1:JO9gFK9LP/w2EeDIn4wU+jKggAFPo06hX5hxFSETqcw= +github.com/fosrl/newt v1.10.3/go.mod h1:iYuuCAG7iabheiogMOX87r61uQN31S39nKxMKRuLS+s= github.com/fosrl/olm v1.4.3 h1:hmAWfrJzpiwtvzw/B6xmCmeqK1OW0BJ2FUbEa7ztlmU= github.com/fosrl/olm v1.4.3/go.mod h1:aC1oieI0tadd66zY7RDjXT3PPsR54mYS0FYMsHqFqs8= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= @@ -132,14 +132,14 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavM github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= -golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 h1:zfMcR1Cs4KNuomFFgGefv5N0czO2XZpUbxGUy8i8ug0= golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0= -golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= -golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= -golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= -golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= +golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= +golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= +golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -147,16 +147,16 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= -golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= -golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= -golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= -golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= +golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= -golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= -golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= +golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= +golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A= From a8e4b482d417804a86d8f8ed9160fa171d2e962a Mon Sep 17 00:00:00 2001 From: Owen Date: Tue, 17 Mar 2026 17:10:40 -0700 Subject: [PATCH 3/5] Update olm --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index d1f726a..32a8090 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/charmbracelet/lipgloss v1.1.0 github.com/creack/pty v1.1.24 github.com/fosrl/newt v1.10.3 - github.com/fosrl/olm v1.4.3 + github.com/fosrl/olm v1.4.4-0.20260317182904-31eed749330e github.com/mattn/go-isatty v0.0.20 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/spf13/cobra v1.10.2 diff --git a/go.sum b/go.sum index b53e7df..1ac61ce 100644 --- a/go.sum +++ b/go.sum @@ -52,8 +52,8 @@ github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6 github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/fosrl/newt v1.10.3 h1:JO9gFK9LP/w2EeDIn4wU+jKggAFPo06hX5hxFSETqcw= github.com/fosrl/newt v1.10.3/go.mod h1:iYuuCAG7iabheiogMOX87r61uQN31S39nKxMKRuLS+s= -github.com/fosrl/olm v1.4.3 h1:hmAWfrJzpiwtvzw/B6xmCmeqK1OW0BJ2FUbEa7ztlmU= -github.com/fosrl/olm v1.4.3/go.mod h1:aC1oieI0tadd66zY7RDjXT3PPsR54mYS0FYMsHqFqs8= +github.com/fosrl/olm v1.4.4-0.20260317182904-31eed749330e h1:zizYDC6zUMfuKcOJbMFRvcJgSVYVYcI4ElqF5J2dV84= +github.com/fosrl/olm v1.4.4-0.20260317182904-31eed749330e/go.mod h1:SxeQNFngDMgwrXpTWjt1gheyoZo2TltmNrQ2vXX7YT4= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= From 064a04a102d5554300538754b194fac9a78394fe Mon Sep 17 00:00:00 2001 From: Owen Date: Tue, 17 Mar 2026 17:56:40 -0700 Subject: [PATCH 4/5] Update cicd flow --- .github/workflows/cicd.yml | 953 ++++++++++++++++++++++++------------- Dockerfile | 5 +- Makefile | 27 +- internal/version/consts.go | 7 +- 4 files changed, 655 insertions(+), 337 deletions(-) diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index f6ec4be..3082333 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -1,9 +1,19 @@ -name: CI/CD Pipeline +name: CI/CD Pipeline (AWS Self-Hosted Runners) + +# CI/CD workflow for building, publishing, attesting, signing container images and building release binaries. +# Native multi-arch pipeline using two AWS EC2 self-hosted runners (x86_64 + arm64) to build and push architecture-specific images in parallel, then create multi-arch manifests. +# +# Required secrets: +# - AWS_ACCOUNT_ID, AWS_ROLE_NAME, AWS_REGION +# - EC2_INSTANCE_ID_AMD_RUNNER, EC2_INSTANCE_ID_ARM_RUNNER +# - DOCKER_HUB_USERNAME / DOCKER_HUB_ACCESS_TOKEN +# - GITHUB_TOKEN +# - COSIGN_PRIVATE_KEY / COSIGN_PASSWORD / COSIGN_PUBLIC_KEY permissions: contents: write # gh-release packages: write # GHCR push - id-token: write # Keyless-Signatures & Attestations + id-token: write # Keyless-Signatures & Attestations (OIDC) attestations: write # actions/attest-build-provenance security-events: write # upload-sarif actions: read @@ -17,16 +27,16 @@ on: workflow_dispatch: inputs: version: - description: "SemVer version to release (e.g., 1.2.3, no leading 'v')" + description: "Version to release (X.Y.Z or X.Y.Z-rc.N)" required: true type: string publish_latest: - description: "Also publish the 'latest' image tag" + description: "Publish latest tag (non-RC only)" required: true type: boolean default: false publish_minor: - description: "Also publish the 'major.minor' image tag (e.g., 1.2)" + description: "Publish minor tag (X.Y) (non-RC only)" required: true type: boolean default: false @@ -40,10 +50,47 @@ concurrency: cancel-in-progress: true jobs: + # --------------------------------------------------------------------------- + # 1) Start AWS EC2 runner instances + # --------------------------------------------------------------------------- + pre-run: + name: Start AWS EC2 runners + runs-on: ubuntu-latest + permissions: write-all + outputs: + image_created: ${{ steps.created.outputs.image_created }} + steps: + - name: Capture created timestamp (shared) + id: created + shell: bash + run: | + set -euo pipefail + echo "image_created=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT" + + - name: Configure AWS credentials (OIDC) + uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v6.0.0 + with: + role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }} + role-duration-seconds: 3600 + aws-region: ${{ secrets.AWS_REGION }} + + - name: Verify AWS identity + run: aws sts get-caller-identity + + - name: Start EC2 instances + run: | + aws ec2 start-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_AMD_RUNNER }} + aws ec2 start-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_ARM_RUNNER }} + echo "EC2 instances started" + + # --------------------------------------------------------------------------- + # 2) Prepare release + # --------------------------------------------------------------------------- prepare: if: github.event_name == 'workflow_dispatch' name: Prepare release (create tag) - runs-on: ubuntu-24.04 + needs: [pre-run] + runs-on: [self-hosted, linux, x64] permissions: contents: write steps: @@ -62,6 +109,7 @@ jobs: echo "Invalid version: $INPUT_VERSION (expected X.Y.Z or X.Y.Z-rc.N)" >&2 exit 1 fi + - name: Create and push tag shell: bash env: @@ -81,14 +129,27 @@ jobs: fi git tag -a "$VERSION" -m "Release $VERSION" git push origin "refs/tags/$VERSION" - release: - if: ${{ github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.actor != 'github-actions[bot]') }} - name: Build and Release - runs-on: ubuntu-24.04 + + # --------------------------------------------------------------------------- + # 3) Build and Release (x86 job) + # --------------------------------------------------------------------------- + build-amd: + name: Build image (linux/amd64) + needs: [pre-run, prepare] + if: ${{ needs.pre-run.result == 'success' && ((github.event_name == 'push' && github.actor != 'github-actions[bot]' && needs.prepare.result == 'skipped') || (github.event_name == 'workflow_dispatch' && (needs.prepare.result == 'success' || needs.prepare.result == 'skipped'))) }} + runs-on: [self-hosted, linux, x64] timeout-minutes: 120 env: - DOCKERHUB_IMAGE: docker.io/fosrl/pangolin-${{ github.event.repository.name }} + DOCKERHUB_IMAGE: docker.io/${{ github.repository_owner }}/${{ github.event.repository.name }} GHCR_IMAGE: ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }} + IMAGE_LICENSE: ${{ github.event.repository.license.spdx_id || 'NOASSERTION' }} + IMAGE_CREATED: ${{ needs.pre-run.outputs.image_created }} + + outputs: + tag: ${{ steps.tag.outputs.tag }} + is_rc: ${{ steps.tag.outputs.is_rc }} + major: ${{ steps.tag.outputs.major }} + minor: ${{ steps.tag.outputs.minor }} steps: - name: Checkout code @@ -96,64 +157,190 @@ jobs: with: fetch-depth: 0 - - name: Capture created timestamp - run: echo "IMAGE_CREATED=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> $GITHUB_ENV + - name: Monitor storage space shell: bash + run: | + THRESHOLD=75 + USED_SPACE=$(df / | grep / | awk '{ print $5 }' | sed 's/%//g') + echo "Used space: $USED_SPACE%" + if [ "$USED_SPACE" -ge "$THRESHOLD" ]; then + echo "Disk usage >= ${THRESHOLD}%, pruning docker..." + echo y | docker system prune -a || true + else + echo "Disk usage < ${THRESHOLD}%, no action needed." + fi - - name: Set up QEMU - uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 + - name: Determine tag + rc/major/minor + id: tag + shell: bash + env: + EVENT_NAME: ${{ github.event_name }} + INPUT_VERSION: ${{ inputs.version }} + run: | + set -euo pipefail + if [ "$EVENT_NAME" = "workflow_dispatch" ]; then + TAG="$INPUT_VERSION" + else + TAG="${{ github.ref_name }}" + fi - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 + if ! [[ "$TAG" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-rc\.[0-9]+)?$ ]]; then + echo "Invalid tag: $TAG" >&2 + exit 1 + fi + + IS_RC="false" + if [[ "$TAG" =~ -rc\.[0-9]+$ ]]; then + IS_RC="true" + fi + + MAJOR="$(echo "$TAG" | cut -d. -f1)" + MINOR="$(echo "$TAG" | cut -d. -f1,2)" + + echo "tag=$TAG" >> "$GITHUB_OUTPUT" + echo "is_rc=$IS_RC" >> "$GITHUB_OUTPUT" + echo "major=$MAJOR" >> "$GITHUB_OUTPUT" + echo "minor=$MINOR" >> "$GITHUB_OUTPUT" + + echo "TAG=$TAG" >> $GITHUB_ENV + echo "IS_RC=$IS_RC" >> $GITHUB_ENV + echo "MAJOR_TAG=$MAJOR" >> $GITHUB_ENV + echo "MINOR_TAG=$MINOR" >> $GITHUB_ENV + + - name: Wait for tag to be visible (dispatch only) + if: ${{ github.event_name == 'workflow_dispatch' }} + shell: bash + run: | + set -euo pipefail + for i in {1..90}; do + if git ls-remote --tags origin "refs/tags/${TAG}" | grep -qE "refs/tags/${TAG}$"; then + echo "Tag ${TAG} is visible on origin"; exit 0 + fi + echo "Tag not yet visible, retrying... ($i/90)" + sleep 2 + done + echo "Tag ${TAG} not visible after waiting" >&2 + exit 1 + + - name: Ensure repository is at the tagged commit (dispatch only) + if: ${{ github.event_name == 'workflow_dispatch' }} + shell: bash + run: | + set -euo pipefail + git fetch --tags --force + git checkout "refs/tags/${TAG}" + echo "Checked out $(git rev-parse --short HEAD) for tag ${TAG}" + + #- name: Set up QEMU + # uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 + + #- name: Set up Docker Buildx + # uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 - name: Log in to Docker Hub - uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 with: registry: docker.io username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} - name: Log in to GHCR - uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Normalize image names to lowercase + shell: bash run: | set -euo pipefail echo "GHCR_IMAGE=${GHCR_IMAGE,,}" >> "$GITHUB_ENV" echo "DOCKERHUB_IMAGE=${DOCKERHUB_IMAGE,,}" >> "$GITHUB_ENV" + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 + + # Build ONLY amd64 and push arch-specific tag suffixes used later for manifest creation. + - name: Build and push (amd64 -> *:amd64-TAG) + id: build_amd + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 + with: + context: . + push: true + platforms: linux/amd64 + build-args: VERSION=${{ env.TAG }} + tags: | + ${{ env.GHCR_IMAGE }}:amd64-${{ env.TAG }} + ${{ env.DOCKERHUB_IMAGE }}:amd64-${{ env.TAG }} + labels: | + org.opencontainers.image.title=${{ github.event.repository.name }} + org.opencontainers.image.version=${{ env.TAG }} + org.opencontainers.image.revision=${{ github.sha }} + org.opencontainers.image.source=${{ github.event.repository.html_url }} + org.opencontainers.image.url=${{ github.event.repository.html_url }} + org.opencontainers.image.documentation=${{ github.event.repository.html_url }} + org.opencontainers.image.description=${{ github.event.repository.description }} + org.opencontainers.image.licenses=${{ env.IMAGE_LICENSE }} + org.opencontainers.image.created=${{ env.IMAGE_CREATED }} + org.opencontainers.image.ref.name=${{ env.TAG }} + org.opencontainers.image.authors=${{ github.repository_owner }} + cache-from: type=gha,scope=${{ github.repository }}-amd64 + cache-to: type=gha,mode=max,scope=${{ github.repository }}-amd64 + + # --------------------------------------------------------------------------- + # 4) Build ARM64 image natively on ARM runner + # --------------------------------------------------------------------------- + build-arm: + name: Build image (linux/arm64) + needs: [pre-run, prepare] + if: ${{ needs.pre-run.result == 'success' && ((github.event_name == 'push' && github.actor != 'github-actions[bot]' && needs.prepare.result == 'skipped') || (github.event_name == 'workflow_dispatch' && (needs.prepare.result == 'success' || needs.prepare.result == 'skipped'))) }} + runs-on: [self-hosted, linux, arm64] # NOTE: ensure label exists on runner + timeout-minutes: 120 + env: + DOCKERHUB_IMAGE: docker.io/${{ github.repository_owner }}/${{ github.event.repository.name }} + GHCR_IMAGE: ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }} + IMAGE_LICENSE: ${{ github.event.repository.license.spdx_id || 'NOASSERTION' }} + IMAGE_CREATED: ${{ needs.pre-run.outputs.image_created }} + steps: + - name: Checkout code + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + fetch-depth: 0 + + - name: Monitor storage space shell: bash + run: | + THRESHOLD=75 + USED_SPACE=$(df / | grep / | awk '{ print $5 }' | sed 's/%//g') + echo "Used space: $USED_SPACE%" + if [ "$USED_SPACE" -ge "$THRESHOLD" ]; then + echo y | docker system prune -a || true + fi - - name: Extract tag name + - name: Determine tag + validate format + shell: bash env: EVENT_NAME: ${{ github.event_name }} INPUT_VERSION: ${{ inputs.version }} run: | + set -euo pipefail if [ "$EVENT_NAME" = "workflow_dispatch" ]; then - echo "TAG=${INPUT_VERSION}" >> $GITHUB_ENV + TAG="$INPUT_VERSION" else - echo "TAG=${{ github.ref_name }}" >> $GITHUB_ENV + TAG="${{ github.ref_name }}" fi - shell: bash - - name: Validate pushed tag format (no leading 'v') - if: ${{ github.event_name == 'push' }} - shell: bash - env: - TAG_GOT: ${{ env.TAG }} - run: | - set -euo pipefail - if [[ "$TAG_GOT" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-rc\.[0-9]+)?$ ]]; then - echo "Tag OK: $TAG_GOT" - exit 0 + if ! [[ "$TAG" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-rc\.[0-9]+)?$ ]]; then + echo "Invalid tag: $TAG" >&2 + exit 1 fi - echo "ERROR: Tag '$TAG_GOT' is not allowed. Use 'X.Y.Z' or 'X.Y.Z-rc.N' (no leading 'v')." >&2 - exit 1 + + echo "TAG=$TAG" >> $GITHUB_ENV + - name: Wait for tag to be visible (dispatch only) if: ${{ github.event_name == 'workflow_dispatch' }} + shell: bash run: | set -euo pipefail for i in {1..90}; do @@ -163,128 +350,171 @@ jobs: echo "Tag not yet visible, retrying... ($i/90)" sleep 2 done - echo "Tag ${TAG} not visible after waiting"; exit 1 - shell: bash - - - name: Update version in consts.go - run: | - TAG=${{ env.TAG }} - VERSION_FILE="internal/version/consts.go" - if [ -f "$VERSION_FILE" ]; then - sed -i 's/const Version = "[^"]*"/const Version = "'"$TAG"'"/' "$VERSION_FILE" - if grep -q "const Version = \"$TAG\"" "$VERSION_FILE"; then - echo "Updated $VERSION_FILE with version $TAG" - else - echo "Error: failed to update version in $VERSION_FILE" - exit 1 - fi - else - echo "Error: $VERSION_FILE not found" - exit 1 - fi + echo "Tag ${TAG} not visible after waiting" >&2 + exit 1 - name: Ensure repository is at the tagged commit (dispatch only) if: ${{ github.event_name == 'workflow_dispatch' }} + shell: bash run: | set -euo pipefail git fetch --tags --force git checkout "refs/tags/${TAG}" echo "Checked out $(git rev-parse --short HEAD) for tag ${TAG}" - shell: bash - - name: Detect release candidate (rc) + - name: Log in to Docker Hub + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + with: + registry: docker.io + username: ${{ secrets.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} + + - name: Log in to GHCR + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Normalize image names to lowercase + shell: bash run: | set -euo pipefail - if [[ "${TAG}" =~ ^[0-9]+\.[0-9]+\.[0-9]+-rc\.[0-9]+$ ]]; then - echo "IS_RC=true" >> $GITHUB_ENV - else - echo "IS_RC=false" >> $GITHUB_ENV - fi - shell: bash + echo "GHCR_IMAGE=${GHCR_IMAGE,,}" >> "$GITHUB_ENV" + echo "DOCKERHUB_IMAGE=${DOCKERHUB_IMAGE,,}" >> "$GITHUB_ENV" - - name: Install Go - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 + + # Build ONLY arm64 and push arch-specific tag suffixes used later for manifest creation. + - name: Build and push (arm64 -> *:arm64-TAG) + id: build_arm + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 with: - go-version-file: go.mod + context: . + push: true + platforms: linux/arm64 + build-args: VERSION=${{ env.TAG }} + tags: | + ${{ env.GHCR_IMAGE }}:arm64-${{ env.TAG }} + ${{ env.DOCKERHUB_IMAGE }}:arm64-${{ env.TAG }} + labels: | + org.opencontainers.image.title=${{ github.event.repository.name }} + org.opencontainers.image.version=${{ env.TAG }} + org.opencontainers.image.revision=${{ github.sha }} + org.opencontainers.image.source=${{ github.event.repository.html_url }} + org.opencontainers.image.url=${{ github.event.repository.html_url }} + org.opencontainers.image.documentation=${{ github.event.repository.html_url }} + org.opencontainers.image.description=${{ github.event.repository.description }} + org.opencontainers.image.licenses=${{ env.IMAGE_LICENSE }} + org.opencontainers.image.created=${{ env.IMAGE_CREATED }} + org.opencontainers.image.ref.name=${{ env.TAG }} + org.opencontainers.image.authors=${{ github.repository_owner }} + cache-from: type=gha,scope=${{ github.repository }}-arm64 + cache-to: type=gha,mode=max,scope=${{ github.repository }}-arm64 + + # --------------------------------------------------------------------------- + # 4b) Build ARMv7 image (linux/arm/v7) on arm runner via QEMU + # --------------------------------------------------------------------------- + build-armv7: + name: Build image (linux/arm/v7) + needs: [pre-run, prepare] + if: ${{ needs.pre-run.result == 'success' && ((github.event_name == 'push' && github.actor != 'github-actions[bot]' && needs.prepare.result == 'skipped') || (github.event_name == 'workflow_dispatch' && (needs.prepare.result == 'success' || needs.prepare.result == 'skipped'))) }} + runs-on: [self-hosted, linux, arm64] + timeout-minutes: 120 + env: + DOCKERHUB_IMAGE: docker.io/${{ github.repository_owner }}/${{ github.event.repository.name }} + GHCR_IMAGE: ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }} + IMAGE_LICENSE: ${{ github.event.repository.license.spdx_id || 'NOASSERTION' }} + IMAGE_CREATED: ${{ needs.pre-run.outputs.image_created }} + steps: + - name: Checkout code + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + fetch-depth: 0 - - name: Resolve publish-latest flag + - name: Determine tag + validate format + shell: bash env: EVENT_NAME: ${{ github.event_name }} - PL_INPUT: ${{ inputs.publish_latest }} - PL_VAR: ${{ vars.PUBLISH_LATEST }} + INPUT_VERSION: ${{ inputs.version }} run: | set -euo pipefail - val="false" if [ "$EVENT_NAME" = "workflow_dispatch" ]; then - if [ "${PL_INPUT}" = "true" ]; then val="true"; fi + TAG="$INPUT_VERSION" else - if [ "${PL_VAR}" = "true" ]; then val="true"; fi + TAG="${{ github.ref_name }}" fi - echo "PUBLISH_LATEST=$val" >> $GITHUB_ENV - shell: bash - - name: Resolve publish-minor flag - env: - EVENT_NAME: ${{ github.event_name }} - PM_INPUT: ${{ inputs.publish_minor }} - PM_VAR: ${{ vars.PUBLISH_MINOR }} + if ! [[ "$TAG" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-rc\.[0-9]+)?$ ]]; then + echo "Invalid tag: $TAG" >&2 + exit 1 + fi + + echo "TAG=$TAG" >> $GITHUB_ENV + + - name: Wait for tag to be visible (dispatch only) + if: ${{ github.event_name == 'workflow_dispatch' }} + shell: bash run: | set -euo pipefail - val="false" - if [ "$EVENT_NAME" = "workflow_dispatch" ]; then - if [ "${PM_INPUT}" = "true" ]; then val="true"; fi - else - if [ "${PM_VAR}" = "true" ]; then val="true"; fi - fi - echo "PUBLISH_MINOR=$val" >> $GITHUB_ENV + for i in {1..90}; do + if git ls-remote --tags origin "refs/tags/${TAG}" | grep -qE "refs/tags/${TAG}$"; then + echo "Tag ${TAG} is visible on origin"; exit 0 + fi + echo "Tag not yet visible, retrying... ($i/90)" + sleep 2 + done + echo "Tag ${TAG} not visible after waiting" >&2 + exit 1 + + - name: Ensure repository is at the tagged commit (dispatch only) + if: ${{ github.event_name == 'workflow_dispatch' }} shell: bash + run: | + set -euo pipefail + git fetch --tags --force + git checkout "refs/tags/${TAG}" + echo "Checked out $(git rev-parse --short HEAD) for tag ${TAG}" - - name: Cache Go modules - if: ${{ hashFiles('**/go.sum') != '' }} - uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 + - name: Log in to Docker Hub + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 with: - path: | - ~/.cache/go-build - ~/go/pkg/mod - key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-go- - - name: Go vet & test - if: ${{ hashFiles('**/go.mod') != '' }} - run: | - go version - go vet ./... - go test ./... -race -covermode=atomic - shell: bash + registry: docker.io + username: ${{ secrets.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} - - name: Resolve license fallback - run: echo "IMAGE_LICENSE=${{ github.event.repository.license.spdx_id || 'NOASSERTION' }}" >> $GITHUB_ENV - shell: bash + - name: Log in to GHCR + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - - name: Resolve registries list (GHCR always, Docker Hub only if creds) + - name: Normalize image names to lowercase shell: bash run: | set -euo pipefail - images="${GHCR_IMAGE}" - if [ -n "${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}" ] && [ -n "${{ secrets.DOCKER_HUB_USERNAME }}" ]; then - images="${images}\n${DOCKERHUB_IMAGE}" - fi - { - echo 'IMAGE_LIST<> "$GITHUB_ENV" - - name: Docker meta - id: meta - uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0 + echo "GHCR_IMAGE=${GHCR_IMAGE,,}" >> "$GITHUB_ENV" + echo "DOCKERHUB_IMAGE=${DOCKERHUB_IMAGE,,}" >> "$GITHUB_ENV" + + - name: Set up QEMU + uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 + + - name: Build and push (arm/v7 -> *:armv7-TAG) + id: build_armv7 + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 with: - images: ${{ env.IMAGE_LIST }} + context: . + push: true + platforms: linux/arm/v7 + build-args: VERSION=${{ env.TAG }} tags: | - type=semver,pattern={{version}},value=${{ env.TAG }} - type=semver,pattern={{major}}.{{minor}},value=${{ env.TAG }},enable=${{ env.PUBLISH_MINOR == 'true' && env.IS_RC != 'true' }} - type=raw,value=latest,enable=${{ env.IS_RC != 'true' }} - flavor: | - latest=false + ${{ env.GHCR_IMAGE }}:armv7-${{ env.TAG }} + ${{ env.DOCKERHUB_IMAGE }}:armv7-${{ env.TAG }} labels: | org.opencontainers.image.title=${{ github.event.repository.name }} org.opencontainers.image.version=${{ env.TAG }} @@ -297,144 +527,269 @@ jobs: org.opencontainers.image.created=${{ env.IMAGE_CREATED }} org.opencontainers.image.ref.name=${{ env.TAG }} org.opencontainers.image.authors=${{ github.repository_owner }} - - name: Echo build config (non-secret) + cache-from: type=gha,scope=${{ github.repository }}-armv7 + cache-to: type=gha,mode=max,scope=${{ github.repository }}-armv7 + + # --------------------------------------------------------------------------- + # 5) Create and push multi-arch manifests (TAG, plus optional latest/major/minor) + # --------------------------------------------------------------------------- + create-manifest: + name: Create multi-arch manifests + needs: [build-amd, build-arm, build-armv7] + if: ${{ needs.build-amd.result == 'success' && needs.build-arm.result == 'success' && needs.build-armv7.result == 'success' }} + runs-on: [self-hosted, linux, x64] # NOTE: ensure label exists on runner + timeout-minutes: 30 + env: + DOCKERHUB_IMAGE: docker.io/${{ github.repository_owner }}/${{ github.event.repository.name }} + GHCR_IMAGE: ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }} + TAG: ${{ needs.build-amd.outputs.tag }} + IS_RC: ${{ needs.build-amd.outputs.is_rc }} + MAJOR_TAG: ${{ needs.build-amd.outputs.major }} + MINOR_TAG: ${{ needs.build-amd.outputs.minor }} + # workflow_dispatch controls are respected only here (tagging policy) + #PUBLISH_LATEST: ${{ github.event_name == 'workflow_dispatch' && inputs.publish_latest || vars.PUBLISH_LATEST }} + #PUBLISH_MINOR: ${{ github.event_name == 'workflow_dispatch' && inputs.publish_minor || vars.PUBLISH_MINOR }} + steps: + - name: Log in to Docker Hub + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + with: + registry: docker.io + username: ${{ secrets.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} + + - name: Log in to GHCR + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Normalize image names to lowercase shell: bash - env: - IMAGE_TITLE: ${{ github.event.repository.name }} - IMAGE_VERSION: ${{ env.TAG }} - IMAGE_REVISION: ${{ github.sha }} - IMAGE_SOURCE_URL: ${{ github.event.repository.html_url }} - IMAGE_URL: ${{ github.event.repository.html_url }} - IMAGE_DESCRIPTION: ${{ github.event.repository.description }} - IMAGE_LICENSE: ${{ env.IMAGE_LICENSE }} - DOCKERHUB_IMAGE: ${{ env.DOCKERHUB_IMAGE }} - GHCR_IMAGE: ${{ env.GHCR_IMAGE }} - DOCKER_HUB_USER: ${{ secrets.DOCKER_HUB_USERNAME }} - REPO: ${{ github.repository }} - OWNER: ${{ github.repository_owner }} - WORKFLOW_REF: ${{ github.workflow_ref }} - REF: ${{ github.ref }} - REF_NAME: ${{ github.ref_name }} - RUN_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} - run: | - set -euo pipefail - echo "=== OCI Label Values ===" - echo "org.opencontainers.image.title=${IMAGE_TITLE}" - echo "org.opencontainers.image.version=${IMAGE_VERSION}" - echo "org.opencontainers.image.revision=${IMAGE_REVISION}" - echo "org.opencontainers.image.source=${IMAGE_SOURCE_URL}" - echo "org.opencontainers.image.url=${IMAGE_URL}" - echo "org.opencontainers.image.description=${IMAGE_DESCRIPTION}" - echo "org.opencontainers.image.licenses=${IMAGE_LICENSE}" - echo - echo "=== Images ===" - echo "DOCKERHUB_IMAGE=${DOCKERHUB_IMAGE}" - echo "GHCR_IMAGE=${GHCR_IMAGE}" - echo "DOCKER_HUB_USERNAME=${DOCKER_HUB_USER}" - echo - echo "=== GitHub Kontext ===" - echo "repository=${REPO}" - echo "owner=${OWNER}" - echo "workflow_ref=${WORKFLOW_REF}" - echo "ref=${REF}" - echo "ref_name=${REF_NAME}" - echo "run_url=${RUN_URL}" - echo - echo "=== docker/metadata-action outputs (Tags/Labels), raw ===" - echo "::group::tags" - echo "${{ steps.meta.outputs.tags }}" - echo "::endgroup::" - echo "::group::labels" - echo "${{ steps.meta.outputs.labels }}" - echo "::endgroup::" - - name: Build and push (Docker Hub + GHCR) - id: build - uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 + run: | + set -euo pipefail + echo "GHCR_IMAGE=${GHCR_IMAGE,,}" >> "$GITHUB_ENV" + echo "DOCKERHUB_IMAGE=${DOCKERHUB_IMAGE,,}" >> "$GITHUB_ENV" + + - name: Set up Docker Buildx (needed for imagetools) + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 + + - name: Create & push multi-arch index (GHCR :TAG) via imagetools + shell: bash + run: | + set -euo pipefail + docker buildx imagetools create \ + -t "${GHCR_IMAGE}:${TAG}" \ + "${GHCR_IMAGE}:amd64-${TAG}" \ + "${GHCR_IMAGE}:arm64-${TAG}" \ + "${GHCR_IMAGE}:armv7-${TAG}" + + - name: Create & push multi-arch index (Docker Hub :TAG) via imagetools + shell: bash + run: | + set -euo pipefail + docker buildx imagetools create \ + -t "${DOCKERHUB_IMAGE}:${TAG}" \ + "${DOCKERHUB_IMAGE}:amd64-${TAG}" \ + "${DOCKERHUB_IMAGE}:arm64-${TAG}" \ + "${DOCKERHUB_IMAGE}:armv7-${TAG}" + + # Additional tags for non-RC releases: latest, major, minor (always) + - name: Publish additional tags (non-RC only) via imagetools + if: ${{ env.IS_RC != 'true' }} + shell: bash + run: | + set -euo pipefail + + tags_to_publish=("${MAJOR_TAG}" "${MINOR_TAG}" "latest") + + for t in "${tags_to_publish[@]}"; do + echo "Publishing GHCR tag ${t} -> ${TAG}" + docker buildx imagetools create \ + -t "${GHCR_IMAGE}:${t}" \ + "${GHCR_IMAGE}:amd64-${TAG}" \ + "${GHCR_IMAGE}:arm64-${TAG}" \ + "${GHCR_IMAGE}:armv7-${TAG}" + + echo "Publishing Docker Hub tag ${t} -> ${TAG}" + docker buildx imagetools create \ + -t "${DOCKERHUB_IMAGE}:${t}" \ + "${DOCKERHUB_IMAGE}:amd64-${TAG}" \ + "${DOCKERHUB_IMAGE}:arm64-${TAG}" \ + "${DOCKERHUB_IMAGE}:armv7-${TAG}" + done + + # --------------------------------------------------------------------------- + # 6) Sign/attest + build binaries + draft release (x86 runner) + # --------------------------------------------------------------------------- + sign-and-release: + name: Sign, attest, and release + needs: [create-manifest, build-amd] + if: ${{ needs.create-manifest.result == 'success' && needs.build-amd.result == 'success' }} + runs-on: [self-hosted, linux, x64] # NOTE: ensure label exists on runner + timeout-minutes: 120 + env: + DOCKERHUB_IMAGE: docker.io/${{ github.repository_owner }}/${{ github.event.repository.name }} + GHCR_IMAGE: ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }} + TAG: ${{ needs.build-amd.outputs.tag }} + IS_RC: ${{ needs.build-amd.outputs.is_rc }} + IMAGE_LICENSE: ${{ github.event.repository.license.spdx_id || 'NOASSERTION' }} + IMAGE_CREATED: ${{ needs.pre-run.outputs.image_created }} + steps: + - name: Checkout code + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: - context: . - push: true - platforms: linux/amd64,linux/arm64,linux/arm/v7 - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha,scope=${{ github.repository }} - cache-to: type=gha,mode=max,scope=${{ github.repository }} - provenance: mode=max - sbom: true + fetch-depth: 0 - - name: Compute image digest refs + - name: Ensure repository is at the tagged commit (dispatch only) + if: ${{ github.event_name == 'workflow_dispatch' }} + shell: bash run: | - echo "DIGEST=${{ steps.build.outputs.digest }}" >> $GITHUB_ENV - echo "GHCR_REF=$GHCR_IMAGE@${{ steps.build.outputs.digest }}" >> $GITHUB_ENV - echo "DH_REF=$DOCKERHUB_IMAGE@${{ steps.build.outputs.digest }}" >> $GITHUB_ENV - echo "Built digest: ${{ steps.build.outputs.digest }}" + set -euo pipefail + git fetch --tags --force + git checkout "refs/tags/${TAG}" + echo "Checked out $(git rev-parse --short HEAD) for tag ${TAG}" + + - name: Install Go + uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 + with: + go-version-file: go.mod + + - name: Log in to Docker Hub + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + with: + registry: docker.io + username: ${{ secrets.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} + + - name: Log in to GHCR + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Normalize image names to lowercase shell: bash + run: | + set -euo pipefail + echo "GHCR_IMAGE=${GHCR_IMAGE,,}" >> "$GITHUB_ENV" + echo "DOCKERHUB_IMAGE=${DOCKERHUB_IMAGE,,}" >> "$GITHUB_ENV" - - name: Attest build provenance (GHCR) - id: attest-ghcr - uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8 # v3.1.0 + - name: Ensure jq is installed + shell: bash + run: | + set -euo pipefail + if command -v jq >/dev/null 2>&1; then + exit 0 + fi + sudo apt-get update -y + sudo apt-get install -y jq + + - name: Set up Docker Buildx (needed for imagetools) + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 + + - name: Resolve multi-arch digest refs (by TAG) + shell: bash + run: | + set -euo pipefail + + get_digest() { + local ref="$1" + local d="" + # Primary: buildx format output + d="$(docker buildx imagetools inspect "$ref" --format '{{.Manifest.Digest}}' 2>/dev/null || true)" + + # Fallback: parse from plain text if format fails + if ! [[ "$d" =~ ^sha256:[0-9a-f]{64}$ ]]; then + d="$(docker buildx imagetools inspect "$ref" 2>/dev/null | awk '/^Digest:/ {print $2; exit}' || true)" + fi + + if ! [[ "$d" =~ ^sha256:[0-9a-f]{64}$ ]]; then + echo "ERROR: Could not extract digest for $ref" >&2 + docker buildx imagetools inspect "$ref" || true + exit 1 + fi + + echo "$d" + } + + GHCR_DIGEST="$(get_digest "${GHCR_IMAGE}:${TAG}")" + echo "GHCR_REF=${GHCR_IMAGE}@${GHCR_DIGEST}" >> "$GITHUB_ENV" + echo "GHCR_DIGEST=${GHCR_DIGEST}" >> "$GITHUB_ENV" + echo "Resolved GHCR_REF=${GHCR_IMAGE}@${GHCR_DIGEST}" + + if [ -n "${{ secrets.DOCKER_HUB_USERNAME }}" ] && [ -n "${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}" ]; then + DH_DIGEST="$(get_digest "${DOCKERHUB_IMAGE}:${TAG}")" + echo "DH_REF=${DOCKERHUB_IMAGE}@${DH_DIGEST}" >> "$GITHUB_ENV" + echo "DH_DIGEST=${DH_DIGEST}" >> "$GITHUB_ENV" + echo "Resolved DH_REF=${DOCKERHUB_IMAGE}@${DH_DIGEST}" + fi + + - name: Attest build provenance (GHCR) (digest) + uses: actions/attest-build-provenance@96278af6caaf10aea03fd8d33a09a777ca52d62f # v3.2.0 with: subject-name: ${{ env.GHCR_IMAGE }} - subject-digest: ${{ steps.build.outputs.digest }} + subject-digest: ${{ env.GHCR_DIGEST }} push-to-registry: true show-summary: true - name: Attest build provenance (Docker Hub) continue-on-error: true - id: attest-dh - uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8 # v3.1.0 + if: ${{ env.DH_DIGEST != '' }} + uses: actions/attest-build-provenance@96278af6caaf10aea03fd8d33a09a777ca52d62f # v3.2.0 with: - subject-name: index.docker.io/fosrl/pangolin-${{ github.event.repository.name }} - subject-digest: ${{ steps.build.outputs.digest }} + subject-name: index.docker.io/${{ github.repository_owner }}/${{ github.event.repository.name }} + subject-digest: ${{ env.DH_DIGEST }} push-to-registry: true show-summary: true - name: Install cosign uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0 with: - cosign-release: 'v3.0.2' + cosign-release: "v3.0.2" - name: Sanity check cosign private key env: COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }} COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }} + shell: bash run: | set -euo pipefail cosign public-key --key env://COSIGN_PRIVATE_KEY >/dev/null + + - name: Generate SBOM (SPDX JSON) from GHCR digest + uses: aquasecurity/trivy-action@97e0b3872f55f89b95b2f65b3dbab56962816478 # v0.34.2 + with: + image-ref: ${{ env.GHCR_REF }} + format: spdx-json + output: sbom.spdx.json + version: v0.69.3 + + - name: Validate + minify SBOM JSON shell: bash + run: | + set -euo pipefail + jq -e . sbom.spdx.json >/dev/null + jq -c . sbom.spdx.json > sbom.min.json && mv sbom.min.json sbom.spdx.json - - name: Sign GHCR image (digest) with key (recursive) + - name: Sign GHCR digest (key, recursive) env: COSIGN_YES: "true" COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }} COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }} + shell: bash run: | set -euo pipefail - echo "Signing ${GHCR_REF} (digest) recursively with provided key" cosign sign --key env://COSIGN_PRIVATE_KEY --recursive "${GHCR_REF}" - echo "Waiting 30 seconds for signatures to propagate..." - sleep 30 - shell: bash - - - name: Generate SBOM (SPDX JSON) - uses: aquasecurity/trivy-action@b6643a29fecd7f34b3597bc6acb0a98b03d33ff8 # v0.33.1 - with: - image-ref: ${{ env.GHCR_IMAGE }}@${{ steps.build.outputs.digest }} - format: spdx-json - output: sbom.spdx.json - - - name: Validate SBOM JSON - run: jq -e . sbom.spdx.json >/dev/null - shell: bash + sleep 20 - - name: Minify SBOM JSON (optional hardening) - run: jq -c . sbom.spdx.json > sbom.min.json && mv sbom.min.json sbom.spdx.json - shell: bash - - - name: Create SBOM attestation (GHCR, private key) + - name: Create SBOM attestation (GHCR, key) env: COSIGN_YES: "true" COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }} COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }} + shell: bash run: | set -euo pipefail cosign attest \ @@ -442,15 +797,15 @@ jobs: --type spdxjson \ --predicate sbom.spdx.json \ "${GHCR_REF}" - shell: bash - - name: Create SBOM attestation (Docker Hub, private key) + - name: Create SBOM attestation (Docker Hub, key) continue-on-error: true env: COSIGN_YES: "true" COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }} COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }} COSIGN_DOCKER_MEDIA_TYPES: "1" + shell: bash run: | set -euo pipefail cosign attest \ @@ -458,155 +813,86 @@ jobs: --type spdxjson \ --predicate sbom.spdx.json \ "${DH_REF}" - shell: bash - name: Keyless sign & verify GHCR digest (OIDC) env: COSIGN_YES: "true" - WORKFLOW_REF: ${{ github.workflow_ref }} # owner/repo/.github/workflows/@refs/tags/ + WORKFLOW_REF: ${{ github.workflow_ref }} ISSUER: https://token.actions.githubusercontent.com + shell: bash run: | set -euo pipefail - echo "Keyless signing ${GHCR_REF}" cosign sign --rekor-url https://rekor.sigstore.dev --recursive "${GHCR_REF}" - echo "Verify keyless (OIDC) signature policy on ${GHCR_REF}" cosign verify \ --certificate-oidc-issuer "${ISSUER}" \ --certificate-identity "https://github.com/${WORKFLOW_REF}" \ "${GHCR_REF}" -o text + + - name: Verify signature (public key) GHCR digest + tag + env: + COSIGN_PUBLIC_KEY: ${{ secrets.COSIGN_PUBLIC_KEY }} + shell: bash + run: | + set -euo pipefail + cosign verify --key env://COSIGN_PUBLIC_KEY "${GHCR_REF}" -o text + cosign verify --key env://COSIGN_PUBLIC_KEY "${GHCR_IMAGE}:${TAG}" -o text + + - name: Verify SBOM attestation (GHCR) + env: + COSIGN_PUBLIC_KEY: ${{ secrets.COSIGN_PUBLIC_KEY }} + run: cosign verify-attestation --key env://COSIGN_PUBLIC_KEY --type spdxjson "${GHCR_REF}" -o text shell: bash - - name: Sign Docker Hub image (digest) with key (recursive) + - name: Sign Docker Hub digest (key, recursive) continue-on-error: true env: COSIGN_YES: "true" COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }} COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }} COSIGN_DOCKER_MEDIA_TYPES: "1" + shell: bash run: | set -euo pipefail - echo "Signing ${DH_REF} (digest) recursively with provided key (Docker media types fallback)" cosign sign --key env://COSIGN_PRIVATE_KEY --recursive "${DH_REF}" - shell: bash - name: Keyless sign & verify Docker Hub digest (OIDC) continue-on-error: true + if: ${{ env.DH_REF != '' }} env: COSIGN_YES: "true" ISSUER: https://token.actions.githubusercontent.com COSIGN_DOCKER_MEDIA_TYPES: "1" + shell: bash run: | set -euo pipefail - echo "Keyless signing ${DH_REF} (force public-good Rekor)" cosign sign --rekor-url https://rekor.sigstore.dev --recursive "${DH_REF}" - echo "Keyless verify via Rekor (strict identity)" - if ! cosign verify \ - --rekor-url https://rekor.sigstore.dev \ + cosign verify \ --certificate-oidc-issuer "${ISSUER}" \ --certificate-identity "https://github.com/${{ github.workflow_ref }}" \ - "${DH_REF}" -o text; then - echo "Rekor verify failed — retry offline bundle verify (no Rekor)" - if ! cosign verify \ - --offline \ - --certificate-oidc-issuer "${ISSUER}" \ - --certificate-identity "https://github.com/${{ github.workflow_ref }}" \ - "${DH_REF}" -o text; then - echo "Offline bundle verify failed — ignore tlog (TEMP for debugging)" - cosign verify \ - --insecure-ignore-tlog=true \ - --certificate-oidc-issuer "${ISSUER}" \ - --certificate-identity "https://github.com/${{ github.workflow_ref }}" \ - "${DH_REF}" -o text || true - fi - fi - - name: Verify signature (public key) GHCR digest + tag - env: - COSIGN_PUBLIC_KEY: ${{ secrets.COSIGN_PUBLIC_KEY }} - run: | - set -euo pipefail - TAG_VAR="${TAG}" - echo "Verifying (digest) ${GHCR_REF}" - cosign verify --key env://COSIGN_PUBLIC_KEY "$GHCR_REF" -o text - echo "Verifying (tag) $GHCR_IMAGE:$TAG_VAR" - cosign verify --key env://COSIGN_PUBLIC_KEY "$GHCR_IMAGE:$TAG_VAR" -o text - shell: bash - - - name: Verify SBOM attestation (GHCR) - env: - COSIGN_PUBLIC_KEY: ${{ secrets.COSIGN_PUBLIC_KEY }} - run: cosign verify-attestation --key env://COSIGN_PUBLIC_KEY --type spdxjson "$GHCR_REF" -o text - shell: bash - - - name: Verify SLSA provenance (GHCR) - env: - ISSUER: https://token.actions.githubusercontent.com - WFREF: ${{ github.workflow_ref }} - run: | - set -euo pipefail - # (optional) show which predicate types are present to aid debugging - cosign download attestation "$GHCR_REF" \ - | jq -r '.payload | @base64d | fromjson | .predicateType' | sort -u || true - # Verify the SLSA v1 provenance attestation (predicate URL) - cosign verify-attestation \ - --type 'https://slsa.dev/provenance/v1' \ - --certificate-oidc-issuer "$ISSUER" \ - --certificate-identity "https://github.com/${WFREF}" \ - --rekor-url https://rekor.sigstore.dev \ - "$GHCR_REF" -o text - shell: bash + "${DH_REF}" -o text - - name: Verify signature (public key) Docker Hub digest + - name: Verify signature (public key) Docker Hub digest + tag continue-on-error: true + if: ${{ env.DH_REF != '' }} env: COSIGN_PUBLIC_KEY: ${{ secrets.COSIGN_PUBLIC_KEY }} COSIGN_DOCKER_MEDIA_TYPES: "1" - run: | - set -euo pipefail - echo "Verifying (digest) ${DH_REF} with Docker media types" - cosign verify --key env://COSIGN_PUBLIC_KEY "${DH_REF}" -o text shell: bash - - - name: Verify signature (public key) Docker Hub tag - continue-on-error: true - env: - COSIGN_PUBLIC_KEY: ${{ secrets.COSIGN_PUBLIC_KEY }} - COSIGN_DOCKER_MEDIA_TYPES: "1" run: | set -euo pipefail - echo "Verifying (tag) $DOCKERHUB_IMAGE:$TAG with Docker media types" - cosign verify --key env://COSIGN_PUBLIC_KEY "$DOCKERHUB_IMAGE:$TAG" -o text - shell: bash - - # - name: Trivy scan (GHCR image) - # id: trivy - # uses: aquasecurity/trivy-action@b6643a29fecd7f34b3597bc6acb0a98b03d33ff8 # v0.33.1 - # with: - # image-ref: ${{ env.GHCR_IMAGE }}@${{ steps.build.outputs.digest }} - # format: sarif - # output: trivy-ghcr.sarif - # ignore-unfixed: true - # vuln-type: os,library - # severity: CRITICAL,HIGH - # exit-code: ${{ (vars.TRIVY_FAIL || '0') }} - - # - name: Upload SARIF,trivy - # if: ${{ always() && hashFiles('trivy-ghcr.sarif') != '' }} - # uses: github/codeql-action/upload-sarif@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5 - # with: - # sarif_file: trivy-ghcr.sarif - # category: Image Vulnerability Scan + cosign verify --key env://COSIGN_PUBLIC_KEY "${DH_REF}" -o text + cosign verify --key env://COSIGN_PUBLIC_KEY "${DOCKERHUB_IMAGE}:${TAG}" -o text - name: Build binaries env: CGO_ENABLED: "0" GOFLAGS: "-trimpath" + shell: bash run: | set -euo pipefail - TAG_VAR="${TAG}" - make -j 10 go-build-release tag=$TAG_VAR - shell: bash + make -j 10 go-build-release VERSION="${TAG}" - - name: Create GitHub Release + - name: Create GitHub Release (draft) uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2 with: tag_name: ${{ env.TAG }} @@ -620,4 +906,31 @@ jobs: ## Container Images - GHCR: `${{ env.GHCR_REF }}` - Docker Hub: `${{ env.DH_REF || 'N/A' }}` - **Digest:** `${{ steps.build.outputs.digest }}` + **Tag:** `${{ env.TAG }}` + + # --------------------------------------------------------------------------- + # 7) Stop AWS EC2 runner instances + # --------------------------------------------------------------------------- + + post-run: + name: Stop AWS EC2 runners + needs: [pre-run, prepare, build-amd, build-arm, build-armv7, create-manifest, sign-and-release] + if: ${{ always() && needs.pre-run.result == 'success' }} + runs-on: ubuntu-latest + permissions: write-all + steps: + - name: Configure AWS credentials (OIDC) + uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v6.0.0 + with: + role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }} + role-duration-seconds: 3600 + aws-region: ${{ secrets.AWS_REGION }} + + - name: Verify AWS identity + run: aws sts get-caller-identity + + - name: Stop EC2 instances + run: | + aws ec2 stop-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_AMD_RUNNER }} + aws ec2 stop-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_ARM_RUNNER }} + echo "EC2 instances stopped" diff --git a/Dockerfile b/Dockerfile index 71f0a50..368dd7b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,7 +16,8 @@ RUN go mod download COPY . . # Build the application -RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /pangolin-cli +ARG VERSION=dev +RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w -X github.com/fosrl/cli/internal/version.Version=${VERSION}" -o /pangolin-cli FROM public.ecr.aws/docker/library/alpine:3.23 AS runner @@ -30,4 +31,4 @@ EXPOSE 2112 RUN chmod +x /entrypoint.sh ENTRYPOINT ["/entrypoint.sh"] -CMD ["pangolin-cli"] +CMD ["pangolin-cli"] \ No newline at end of file diff --git a/Makefile b/Makefile index 75512cf..57b879b 100644 --- a/Makefile +++ b/Makefile @@ -1,16 +1,17 @@ -.PHONY: build clean install docs +.PHONY: all build clean install docs docker-build docker-build-release BINARY_NAME=pangolin OUTPUT_DIR=bin -LDFLAGS=-ldflags="-s -w" -# GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -o bin/pangolin . +VERSION ?= dev +LDFLAGS = -s -w -X github.com/fosrl/cli/internal/version.Version=$(VERSION) + all: clean build build: @echo "Building $(BINARY_NAME)..." @mkdir -p $(OUTPUT_DIR) - @go build $(LDFLAGS) -o $(OUTPUT_DIR)/$(BINARY_NAME) . + @go build -ldflags "$(LDFLAGS)" -o $(OUTPUT_DIR)/$(BINARY_NAME) . @echo "Build complete: $(OUTPUT_DIR)/$(BINARY_NAME)" clean: @@ -20,7 +21,7 @@ clean: install: build @echo "Installing $(BINARY_NAME)..." - @go install $(LDFLAGS) . + @go install -ldflags "$(LDFLAGS)" . docs: @echo "Generating markdown documentation..." @@ -59,25 +60,25 @@ go-build-release: \ go-build-release-windows-amd64 go-build-release-linux-arm64: - CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o bin/pangolin-cli_linux_arm64 + CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags "$(LDFLAGS)" -o bin/pangolin-cli_linux_arm64 go-build-release-linux-arm32-v7: - CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 go build -o bin/pangolin-cli_linux_arm32 + CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 go build -ldflags "$(LDFLAGS)" -o bin/pangolin-cli_linux_arm32 go-build-release-linux-arm32-v6: - CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=6 go build -o bin/pangolin-cli_linux_arm32v6 + CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=6 go build -ldflags "$(LDFLAGS)" -o bin/pangolin-cli_linux_arm32v6 go-build-release-linux-amd64: - CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/pangolin-cli_linux_amd64 + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o bin/pangolin-cli_linux_amd64 go-build-release-linux-riscv64: - CGO_ENABLED=0 GOOS=linux GOARCH=riscv64 go build -o bin/pangolin-cli_linux_riscv64 + CGO_ENABLED=0 GOOS=linux GOARCH=riscv64 go build -ldflags "$(LDFLAGS)" -o bin/pangolin-cli_linux_riscv64 go-build-release-darwin-arm64: - CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -o bin/pangolin-cli_darwin_arm64 + CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags "$(LDFLAGS)" -o bin/pangolin-cli_darwin_arm64 go-build-release-darwin-amd64: - CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o bin/pangolin-cli_darwin_amd64 + CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o bin/pangolin-cli_darwin_amd64 go-build-release-windows-amd64: - CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o bin/pangolin-cli_windows_amd64.exe \ No newline at end of file + CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o bin/pangolin-cli_windows_amd64.exe \ No newline at end of file diff --git a/internal/version/consts.go b/internal/version/consts.go index ea7cca8..7c083e3 100644 --- a/internal/version/consts.go +++ b/internal/version/consts.go @@ -1,4 +1,7 @@ package version -// Version is the current version of the Pangolin CLI -const Version = "0.5.0" +// Version is the current version of the Pangolin CLI. +// This value can be overridden at build time using ldflags: +// +// go build -ldflags "-X github.com/fosrl/cli/internal/version.Version=" +var Version = "version_replaceme" \ No newline at end of file From 5a429527a861b31e39c04446c33799fa15fb4ade Mon Sep 17 00:00:00 2001 From: Owen Date: Tue, 17 Mar 2026 17:58:12 -0700 Subject: [PATCH 5/5] Remove local --- .github/workflows/test.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 490be06..c1cb0af 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,7 +15,6 @@ jobs: strategy: matrix: target: - - local - docker-build - go-build-release-darwin-amd64 - go-build-release-darwin-arm64