diff --git a/.github/docker/.gitignore b/.github/docker/.gitignore new file mode 100644 index 000000000..567609b12 --- /dev/null +++ b/.github/docker/.gitignore @@ -0,0 +1 @@ +build/ diff --git a/.github/docker/cli.Dockerfile b/.github/docker/cli.Dockerfile new file mode 100644 index 000000000..d64700de0 --- /dev/null +++ b/.github/docker/cli.Dockerfile @@ -0,0 +1,34 @@ +# syntax=docker/dockerfile:1 + +ARG ALPINE_IMAGE +ARG BUILDARCH + +# Build stage - copy binaries from goreleaser output +FROM --platform=$BUILDARCH scratch AS dist +COPY dist/nix_linux_amd64_v1/temporal /dist/amd64/temporal +COPY dist/nix_linux_arm64_v8.0/temporal /dist/arm64/temporal + +# Stage to extract CA certificates and create user files +FROM ${ALPINE_IMAGE} AS certs +RUN apk add --no-cache ca-certificates && \ + adduser -u 1000 -D temporal + +# Final stage - minimal scratch-based image +FROM scratch + +ARG TARGETARCH + +# Copy CA certificates from certs stage +COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ + +# Copy passwd and group files for non-root user +COPY --from=certs /etc/passwd /etc/passwd +COPY --from=certs /etc/group /etc/group + +# Copy the appropriate binary for target architecture +COPY --from=dist /dist/$TARGETARCH/temporal /temporal + +# Run as non-root user temporal (uid 1000) +USER 1000:1000 + +ENTRYPOINT ["/temporal"] diff --git a/.github/docker/docker-bake.hcl b/.github/docker/docker-bake.hcl new file mode 100644 index 000000000..f890ae32e --- /dev/null +++ b/.github/docker/docker-bake.hcl @@ -0,0 +1,55 @@ +variable "IMAGE_REPO" { + default = "ghcr.io" +} + +variable "IMAGE_NAMESPACE" { + default = "" +} + +variable "IMAGE_NAME" { + default = "temporal" +} + +variable "IMAGE_SHA_TAG" {} + +variable "IMAGE_BRANCH_TAG" {} + +variable "CLI_SHA" { + default = "" +} + +variable "VERSION" { + default = "dev" +} + +variable "TAG_LATEST" { + default = false +} + +# Alpine base image with digest for reproducible builds +variable "ALPINE_IMAGE" { + default = "alpine:3.22@sha256:4b7ce07002c69e8f3d704a9c5d6fd3053be500b7f1c69fc0d80990c2ad8dd412" +} + +target "cli" { + dockerfile = ".github/docker/cli.Dockerfile" + context = "." + tags = compact([ + IMAGE_REPO == "" ? "${IMAGE_NAMESPACE}/${IMAGE_NAME}:${IMAGE_SHA_TAG}" : "${IMAGE_REPO}/${IMAGE_NAMESPACE}/${IMAGE_NAME}:${IMAGE_SHA_TAG}", + IMAGE_REPO == "" ? "${IMAGE_NAMESPACE}/${IMAGE_NAME}:${VERSION}" : "${IMAGE_REPO}/${IMAGE_NAMESPACE}/${IMAGE_NAME}:${VERSION}", + TAG_LATEST ? (IMAGE_REPO == "" ? "${IMAGE_NAMESPACE}/${IMAGE_NAME}:latest" : "${IMAGE_REPO}/${IMAGE_NAMESPACE}/${IMAGE_NAME}:latest") : "", + ]) + platforms = ["linux/amd64", "linux/arm64"] + args = { + ALPINE_IMAGE = "${ALPINE_IMAGE}" + } + labels = { + "org.opencontainers.image.title" = "temporal" + "org.opencontainers.image.description" = "Temporal CLI" + "org.opencontainers.image.url" = "https://github.com/temporalio/cli" + "org.opencontainers.image.source" = "https://github.com/temporalio/cli" + "org.opencontainers.image.licenses" = "MIT" + "org.opencontainers.image.revision" = "${CLI_SHA}" + "org.opencontainers.image.created" = timestamp() + } +} diff --git a/.github/workflows/build-and-publish.yml b/.github/workflows/build-and-publish.yml new file mode 100644 index 000000000..bec203da6 --- /dev/null +++ b/.github/workflows/build-and-publish.yml @@ -0,0 +1,235 @@ +name: Build and Publish (Reusable) + +on: + workflow_call: + inputs: + publish: + description: "Whether to publish the release and Docker image" + required: true + type: boolean + version: + description: "Version tag for the release (required if publish is true)" + required: false + type: string + registry: + description: "Container registry (docker.io, ghcr.io, etc.)" + required: false + type: string + default: "" + registry_namespace: + description: "Registry namespace/organization" + required: false + type: string + default: "" + image_name: + description: "Image name" + required: false + type: string + default: "temporal" + secrets: + DOCKER_USERNAME: + required: false + DOCKER_PASSWORD: + required: false + +jobs: + build: + name: Build and Publish + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + with: + fetch-depth: 0 + + - name: Set up Go + uses: actions/setup-go@fac708d6674e30b6ba41289acaab6d4b75aa0753 # v4.0.1 + with: + go-version-file: "go.mod" + check-latest: true + cache: true + + - name: Get build date + id: date + run: echo "date=$(date '+%F-%T')" >> $GITHUB_OUTPUT + + - name: Get build unix timestamp + id: timestamp + run: echo "timestamp=$(date '+%s')" >> $GITHUB_OUTPUT + + - name: Get git branch + id: branch + run: echo "branch=$(git rev-parse --abbrev-ref HEAD)" >> $GITHUB_OUTPUT + + - name: Get build platform + id: platform + run: echo "platform=$(go version | cut -d ' ' -f 4)" >> $GITHUB_OUTPUT + + - name: Get Go version + id: go + run: echo "go=$(go version | cut -d ' ' -f 3)" >> $GITHUB_OUTPUT + + - name: Run GoReleaser (release) + if: inputs.publish + uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0 + with: + version: v2.12.7 + args: release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BUILD_DATE: ${{ steps.date.outputs.date }} + BUILD_TS_UNIX: ${{ steps.timestamp.outputs.timestamp }} + GIT_BRANCH: ${{ steps.branch.outputs.branch }} + BUILD_PLATFORM: ${{ steps.platform.outputs.platform }} + GO_VERSION: ${{ steps.go.outputs.go }} + + - name: Run GoReleaser (snapshot) + if: ${{ !inputs.publish }} + uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0 + with: + version: v2.12.7 + args: release --snapshot --clean + env: + BUILD_DATE: ${{ steps.date.outputs.date }} + BUILD_TS_UNIX: ${{ steps.timestamp.outputs.timestamp }} + GIT_BRANCH: ${{ steps.branch.outputs.branch }} + BUILD_PLATFORM: ${{ steps.platform.outputs.platform }} + GO_VERSION: ${{ steps.go.outputs.go }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Get build metadata + id: meta + env: + INPUT_VERSION: ${{ inputs.version }} + INPUT_PUBLISH: ${{ inputs.publish }} + INPUT_REGISTRY: ${{ inputs.registry }} + INPUT_REGISTRY_NAMESPACE: ${{ inputs.registry_namespace }} + INPUT_IMAGE_NAME: ${{ inputs.image_name }} + REPO_OWNER: ${{ github.repository_owner }} + run: | + echo "cli_sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT + echo "image_sha_tag=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT + echo "image_branch_tag=$(git rev-parse --abbrev-ref HEAD)" >> $GITHUB_OUTPUT + + if [[ "$INPUT_PUBLISH" == "true" ]]; then + # Get version from input, strip 'v' prefix + VERSION="$INPUT_VERSION" + VERSION="${VERSION#v}" + echo "version=$VERSION" >> $GITHUB_OUTPUT + else + echo "version=snapshot" >> $GITHUB_OUTPUT + fi + + # Determine registry (with auto-detection for temporalio vs forks) + REGISTRY="$INPUT_REGISTRY" + if [[ -z "$REGISTRY" ]]; then + if [[ "$REPO_OWNER" == "temporalio" ]]; then + REGISTRY="docker.io" + else + REGISTRY="ghcr.io" + fi + fi + + # Determine registry type for authentication + if [[ "$REGISTRY" == "ghcr.io" ]]; then + echo "registry_type=ghcr" >> $GITHUB_OUTPUT + elif [[ "$REGISTRY" == "docker.io" ]]; then + echo "registry_type=dockerhub" >> $GITHUB_OUTPUT + else + echo "registry_type=other" >> $GITHUB_OUTPUT + fi + + # Set namespace (defaults to repository owner) + NAMESPACE="$INPUT_REGISTRY_NAMESPACE" + if [[ -z "$NAMESPACE" ]]; then + NAMESPACE="$REPO_OWNER" + fi + + # Set image name (defaults to 'temporal') + IMAGE_NAME="$INPUT_IMAGE_NAME" + if [[ -z "$IMAGE_NAME" ]]; then + IMAGE_NAME="temporal" + fi + + # For Docker Hub, use empty string as registry (special case) + if [[ "$REGISTRY" == "docker.io" ]]; then + echo "image_repo=" >> $GITHUB_OUTPUT + else + echo "image_repo=${REGISTRY}" >> $GITHUB_OUTPUT + fi + + echo "image_namespace=${NAMESPACE}" >> $GITHUB_OUTPUT + echo "image_name=${IMAGE_NAME}" >> $GITHUB_OUTPUT + + - name: Log in to GitHub Container Registry + if: inputs.publish && steps.meta.outputs.registry_type == 'ghcr' + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Log in to Docker Hub + if: inputs.publish && steps.meta.outputs.registry_type == 'dockerhub' + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Check if release is latest + if: inputs.publish + id: check_latest + uses: actions/github-script@v7 + with: + script: | + const { data: release } = await github.rest.repos.getReleaseByTag({ + owner: context.repo.owner, + repo: context.repo.repo, + tag: context.ref.replace('refs/tags/', '') + }); + // Tag as latest only if release is marked as latest (not pre-release) + core.setOutput('tag_latest', release.prerelease ? 'false' : 'true'); + console.log(`Release prerelease: ${release.prerelease}, tag_latest: ${!release.prerelease}`) + + - name: Build and push Docker image + if: inputs.publish + run: | + docker buildx bake \ + --file .github/docker/docker-bake.hcl \ + --push \ + cli + env: + CLI_SHA: ${{ steps.meta.outputs.cli_sha }} + IMAGE_SHA_TAG: ${{ steps.meta.outputs.image_sha_tag }} + IMAGE_BRANCH_TAG: ${{ steps.meta.outputs.image_branch_tag }} + VERSION: ${{ steps.meta.outputs.version }} + TAG_LATEST: ${{ steps.check_latest.outputs.tag_latest }} + IMAGE_REPO: ${{ steps.meta.outputs.image_repo }} + IMAGE_NAMESPACE: ${{ steps.meta.outputs.image_namespace }} + IMAGE_NAME: ${{ steps.meta.outputs.image_name }} + + - name: Build Docker image + if: ${{ !inputs.publish }} + run: | + docker buildx bake \ + --file .github/docker/docker-bake.hcl \ + cli + env: + CLI_SHA: ${{ steps.meta.outputs.cli_sha }} + IMAGE_SHA_TAG: ${{ steps.meta.outputs.image_sha_tag }} + IMAGE_BRANCH_TAG: ${{ steps.meta.outputs.image_branch_tag }} + VERSION: ${{ steps.meta.outputs.version }} + TAG_LATEST: false + IMAGE_REPO: ${{ steps.meta.outputs.image_repo }} + IMAGE_NAMESPACE: ${{ steps.meta.outputs.image_namespace }} + IMAGE_NAME: ${{ steps.meta.outputs.image_name }} + + - name: Upload build artifacts + if: ${{ !inputs.publish }} + uses: actions/upload-artifact@v4 + with: + name: temporal-cli-dist + path: dist/ + retention-days: 7 diff --git a/.github/workflows/build-docker-image.yml b/.github/workflows/build-docker-image.yml new file mode 100644 index 000000000..baa33d148 --- /dev/null +++ b/.github/workflows/build-docker-image.yml @@ -0,0 +1,15 @@ +name: Build Docker Image + +on: + pull_request: + push: + branches: + - main + +jobs: + build: + permissions: + contents: read + uses: ./.github/workflows/build-and-publish.yml + with: + publish: false diff --git a/.github/workflows/goreleaser.yml b/.github/workflows/goreleaser.yml index 3ef1d0b13..dfd79cedd 100644 --- a/.github/workflows/goreleaser.yml +++ b/.github/workflows/goreleaser.yml @@ -1,4 +1,4 @@ -name: goreleaser +name: Release on: workflow_dispatch: @@ -6,50 +6,15 @@ on: types: - published -jobs: - goreleaser: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 - with: - fetch-depth: 0 - - - name: Set up Go - uses: actions/setup-go@fac708d6674e30b6ba41289acaab6d4b75aa0753 # v4.0.1 - with: - go-version-file: "go.mod" - check-latest: true - - - name: Get build date - id: date - run: echo "::set-output name=date::$(date '+%F-%T')" - - - name: Get build unix timestamp - id: timestamp - run: echo "::set-output name=timestamp::$(date '+%s')" - - - name: Get git branch - id: branch - run: echo "::set-output name=branch::$(git rev-parse --abbrev-ref HEAD)" +permissions: + contents: write - - name: Get build platform - id: platform - run: echo "::set-output name=platform::$(go version | cut -d ' ' -f 4)" - - - name: Get Go version - id: go - run: echo "::set-output name=go::$(go version | cut -d ' ' -f 3)" - - - name: Run GoReleaser - uses: goreleaser/goreleaser-action@336e29918d653399e599bfca99fadc1d7ffbc9f7 # v4.3.0 - with: - version: v1.26.2 - args: release - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - BUILD_DATE: ${{ steps.date.outputs.date }} - BUILD_TS_UNIX: ${{ steps.timestamp.outputs.timestamp }} - GIT_BRANCH: ${{ steps.branch.outputs.branch }} - BUILD_PLATFORM: ${{ steps.platform.outputs.platform }} - GO_VERSION: ${{ steps.go.outputs.go }} +jobs: + release: + uses: ./.github/workflows/build-and-publish.yml + with: + publish: true + version: ${{ github.ref_name }} + secrets: + DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} + DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} diff --git a/.github/workflows/trigger-publish.yml b/.github/workflows/trigger-publish.yml deleted file mode 100644 index 6f65fefe4..000000000 --- a/.github/workflows/trigger-publish.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: 'Trigger Docker image build' - -on: - workflow_dispatch: - release: - types: [published] - -jobs: - trigger: - if: ${{ ! contains(github.ref, '-rc.') }} - name: 'trigger Docker image build' - runs-on: ubuntu-latest - - defaults: - run: - shell: bash - - steps: - - name: Generate a token - id: generate_token - uses: actions/create-github-app-token@v2 - with: - app-id: ${{ secrets.TEMPORAL_CICD_APP_ID }} - private-key: ${{ secrets.TEMPORAL_CICD_PRIVATE_KEY }} - owner: temporalio - repositories: | - cli - docker-builds - - - name: Dispatch docker builds Github Action - env: - PAT: ${{ steps.generate_token.outputs.token }} - PARENT_REPO: temporalio/docker-builds - PARENT_BRANCH: ${{ toJSON('main') }} - WORKFLOW_ID: update-submodules.yml - REPO: ${{ toJSON('cli') }} - BRANCH: ${{ toJSON('main') }} - COMMIT: ${{ toJSON(github.sha) }} - run: | - curl -fL -X POST -H "Accept: application/vnd.github.v3+json" -H "Authorization: token $PAT" "https://api.github.com/repos/$PARENT_REPO/actions/workflows/$WORKFLOW_ID/dispatches" -d '{"ref":'"$PARENT_BRANCH"', "inputs": { "repo":'"$REPO"', "branch":'"$BRANCH"', "commit": '"$COMMIT"' }}' diff --git a/.goreleaser.yml b/.goreleaser.yml index 9c94e1b4f..f3e29ced2 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,3 +1,5 @@ +version: 2 + before: hooks: - go mod download @@ -11,24 +13,26 @@ archives: - <<: &archive_defaults name_template: "temporal_cli_{{ .Version }}_{{ .Os }}_{{ .Arch }}" id: nix - builds: + ids: - nix - format: tar.gz + formats: + - tar.gz files: - LICENSE - <<: *archive_defaults id: windows-zip - builds: + ids: - windows - format: zip + formats: + - zip files: - LICENSE # used by SDKs as zip cannot be used by rust https://github.com/zip-rs/zip/issues/108 - <<: *archive_defaults id: windows-targz - builds: + ids: - windows files: - LICENSE @@ -61,7 +65,7 @@ checksum: algorithm: sha256 changelog: - skip: true + disable: true announce: skip: "true" diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 05c3938a3..000000000 --- a/Dockerfile +++ /dev/null @@ -1,12 +0,0 @@ -FROM --platform=$BUILDARCH scratch AS dist -COPY ./dist/nix_linux_amd64_v1/temporal /dist/amd64/temporal -COPY ./dist/nix_linux_arm64/temporal /dist/arm64/temporal - -FROM alpine:3.22 -ARG TARGETARCH -RUN apk add --no-cache ca-certificates -COPY --from=dist /dist/$TARGETARCH/temporal /usr/local/bin/temporal -RUN adduser -u 1000 -D temporal -USER temporal - -ENTRYPOINT ["temporal"] diff --git a/Makefile b/Makefile index 448ee5246..71de3c2d1 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: all gen build fmt-imports +.PHONY: all gen build fmt-imports update-alpine all: gen build @@ -9,3 +9,23 @@ internal/commands.gen.go: internal/commandsgen/commands.yml build: go build ./cmd/temporal + +update-alpine: ## Update Alpine base image to latest version and digest (usage: make update-alpine [ALPINE_TAG=3.22]) + @if [ -n "$(ALPINE_TAG)" ]; then \ + LATEST_TAG=$(ALPINE_TAG); \ + else \ + echo "Fetching latest Alpine version from Docker Hub..."; \ + LATEST_TAG=$$(curl -s https://registry.hub.docker.com/v2/repositories/library/alpine/tags\?page_size=100 | \ + jq -r '.results[].name' | grep -E '^3\.[0-9]+$$' | sort -V | tail -1); \ + fi && \ + echo "Alpine version: $$LATEST_TAG" && \ + DIGEST=$$(docker buildx imagetools inspect alpine:$$LATEST_TAG 2>/dev/null | grep "Digest:" | head -1 | awk '{print $$2}') && \ + DIGEST_HASH=$${DIGEST#sha256:} && \ + echo "Digest: sha256:$$DIGEST_HASH" && \ + ALPINE_FULL="alpine:$$LATEST_TAG@sha256:$$DIGEST_HASH" && \ + if sed --version 2>&1 | grep -q GNU; then \ + sed -i "s|default = \"alpine:[^\"]*\"|default = \"$$ALPINE_FULL\"|" .github/docker/docker-bake.hcl; \ + else \ + sed -i '' "s|default = \"alpine:[^\"]*\"|default = \"$$ALPINE_FULL\"|" .github/docker/docker-bake.hcl; \ + fi && \ + echo "Updated .github/docker/docker-bake.hcl with $$ALPINE_FULL"