Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 23 additions & 12 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ env:

jobs:
# ---------------------------------------------------------------------------
# Job 1: Resolve the platforms input into a JSON matrix.
# Job 1: Resolve the platforms input into a JSON matrix, and the version the
# built binary reports.
#
# Why a separate job: workflow-level matrix entries must be static or come
# from job outputs; they cannot be computed inline from an input string.
Expand All @@ -61,6 +62,7 @@ jobs:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
version: ${{ steps.set-version.outputs.version }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
Expand Down Expand Up @@ -95,6 +97,19 @@ jobs:
')
echo "matrix=$MATRIX" >> $GITHUB_OUTPUT

# Resolved once and read by both the build arg and the smoke test, so the
# baked version and the asserted one cannot drift apart (#1133).
- name: Resolve the version to build
id: set-version
env:
TAG: ${{ inputs.tag }}
run: |
# A version tag is reported as-is; any other tag is a dev build.
# Loose on purpose: ^v[0-9] also accepts the -rc tags this repo cuts.
VERSION="$TAG"
[[ "$TAG" =~ ^v[0-9] ]] || VERSION="dev+$TAG"
echo "version=$VERSION" >> $GITHUB_OUTPUT


# ---------------------------------------------------------------------------
# Job 2: Build each platform on its native runner, push by digest.
Expand Down Expand Up @@ -127,9 +142,7 @@ jobs:
- uses: actions/checkout@v7
with:
fetch-depth: 3
# The Makefile derives the version from `git describe`, and
# fetch-depth > 0 fetches no tags unless asked (#1133).
fetch-tags: true
# No fetch-tags: the version arrives as a build arg, not from here.
# .git/ ships in the build context, which mode=max exports to the
# public cache ref.
persist-credentials: false
Expand All @@ -151,6 +164,8 @@ jobs:
uses: docker/build-push-action@v7
with:
context: .
build-args: |
VERSION=${{ needs.prepare.outputs.version }}
# Push without a tag — the merge job creates the final manifest.
platforms: ${{ matrix.platform }}
outputs: type=image,name=${{ env.IMAGE }},push-by-digest=true,name-canonical=true,push=true
Expand Down Expand Up @@ -183,7 +198,7 @@ jobs:
merge:
name: Docker Merge & Attest
runs-on: ubuntu-latest
needs: [build]
needs: [prepare, build]
permissions:
id-token: write
contents: write
Expand Down Expand Up @@ -348,13 +363,9 @@ jobs:
env:
IMAGE: ${{ env.IMAGE }}
TAG: ${{ inputs.tag }}
run: |
# Release builds must report their tag; others are tagged with a
# sha and report dev+<sha>.
if [[ "$TAG" =~ ^v[0-9] ]]; then
export EXPECTED_VERSION="$TAG"
fi
./scripts/docker-smoke-tests.sh
# The same value the build job baked in.
EXPECTED_VERSION: ${{ needs.prepare.outputs.version }}
run: ./scripts/docker-smoke-tests.sh

- name: Report Docker smoke test attestation to Kosli
if: ${{ inputs.report_to_kosli != 'none' && (success() || failure()) }}
Expand Down
8 changes: 7 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
ARG GO_VERSION="1.26"
ARG ALPINE_VERSION="3.23"

# The version the binary reports; empty builds dev+<sha>. Passed in rather than
# read from git tags, so the image cannot depend on the checkout's tags (#1133).
ARG VERSION=""


### Go Builder ###
FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS builder
Expand All @@ -15,7 +19,9 @@ WORKDIR /go/src/kosli

COPY . .

RUN make build
# Re-declare inside the stage — a global ARG is not in scope in a build stage.
ARG VERSION
RUN make build VERSION="${VERSION}"

RUN mkdir -p /image-tmp

Expand Down
20 changes: 14 additions & 6 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,26 @@ GOTESTSUM = $(shell which gotestsum || echo "~/go/bin/gotestsum")
# These are only used when running tests locally (real CI already sets them).
FAKE_CI_ENV = GITHUB_RUN_NUMBER=1 GITHUB_SERVER_URL=https://github.com GITHUB_REPOSITORY=kosli-dev/cli GITHUB_REPOSITORY_ID=123456

ifdef VERSION
# VERSION is the version the binary reports. Unset, we fall back to the tag at
# HEAD so a local `make build` in a checked-out release still reports it; passed
# on the command line it wins outright, which is how CI stamps the version it
# resolved for the build (#1133). Only a command-line assignment counts, so an
# ambient VERSION cannot stamp a release: `origin` yields multi-word strings
# ("command line", "environment override"), so filter-out, not filter.
ifneq ($(filter-out command line override,$(origin VERSION)),)
BINARY_VERSION = $(GIT_TAG)
else
BINARY_VERSION = $(VERSION)
endif
BINARY_VERSION ?= ${GIT_TAG}

# Only set Version if building a tag or VERSION is set
# Only set Version if we have one; otherwise the binary keeps its "dev" default
ifneq ($(BINARY_VERSION),)
LDFLAGS += -X github.com/kosli-dev/cli/internal/version.version=${BINARY_VERSION}
endif

# With a version, report it alone; without one, "dev" plus the short sha.
VERSION_METADATA = $(GIT_SHA)
# Clear the short-sha BuildMetadata for tagged releases
ifneq ($(GIT_TAG),)
ifneq ($(BINARY_VERSION),)
VERSION_METADATA =
endif

Expand Down Expand Up @@ -180,8 +187,9 @@ follow_integration_test_server:
enter_integration_test_server:
@docker exec -it --workdir / cli_kosli_server bash

# The image never reads git tags itself, so pass the tag at HEAD in.
docker: ## Build CLI Docker image
@docker build -t kosli-cli .
@docker build -t kosli-cli --build-arg VERSION="$(GIT_TAG)" .

licenses: ## Update licenses
@rm -rf licenses || true
Expand Down
19 changes: 7 additions & 12 deletions scripts/docker-smoke-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@
# outcome to $RESULTS_FILE for the CI workflow to report as a Kosli
# attestation.
#
# Usage: IMAGE=... TAG=... RESULTS_FILE=... [EXPECTED_VERSION=...] \
# Usage: IMAGE=... TAG=... RESULTS_FILE=... EXPECTED_VERSION=... \
# ./scripts/docker-smoke-tests.sh
set -uo pipefail

IMAGE="${IMAGE:?IMAGE is required}"
TAG="${TAG:?TAG is required}"
RESULTS_FILE="${RESULTS_FILE:?RESULTS_FILE is required}"
EXPECTED_VERSION="${EXPECTED_VERSION:?EXPECTED_VERSION is required}"

REPO_ROOT="${GITHUB_WORKSPACE:-$(git rev-parse --show-toplevel)}"
if [ -z "$REPO_ROOT" ]; then
Expand Down Expand Up @@ -58,12 +59,11 @@ run_case() {
# Add a new smoke test by writing a test_* function below and adding one
# entry to the CASES array further down — no CI workflow changes needed.

# Asserts the image reports the release it was published as, built from a clean
# tree at the built commit — the two halves of #1133. EXPECTED_VERSION is the
# exact version required; unset for non-release builds, which are tagged with a
# sha and report dev+<sha>.
# Asserts the image reports the version it ships as, built from a clean tree at
# the built commit — the two halves of #1133. EXPECTED_VERSION is what CI baked in.
test_version() {
local full short commit

full="$(docker run --rm -e KOSLI_NO_UPDATE_CHECK=1 "${IMAGE}:${TAG}" version)" || return 1
echo "$full"

Expand All @@ -83,13 +83,8 @@ test_version() {
short="$(docker run --rm -e KOSLI_NO_UPDATE_CHECK=1 "${IMAGE}:${TAG}" version --short)" || return 1
echo "version --short: ${short}"

if [ -n "${EXPECTED_VERSION:-}" ]; then
if [ "$short" != "$EXPECTED_VERSION" ]; then
echo "expected version ${EXPECTED_VERSION}, got ${short}" >&2
return 1
fi
elif ! grep -qE '^dev\+[0-9a-f]{7,}$' <<< "$short"; then
echo "expected dev+<sha> for a non-release build, got ${short}" >&2
if [ "$short" != "$EXPECTED_VERSION" ]; then
echo "expected version ${EXPECTED_VERSION}, got ${short}" >&2
return 1
Comment thread
mbevc1 marked this conversation as resolved.
fi
}
Expand Down
Loading