fix: resolve docker image tags from git and auto-detect CLI version (#9411)

## Summary

- Resolve git ref and SHA from the checked-out tree (`git rev-parse`,
`git symbolic-ref`) instead of `GITHUB_REF`/`GITHUB_SHA`, which point to
the dispatch branch for `workflow_dispatch` events
- Auto-detect CLI version from the downloaded binary via `temporal
--version` fixing the admin-tools OCI label showing "unknown"
- Remove `ref` input from the manual docker build workflow -- users
should dispatch from the branch they want to build from
This commit is contained in:
Alex Stanfield
2026-03-10 18:33:59 -05:00
committed by GitHub
parent 15725d44a5
commit c3db52acf5
5 changed files with 183 additions and 69 deletions

View File

@@ -23,9 +23,9 @@ inputs:
required: false
default: "false"
cli-version:
description: "Temporal CLI version to download"
description: "Temporal CLI version to download (uses default from build helper if empty)"
required: false
default: "1.5.1"
default: ""
alpine-tag:
description: "Alpine base image tag"
required: false
@@ -37,6 +37,14 @@ inputs:
description: "Docker Hub token"
required: false
outputs:
branch-tag:
description: "Docker-safe branch tag (e.g., branch-main, branch-release-v1.30.0)"
value: ${{ steps.image-tags.outputs.tag }}
sha-tag:
description: "Docker SHA tag (e.g., sha-abc1234)"
value: ${{ steps.image-tags.outputs.sha }}
runs:
using: composite
steps:
@@ -70,12 +78,19 @@ runs:
run: |
.github/actions/build-docker-images/scripts/docker-build-helper download-cli
- name: Extract CLI version from binary
id: extract-cli-version
shell: bash
working-directory: ${{ github.workspace }}
run: |
.github/actions/build-docker-images/scripts/docker-build-helper extract-binary-version temporal cli-version
- name: Extract server version from binary
id: extract-version
shell: bash
working-directory: ${{ github.workspace }}
run: |
.github/actions/build-docker-images/scripts/docker-build-helper extract-version
.github/actions/build-docker-images/scripts/docker-build-helper extract-binary-version temporal-server server-version
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
@@ -98,10 +113,11 @@ runs:
IMAGE_REPO: temporaliotest
IMAGE_SHA_TAG: ${{ steps.image-tags.outputs.sha }}
IMAGE_BRANCH_TAG: ${{ steps.image-tags.outputs.tag }}
TEMPORAL_SHA: ${{ github.sha }}
TEMPORAL_SHA: ${{ steps.image-tags.outputs.git-sha }}
TAG_LATEST: ${{ inputs.tag-latest }}
ALPINE_TAG: ${{ inputs.alpine-tag }}
SERVER_VERSION: ${{ steps.extract-version.outputs.server-version }}
CLI_VERSION: ${{ steps.extract-cli-version.outputs.cli-version }}
run: |
if [ -n "${{ inputs.platform }}" ]; then
docker buildx bake \
@@ -124,10 +140,11 @@ runs:
IMAGE_REPO: temporaliotest
IMAGE_SHA_TAG: ${{ steps.image-tags.outputs.sha }}
IMAGE_BRANCH_TAG: ${{ steps.image-tags.outputs.tag }}
TEMPORAL_SHA: ${{ github.sha }}
TEMPORAL_SHA: ${{ steps.image-tags.outputs.git-sha }}
TAG_LATEST: ${{ inputs.tag-latest }}
ALPINE_TAG: ${{ inputs.alpine-tag }}
SERVER_VERSION: ${{ steps.extract-version.outputs.server-version }}
CLI_VERSION: ${{ steps.extract-cli-version.outputs.cli-version }}
run: |
docker buildx bake \
--push \

View File

@@ -23,7 +23,7 @@ func main() {
fmt.Fprintf(os.Stderr, " set-image-tags - Generate Docker image tags from branch and SHA\n")
fmt.Fprintf(os.Stderr, " organize-binaries - Organize binaries for Docker\n")
fmt.Fprintf(os.Stderr, " download-cli - Download Temporal CLI\n")
fmt.Fprintf(os.Stderr, " extract-version - Extract version from temporal-server binary\n")
fmt.Fprintf(os.Stderr, " extract-binary-version <binary-name> <output-name> - Extract version from a binary\n")
os.Exit(1)
}
@@ -45,8 +45,12 @@ func main() {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
case "extract-version":
if err := extractVersion(); err != nil {
case "extract-binary-version":
if len(os.Args) != 4 {
fmt.Fprintf(os.Stderr, "Usage: %s extract-binary-version <binary-name> <output-name>\n", os.Args[0])
os.Exit(1)
}
if err := extractBinaryVersion(os.Args[2], os.Args[3]); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
@@ -56,18 +60,37 @@ func main() {
}
}
// setImageTags generates Docker image tags from branch name and commit SHA
func setImageTags() error {
// Get GITHUB_REF from environment
ref := os.Getenv("GITHUB_REF")
if ref == "" {
return fmt.Errorf("GITHUB_REF environment variable not set")
// resolveGitInfo resolves the current git ref and SHA from the working tree.
func resolveGitInfo() (ref string, sha string, err error) {
shaCmd := exec.Command("git", "rev-parse", "HEAD")
shaOut, err := shaCmd.Output()
if err != nil {
return "", "", fmt.Errorf("failed to resolve git SHA: %w", err)
}
sha = strings.TrimSpace(string(shaOut))
// Use the symbolic ref (branch name) if available, otherwise fall back
// to a tag name or the raw SHA.
refCmd := exec.Command("git", "symbolic-ref", "HEAD")
if refOut, err := refCmd.Output(); err == nil {
ref = strings.TrimSpace(string(refOut))
} else {
tagCmd := exec.Command("git", "describe", "--tags", "--exact-match", "HEAD")
if tagOut, tagErr := tagCmd.Output(); tagErr == nil {
ref = strings.TrimSpace(string(tagOut))
} else {
ref = sha
}
}
// Get GITHUB_SHA from environment
sha := os.Getenv("GITHUB_SHA")
if sha == "" {
return fmt.Errorf("GITHUB_SHA environment variable not set")
return ref, sha, nil
}
// setImageTags generates Docker image tags from branch name and commit SHA
func setImageTags() error {
ref, sha, err := resolveGitInfo()
if err != nil {
return err
}
// Remove refs/heads/ or refs/tags/ prefix
@@ -119,6 +142,9 @@ func setImageTags() error {
if err := setOutput("sha", shaTag); err != nil {
return fmt.Errorf("failed to set sha output: %w", err)
}
if err := setOutput("git-sha", sha); err != nil {
return fmt.Errorf("failed to set git-sha output: %w", err)
}
return nil
}
@@ -403,50 +429,57 @@ func downloadCLIForArch(arch string) error {
return nil
}
// extractVersion extracts the version from the temporal-server binary
func extractVersion() error {
// Try to find the temporal-server binary in any available architecture directory
var binaryPath string
// findBuildBinary finds a binary by name in the docker/build/{arch}/ directories.
func findBuildBinary(name string) (string, error) {
for _, arch := range validArchs {
candidatePath := filepath.Join("docker", "build", arch, "temporal-server")
candidatePath := filepath.Join("docker", "build", arch, name)
if _, err := os.Stat(candidatePath); err == nil {
binaryPath = candidatePath
break
return candidatePath, nil
}
}
return "", fmt.Errorf("%s binary not found in docker/build/{amd64,arm64}/", name)
}
if binaryPath == "" {
return fmt.Errorf("temporal-server binary not found in docker/build/{amd64,arm64}/")
// extractBinaryVersion finds a binary, runs --version, parses the output, and sets a GitHub Actions output.
func extractBinaryVersion(binaryName, outputName string) error {
binaryPath, err := findBuildBinary(binaryName)
if err != nil {
return err
}
fmt.Printf("Extracting version from %s\n", binaryPath)
// Run the binary with --version flag
cmd := exec.Command(binaryPath, "--version")
output, err := cmd.Output()
if err != nil {
return fmt.Errorf("failed to run %s --version: %w", binaryPath, err)
}
// Parse the version from output like "temporal version 1.29.0"
outputStr := strings.TrimSpace(string(output))
versionRegex := regexp.MustCompile(`^temporal version (\d+\.\d+\.\d+)`)
matches := versionRegex.FindStringSubmatch(outputStr)
if len(matches) < 2 {
return fmt.Errorf("failed to parse version from output: %s", outputStr)
version, err := parseTemporalVersion(string(output))
if err != nil {
return err
}
version := matches[1]
fmt.Printf("Extracted version: %s\n", version)
// Set output for GitHub Actions
if err := setOutput("server-version", version); err != nil {
if err := setOutput(outputName, version); err != nil {
return fmt.Errorf("failed to set output: %w", err)
}
return nil
}
// parseTemporalVersion extracts the version from output like
// "temporal version 1.29.0" or "temporal version 0.0.0-DEV (Server 1.30.1, UI 2.45.3)"
func parseTemporalVersion(output string) (string, error) {
s := strings.TrimSpace(output)
re := regexp.MustCompile(`^temporal version\s+(\d+\.\d+\.\d+\S*)`)
matches := re.FindStringSubmatch(s)
if len(matches) < 2 {
return "", fmt.Errorf("failed to parse version from output: %s", s)
}
return matches[1], nil
}
// Helper functions
func setOutput(name, value string) error {

View File

@@ -0,0 +1,78 @@
package main
import (
"testing"
)
func TestParseTemporalVersion(t *testing.T) {
tests := []struct {
name string
output string
want string
wantErr bool
}{
{
name: "server version",
output: "temporal version 1.29.0",
want: "1.29.0",
},
{
name: "cli release version",
output: "temporal version 1.6.0 (Server 1.30.0, UI 2.45.0)",
want: "1.6.0",
},
{
name: "cli dev version",
output: "temporal version 0.0.0-DEV (Server 1.30.1, UI 2.45.3)",
want: "0.0.0-DEV",
},
{
name: "with trailing newline",
output: "temporal version 1.6.0 (Server 1.30.0, UI 2.45.0)\n",
want: "1.6.0",
},
{
name: "with leading and trailing whitespace",
output: " \n temporal version 1.6.0 (Server 1.30.0, UI 2.45.0) \n ",
want: "1.6.0",
},
{
name: "pre-release version",
output: "temporal version 1.31.0-151.5",
want: "1.31.0-151.5",
},
{
name: "extra spaces between version and number",
output: "temporal version 1.6.1",
want: "1.6.1",
},
{
name: "empty output",
output: "",
wantErr: true,
},
{
name: "unexpected format",
output: "something else entirely",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := parseTemporalVersion(tt.output)
if tt.wantErr {
if err == nil {
t.Fatalf("expected error, got version %q", got)
}
} else {
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got != tt.want {
t.Fatalf("got %q, want %q", got, tt.want)
}
}
})
}
}

View File

@@ -1,16 +1,9 @@
name: Manual Docker Build
# Dispatch this workflow from the branch you want to build from.
on:
workflow_dispatch:
inputs:
ref:
description: "Git ref (branch, tag, or SHA) to build from"
required: true
default: "main"
cli-version:
description: "Temporal CLI version to include in images"
required: true
default: "1.5.1"
alpine-tag:
description: "Alpine base image tag (e.g., 3.23.3)"
required: true
@@ -45,7 +38,6 @@ jobs:
- name: Checkout code
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref }}
fetch-depth: 0
- name: Determine single-arch parameter
@@ -66,40 +58,46 @@ jobs:
single-arch: ${{ steps.arch-param.outputs.single-arch }}
- name: Build Docker images
id: build-docker
if: ${{ !inputs.push }}
uses: ./.github/actions/build-docker-images
with:
push: false
tag-latest: ${{ inputs.tag-latest }}
platform: ${{ inputs.platform }}
cli-version: ${{ inputs.cli-version }}
alpine-tag: ${{ inputs.alpine-tag }}
load: ${{ inputs.platform == 'linux/amd64' || inputs.platform == '' }}
- name: Build and push Docker images
id: push-docker
if: ${{ inputs.push }}
uses: ./.github/actions/build-docker-images
with:
push: true
tag-latest: ${{ inputs.tag-latest }}
platform: ${{ inputs.platform }}
cli-version: ${{ inputs.cli-version }}
alpine-tag: ${{ inputs.alpine-tag }}
dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }}
dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Output image tags
env:
SHA_TAG: ${{ steps.build-docker.outputs.sha-tag || steps.push-docker.outputs.sha-tag }}
BRANCH_TAG: ${{ steps.build-docker.outputs.branch-tag || steps.push-docker.outputs.branch-tag }}
run: |
{
echo "### Docker Images Built"
echo ""
echo "**Git Ref:** ${{ inputs.ref }}"
echo "**CLI Version:** ${{ inputs.cli-version }}"
echo "**Branch:** ${{ github.ref_name }}"
echo "**SHA Tag:** ${SHA_TAG}"
echo "**Branch Tag:** ${BRANCH_TAG}"
echo "**Platform:** ${{ inputs.platform || 'linux/amd64,linux/arm64' }}"
echo "**Pushed to Docker Hub:** ${{ inputs.push }}"
echo "**Tagged as latest:** ${{ inputs.tag-latest }}"
echo ""
echo "**Image Tags:**"
echo "- temporaliotest/server:sha-${GITHUB_SHA:0:7}"
echo "- temporaliotest/admin-tools:sha-${GITHUB_SHA:0:7}"
echo "- temporaliotest/server:${SHA_TAG}"
echo "- temporaliotest/admin-tools:${SHA_TAG}"
echo "- temporaliotest/server:${BRANCH_TAG}"
echo "- temporaliotest/admin-tools:${BRANCH_TAG}"
} >> "$GITHUB_STEP_SUMMARY"

View File

@@ -32,6 +32,7 @@ jobs:
single-arch: amd64
- name: Build Docker images
id: build-docker
uses: ./.github/actions/build-docker-images
with:
push: false
@@ -39,24 +40,11 @@ jobs:
platform: linux/amd64
load: true
- name: Get Docker image tag
id: image-tag
uses: actions/github-script@v8
with:
script: |
const ref = context.ref.replace('refs/heads/', '').replace('refs/tags/', '');
const branchName = `branch-${ref}`;
let safeTag = branchName.replace(/[^a-zA-Z0-9._-]/g, '-');
safeTag = safeTag.toLowerCase();
safeTag = safeTag.replace(/^[^a-z0-9]+/, '');
safeTag = safeTag.substring(0, 128);
core.setOutput('tag', safeTag);
- name: Save Docker image as artifact
run: |
docker save temporaliotest/server:${{ steps.image-tag.outputs.tag }} -o /tmp/temporal-server.tar
docker save temporaliotest/admin-tools:${{ steps.image-tag.outputs.tag }} -o /tmp/temporal-admin-tools.tar
echo ${{ steps.image-tag.outputs.tag }} > /tmp/image_tag
docker save temporaliotest/server:${{ steps.build-docker.outputs.branch-tag }} -o /tmp/temporal-server.tar
docker save temporaliotest/admin-tools:${{ steps.build-docker.outputs.branch-tag }} -o /tmp/temporal-admin-tools.tar
echo ${{ steps.build-docker.outputs.branch-tag }} > /tmp/image_tag
- name: Prepare artifact
working-directory: ${{ github.workspace }}