diff --git a/.github/workflows/build-and-publish.yml b/.github/workflows/build-and-publish.yml new file mode 100644 index 000000000..bf0eea9e8 --- /dev/null +++ b/.github/workflows/build-and-publish.yml @@ -0,0 +1,243 @@ +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@v3 + with: + fetch-depth: 0 + + - name: Set up Go + uses: actions/setup-go@v4 + 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: Check if release is latest + if: inputs.publish + id: check_latest_release + uses: actions/github-script@v7 + with: + script: | + const releaseTag = '${{ inputs.version }}'; + const { data: release } = await github.rest.repos.getReleaseByTag({ + owner: context.repo.owner, + repo: context.repo.repo, + tag: releaseTag + }); + + const isLatest = !release.prerelease && !release.draft; + core.setOutput('is_latest', isLatest); + console.log(`Release: ${release.tag_name}`); + console.log(`Prerelease: ${release.prerelease}, Draft: ${release.draft}`); + console.log(`Should tag as latest: ${isLatest}`); + + - name: Run GoReleaser (release) + if: inputs.publish + uses: goreleaser/goreleaser-action@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@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 }} + uses: actions/github-script@v7 + with: + script: | + const inputVersion = process.env.INPUT_VERSION; + const inputPublish = process.env.INPUT_PUBLISH; + const inputRegistry = process.env.INPUT_REGISTRY; + const inputRegistryNamespace = process.env.INPUT_REGISTRY_NAMESPACE; + const inputImageName = process.env.INPUT_IMAGE_NAME; + const repoOwner = process.env.REPO_OWNER; + + // Get git information + const { execSync } = require('child_process'); + const cliSha = execSync('git rev-parse HEAD', { encoding: 'utf8' }).trim(); + const imageShaTag = execSync('git rev-parse --short HEAD', { encoding: 'utf8' }).trim(); + const imageBranchTag = execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf8' }).trim(); + + core.setOutput('cli_sha', cliSha); + core.setOutput('image_sha_tag', imageShaTag); + core.setOutput('image_branch_tag', imageBranchTag); + + // Determine version + let version; + if (inputPublish === 'true') { + // Get version from input, strip 'v' prefix + version = inputVersion.startsWith('v') ? inputVersion.slice(1) : inputVersion; + } else { + version = 'snapshot'; + } + core.setOutput('version', version); + + // Determine registry (with auto-detection for temporalio vs forks) + let registry = inputRegistry; + if (!registry) { + if (repoOwner === 'temporalio') { + registry = 'docker.io'; + } else { + registry = 'ghcr.io'; + } + } + + // Determine registry type for authentication + let registryType; + if (registry === 'ghcr.io') { + registryType = 'ghcr'; + } else if (registry === 'docker.io') { + registryType = 'dockerhub'; + } else { + registryType = 'other'; + } + core.setOutput('registry_type', registryType); + + // Set namespace (defaults to repository owner) + const namespace = inputRegistryNamespace || repoOwner; + core.setOutput('image_namespace', namespace); + + // Set image name (defaults to 'temporal') + const imageName = inputImageName || 'temporal'; + core.setOutput('image_name', imageName); + + // For Docker Hub, use empty string as registry (special case) + const imageRepo = registry === 'docker.io' ? '' : registry; + core.setOutput('image_repo', imageRepo); + + console.log(`Registry: ${registry}, Type: ${registryType}, Namespace: ${namespace}, Image: ${imageName}`); + + - 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: Build and push Docker image + if: inputs.publish + run: | + docker buildx bake \ + --file 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_release.outputs.is_latest == 'true' }} + IMAGE_REPO: ${{ steps.meta.outputs.image_repo }} + IMAGE_NAMESPACE: ${{ steps.meta.outputs.image_namespace }} + IMAGE_NAME: ${{ steps.meta.outputs.image_name }} + GITHUB_REPOSITORY: ${{ github.repository }} + + - name: Build Docker image + if: ${{ !inputs.publish }} + run: | + docker buildx bake \ + --file 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 }} + GITHUB_REPOSITORY: ${{ github.repository }} 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..100129438 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,16 @@ 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 + packages: 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-docs.yml b/.github/workflows/trigger-docs.yml index 645d976b5..b39848cb3 100644 --- a/.github/workflows/trigger-docs.yml +++ b/.github/workflows/trigger-docs.yml @@ -5,6 +5,7 @@ on: types: [published] jobs: update: + if: github.repository == 'temporalio/cli' runs-on: ubuntu-latest defaults: run: 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/.github/workflows/update-latest-tag.yml b/.github/workflows/update-latest-tag.yml new file mode 100644 index 000000000..b226ee55e --- /dev/null +++ b/.github/workflows/update-latest-tag.yml @@ -0,0 +1,127 @@ +name: Update Latest Docker Tag + +on: + release: + types: + - edited + - released + +permissions: + contents: read + packages: write + +jobs: + update-latest: + name: Update Latest Tag + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + ref: ${{ github.event.release.tag_name }} + + - name: Check if release is latest + id: check_latest + env: + RELEASE_TAG: ${{ github.event.release.tag_name }} + uses: actions/github-script@v7 + with: + script: | + const releaseTag = process.env.RELEASE_TAG; + const { data: release } = await github.rest.repos.getReleaseByTag({ + owner: context.repo.owner, + repo: context.repo.repo, + tag: releaseTag + }); + + const isLatest = !release.prerelease && !release.draft; + core.setOutput('is_latest', isLatest); + console.log(`Release: ${release.tag_name}`); + console.log(`Prerelease: ${release.prerelease}, Draft: ${release.draft}`); + console.log(`Should tag as latest: ${isLatest}`); + + - name: Set up Docker Buildx + if: steps.check_latest.outputs.is_latest == 'true' + uses: docker/setup-buildx-action@v3 + + - name: Get registry configuration + if: steps.check_latest.outputs.is_latest == 'true' + id: registry + uses: actions/github-script@v7 + with: + script: | + const repoOwner = context.repo.owner; + + // Auto-detect registry based on repository owner + let registry, type, repo; + if (repoOwner === 'temporalio') { + registry = 'docker.io'; + type = 'dockerhub'; + repo = ''; + } else { + registry = 'ghcr.io'; + type = 'ghcr'; + repo = registry; + } + + core.setOutput('type', type); + core.setOutput('repo', repo); + core.setOutput('namespace', repoOwner); + core.setOutput('image', 'temporal'); + + console.log(`Registry: ${registry}, Type: ${type}, Namespace: ${repoOwner}`); + + - name: Log in to GitHub Container Registry + if: steps.check_latest.outputs.is_latest == 'true' && steps.registry.outputs.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: steps.check_latest.outputs.is_latest == 'true' && steps.registry.outputs.type == 'dockerhub' + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Get version tag + if: steps.check_latest.outputs.is_latest == 'true' + id: version + env: + RELEASE_TAG: ${{ github.event.release.tag_name }} + run: | + VERSION="$RELEASE_TAG" + VERSION="${VERSION#v}" + echo "version=${VERSION}" >> $GITHUB_OUTPUT + + - name: Pull and retag image as latest + if: steps.check_latest.outputs.is_latest == 'true' + run: | + # Construct image paths + REPO="${{ steps.registry.outputs.repo }}" + NAMESPACE="${{ steps.registry.outputs.namespace }}" + IMAGE="${{ steps.registry.outputs.image }}" + VERSION="${{ steps.version.outputs.version }}" + + if [[ -z "$REPO" ]]; then + # Docker Hub format + SOURCE_IMAGE="${NAMESPACE}/${IMAGE}:${VERSION}" + LATEST_IMAGE="${NAMESPACE}/${IMAGE}:latest" + else + # Other registries + SOURCE_IMAGE="${REPO}/${NAMESPACE}/${IMAGE}:${VERSION}" + LATEST_IMAGE="${REPO}/${NAMESPACE}/${IMAGE}:latest" + fi + + echo "Pulling ${SOURCE_IMAGE}..." + docker pull ${SOURCE_IMAGE} + + echo "Tagging as ${LATEST_IMAGE}..." + docker tag ${SOURCE_IMAGE} ${LATEST_IMAGE} + + echo "Pushing ${LATEST_IMAGE}..." + docker push ${LATEST_IMAGE} + + echo "✅ Successfully updated latest tag to point to version ${VERSION}" 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 index 05c3938a3..23de4ed74 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,16 @@ -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 +# Intermediate stage to normalize goreleaser output paths +# This copies both architecture binaries and renames them to clean paths, +# allowing the final stage to select the correct binary using TARGETARCH +FROM scratch AS dist +COPY dist/nix_linux_amd64_v1/temporal /dist/amd64/temporal +COPY dist/nix_linux_arm64_v8.0/temporal /dist/arm64/temporal + +FROM alpine:3.22@sha256:4b7ce07002c69e8f3d704a9c5d6fd3053be500b7f1c69fc0d80990c2ad8dd412 -FROM alpine:3.22 ARG TARGETARCH + RUN apk add --no-cache ca-certificates -COPY --from=dist /dist/$TARGETARCH/temporal /usr/local/bin/temporal +COPY --chmod=755 --from=dist /dist/${TARGETARCH}/temporal /usr/local/bin/temporal RUN adduser -u 1000 -D temporal USER temporal diff --git a/Makefile b/Makefile deleted file mode 100644 index 448ee5246..000000000 --- a/Makefile +++ /dev/null @@ -1,11 +0,0 @@ -.PHONY: all gen build fmt-imports - -all: gen build - -gen: internal/commands.gen.go - -internal/commands.gen.go: internal/commandsgen/commands.yml - go run ./internal/cmd/gen-commands - -build: - go build ./cmd/temporal diff --git a/docker-bake.hcl b/docker-bake.hcl new file mode 100644 index 000000000..b4343e7ee --- /dev/null +++ b/docker-bake.hcl @@ -0,0 +1,53 @@ +variable "IMAGE_REPO" { + default = "ghcr.io" +} + +variable "IMAGE_NAMESPACE" { + default = "" +} + +variable "IMAGE_NAME" { + default = "temporal" +} + +variable "GITHUB_REPOSITORY" { + default = "temporalio/cli" +} + +variable "IMAGE_SHA_TAG" {} + +variable "IMAGE_BRANCH_TAG" {} + +variable "CLI_SHA" { + default = "" +} + +variable "VERSION" { + default = "dev" +} + +variable "TAG_LATEST" { + default = false +} + + + +target "cli" { + dockerfile = "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"] + labels = { + "org.opencontainers.image.title" = "temporal" + "org.opencontainers.image.description" = "Temporal CLI" + "org.opencontainers.image.url" = "https://github.com/${GITHUB_REPOSITORY}" + "org.opencontainers.image.source" = "https://github.com/${GITHUB_REPOSITORY}" + "org.opencontainers.image.licenses" = "MIT" + "org.opencontainers.image.revision" = "${CLI_SHA}" + "org.opencontainers.image.created" = timestamp() + } +}