Files
coder/.github/workflows/release.yaml
T
Garrett Delfosse b95697a370 ci: rewrite release workflow to be fully GitHub Actions-driven (#25162)
Replace the local interactive release CLI and legacy shell scripts with
a non-interactive Go tool (`scripts/release-action/`) and a rewritten
`release.yaml` workflow. Release managers trigger releases from the
GitHub Actions UI by selecting a branch, picking a release type (`rc`,
`release`, or `create-release-branch`), and optionally providing a
commit SHA.

The Go tool has four subcommands: `calculate-version` (computes next
version from git state), `generate-notes` (release notes from commit log
and PR metadata), `publish` (creates GitHub release with checksums), and
the workflow handles tag creation, branch creation, building, and
downstream publishing.

`scripts/version.sh` fallback now uses `git describe` (nearest ancestor
tag) instead of global latest so dev builds on release branches show the
correct version series.
2026-06-04 14:38:48 -04:00

928 lines
36 KiB
YAML

# GitHub release workflow.
name: Release
on:
workflow_dispatch:
inputs:
release_type:
type: choice
description: "Type of release (use 'Use workflow from' to pick the branch)"
required: true
options:
- rc
- release
- create-release-branch
commit_sha:
description: "Optional: commit SHA to tag (defaults to HEAD of selected branch)"
type: string
default: ""
permissions:
contents: read
concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs:
# Only allow maintainers/admins to release.
check-perms:
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-8' || 'ubuntu-latest' }}
steps:
- name: Allow only maintainers/admins
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const {data} = await github.rest.repos.getCollaboratorPermissionLevel({
owner: context.repo.owner,
repo: context.repo.repo,
username: context.actor
});
const role = data.role_name || data.user?.role_name || data.permission;
const perms = data.user?.permissions || {};
core.info(`Actor ${context.actor} permission=${data.permission}, role_name=${role}`);
const allowed =
role === 'admin' ||
role === 'maintain' ||
perms.admin === true ||
perms.maintain === true;
if (!allowed) core.setFailed('Denied: requires maintain or admin');
prepare-release:
name: Prepare release
needs: [check-perms]
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-8' || 'ubuntu-latest' }}
permissions:
contents: write
outputs:
version: ${{ steps.prepare.outputs.version }}
previous_version: ${{ steps.prepare.outputs.previous_version }}
stable: ${{ steps.prepare.outputs.stable }}
target_ref: ${{ steps.prepare.outputs.target_ref }}
create_branch: ${{ steps.prepare.outputs.create_branch }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0
with:
egress-policy: audit
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
persist-credentials: true
- name: Fetch git tags
run: git fetch --tags --force
- name: Setup Go
uses: ./.github/actions/setup-go
with:
use-cache: false
- name: Calculate version and create tag
id: prepare
env:
RELEASE_TYPE: ${{ inputs.release_type }}
REF_NAME: ${{ github.ref_name }}
COMMIT_SHA: ${{ inputs.commit_sha }}
run: |
set -euo pipefail
args=(--type "$RELEASE_TYPE" --ref "$REF_NAME")
if [[ -n "$COMMIT_SHA" ]]; then
args+=(--commit "$COMMIT_SHA")
fi
output=$(go run ./scripts/release-action calculate-version "${args[@]}")
echo "Raw output: $output"
version=$(echo "$output" | jq -r '.version')
previous_version=$(echo "$output" | jq -r '.previous_version')
stable=$(echo "$output" | jq -r '.stable')
target_ref=$(echo "$output" | jq -r '.target_ref')
create_branch=$(echo "$output" | jq -r '.create_branch // empty')
# Validate required outputs are non-empty.
for var in version previous_version target_ref; do
eval "val=\$$var"
if [[ -z "$val" || "$val" == "null" ]]; then
echo "::error::calculate-version returned empty or null '$var'"
exit 1
fi
done
{
echo "version=$version"
echo "previous_version=$previous_version"
echo "stable=$stable"
echo "target_ref=$target_ref"
echo "create_branch=$create_branch"
} >> "$GITHUB_OUTPUT"
{
echo "### Release preparation"
echo "| Field | Value |"
echo "|-------|-------|"
echo "| Version | \`$version\` |"
echo "| Previous | \`$previous_version\` |"
echo "| Stable | \`$stable\` |"
echo "| Target ref | \`$target_ref\` |"
if [[ -n "$create_branch" ]]; then
echo "| Create branch | \`$create_branch\` |"
fi
} >> "$GITHUB_STEP_SUMMARY"
- name: Create and push tag
env:
VERSION: ${{ steps.prepare.outputs.version }}
TARGET_REF: ${{ steps.prepare.outputs.target_ref }}
run: |
set -euo pipefail
# Skip if tag already exists (idempotent)
if git rev-parse "$VERSION" >/dev/null 2>&1; then
echo "Tag $VERSION already exists, skipping."
exit 0
fi
git tag -a "$VERSION" -m "Release $VERSION" "$TARGET_REF"
git push origin "$VERSION"
- name: Create release branch
if: ${{ steps.prepare.outputs.create_branch != '' }}
env:
CREATE_BRANCH: ${{ steps.prepare.outputs.create_branch }}
TARGET_REF: ${{ steps.prepare.outputs.target_ref }}
run: |
set -euo pipefail
# Skip if branch already exists
if git ls-remote --exit-code origin "refs/heads/$CREATE_BRANCH" >/dev/null 2>&1; then
echo "Branch $CREATE_BRANCH already exists, skipping."
exit 0
fi
git branch "$CREATE_BRANCH" "$TARGET_REF"
git push origin "$CREATE_BRANCH"
- name: Generate release notes
env:
VERSION: ${{ steps.prepare.outputs.version }}
PREV_VERSION: ${{ steps.prepare.outputs.previous_version }}
run: |
set -euo pipefail
go run ./scripts/release-action generate-notes \
--version "$VERSION" \
--previous-version "$PREV_VERSION" > /tmp/release_notes.md
- name: Upload release notes
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: release-notes
path: /tmp/release_notes.md
retention-days: 30
release:
name: Build and publish
needs: [check-perms, prepare-release]
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-8' || 'ubuntu-latest' }}
permissions:
# Required to publish a release
contents: write
# Necessary to push docker images to ghcr.io.
packages: write
# Necessary for GCP authentication (https://github.com/google-github-actions/setup-gcloud#usage)
# Also necessary for keyless cosign (https://docs.sigstore.dev/cosign/signing/overview/)
# And for GitHub Actions attestation
id-token: write
# Required for GitHub Actions attestation
attestations: write
env:
CODER_RELEASE: "true"
CODER_RELEASE_STABLE: ${{ needs.prepare-release.outputs.stable }}
# Necessary for Docker manifest
DOCKER_CLI_EXPERIMENTAL: "enabled"
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0
with:
egress-policy: audit
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
persist-credentials: false
# If the event that triggered the build was an annotated tag (which our
# tags are supposed to be), actions/checkout has a bug where the tag in
# question is only a lightweight tag and not a full annotated tag. This
# command seems to fix it.
# https://github.com/actions/checkout/issues/290
- name: Fetch git tags
run: git fetch --tags --force
- name: Checkout release commit
env:
VERSION: ${{ needs.prepare-release.outputs.version }}
run: |
set -euo pipefail
git checkout "refs/tags/$VERSION"
- name: Print version
id: version
env:
VERSION: ${{ needs.prepare-release.outputs.version }}
run: |
set -euo pipefail
# VERSION comes from the env block, not a misspelling of the local 'version'.
# shellcheck disable=SC2153
# Strip the "v" prefix for use in build steps.
version="${VERSION#v}"
echo "version=$version" >> "$GITHUB_OUTPUT"
# Speed up future version.sh calls.
echo "CODER_FORCE_VERSION=$version" >> "$GITHUB_ENV"
echo "$version"
- name: Download release notes
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
with:
name: release-notes
path: /tmp
- name: Set release notes env
run: echo CODER_RELEASE_NOTES_FILE=/tmp/release_notes.md >> "$GITHUB_ENV"
- name: Show release notes
run: |
set -euo pipefail
cat "$CODER_RELEASE_NOTES_FILE"
- name: Docker Login
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up mise tools
uses: ./.github/actions/setup-mise
with:
install-args: "go node pnpm helm cosign syft"
- name: Install pnpm dependencies
uses: ./.github/actions/pnpm-install
- name: Install Go mise tools
run: ./.github/scripts/retry.sh -- mise install --locked go:github.com/tc-hib/go-winres go:github.com/goreleaser/nfpm/v2/cmd/nfpm
# Necessary for signing Windows binaries.
- name: Setup Java
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
distribution: "zulu"
java-version: "11.0"
- name: Install nsis and zstd
run: sudo apt-get install -y nsis zstd
- name: Install rcodesign
run: |
set -euo pipefail
wget -O /tmp/rcodesign.tar.gz https://github.com/indygreg/apple-platform-rs/releases/download/apple-codesign%2F0.22.0/apple-codesign-0.22.0-x86_64-unknown-linux-musl.tar.gz
sudo tar -xzf /tmp/rcodesign.tar.gz \
-C /usr/bin \
--strip-components=1 \
apple-codesign-0.22.0-x86_64-unknown-linux-musl/rcodesign
rm /tmp/rcodesign.tar.gz
- name: Setup Apple Developer certificate and API key
run: |
set -euo pipefail
touch /tmp/{apple_cert.p12,apple_cert_password.txt,apple_apikey.p8}
chmod 600 /tmp/{apple_cert.p12,apple_cert_password.txt,apple_apikey.p8}
echo "$AC_CERTIFICATE_P12_BASE64" | base64 -d > /tmp/apple_cert.p12
echo "$AC_CERTIFICATE_PASSWORD" > /tmp/apple_cert_password.txt
echo "$AC_APIKEY_P8_BASE64" | base64 -d > /tmp/apple_apikey.p8
env:
AC_CERTIFICATE_P12_BASE64: ${{ secrets.AC_CERTIFICATE_P12_BASE64 }}
AC_CERTIFICATE_PASSWORD: ${{ secrets.AC_CERTIFICATE_PASSWORD }}
AC_APIKEY_P8_BASE64: ${{ secrets.AC_APIKEY_P8_BASE64 }}
- name: Setup Windows EV Signing Certificate
run: |
set -euo pipefail
touch /tmp/ev_cert.pem
chmod 600 /tmp/ev_cert.pem
echo "$EV_SIGNING_CERT" > /tmp/ev_cert.pem
wget https://github.com/ebourg/jsign/releases/download/6.0/jsign-6.0.jar -O /tmp/jsign-6.0.jar
env:
EV_SIGNING_CERT: ${{ secrets.EV_SIGNING_CERT }}
- name: Test migrations from current ref to main
run: |
POSTGRES_VERSION=13 make test-migrations
# Setup GCloud for signing Windows binaries.
- name: Authenticate to Google Cloud
id: gcloud_auth
uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3.0.0
with:
workload_identity_provider: ${{ vars.GCP_CODE_SIGNING_WORKLOAD_ID_PROVIDER }}
service_account: ${{ vars.GCP_CODE_SIGNING_SERVICE_ACCOUNT }}
token_format: "access_token"
- name: Setup GCloud SDK
uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db # v3.0.1
- name: Build binaries
run: |
set -euo pipefail
./.github/scripts/retry.sh -- go mod download
version="$(./scripts/version.sh)"
make gen/mark-fresh
make -j \
build/coder_"$version"_linux_{amd64,armv7,arm64}.{tar.gz,apk,deb,rpm} \
build/coder_"$version"_{darwin,windows}_{amd64,arm64}.zip \
build/coder_"$version"_windows_amd64_installer.exe \
build/coder_helm_"$version".tgz \
build/provisioner_helm_"$version".tgz
env:
CODER_SIGN_WINDOWS: "1"
CODER_SIGN_DARWIN: "1"
CODER_SIGN_GPG: "1"
CODER_GPG_RELEASE_KEY_BASE64: ${{ secrets.GPG_RELEASE_KEY_BASE64 }}
CODER_WINDOWS_RESOURCES: "1"
AC_CERTIFICATE_FILE: /tmp/apple_cert.p12
AC_CERTIFICATE_PASSWORD_FILE: /tmp/apple_cert_password.txt
AC_APIKEY_ISSUER_ID: ${{ secrets.AC_APIKEY_ISSUER_ID }}
AC_APIKEY_ID: ${{ secrets.AC_APIKEY_ID }}
AC_APIKEY_FILE: /tmp/apple_apikey.p8
EV_KEY: ${{ secrets.EV_KEY }}
EV_KEYSTORE: ${{ secrets.EV_KEYSTORE }}
EV_TSA_URL: ${{ secrets.EV_TSA_URL }}
EV_CERTIFICATE_PATH: /tmp/ev_cert.pem
GCLOUD_ACCESS_TOKEN: ${{ steps.gcloud_auth.outputs.access_token }}
JSIGN_PATH: /tmp/jsign-6.0.jar
- name: Delete Apple Developer certificate and API key
run: rm -f /tmp/{apple_cert.p12,apple_cert_password.txt,apple_apikey.p8}
- name: Delete Windows EV Signing Cert
run: rm /tmp/ev_cert.pem
- name: Determine base image tag
id: image-base-tag
run: |
set -euo pipefail
# Empty value means use the default and avoid building a fresh one.
echo "tag=$(CODER_IMAGE_BASE=ghcr.io/coder/coder-base ./scripts/image_tag.sh)" >> "$GITHUB_OUTPUT"
- name: Create empty base-build-context directory
if: steps.image-base-tag.outputs.tag != ''
run: mkdir base-build-context
- name: Install depot.dev CLI
if: steps.image-base-tag.outputs.tag != ''
uses: depot/setup-action@15c09a5f77a0840ad4bce955686522a257853461 # v1.7.1
# This uses OIDC authentication, so no auth variables are required.
- name: Build base Docker image via depot.dev
id: build_base_image
if: steps.image-base-tag.outputs.tag != ''
uses: depot/build-push-action@5f3b3c2e5a00f0093de47f657aeaefcedff27d18 # v1.17.0
with:
project: wl5hnrrkns
context: base-build-context
file: scripts/Dockerfile.base
platforms: linux/amd64,linux/arm64,linux/arm/v7
provenance: true
sbom: true
pull: true
no-cache: true
push: true
tags: |
${{ steps.image-base-tag.outputs.tag }}
- name: Verify that images are pushed properly
if: steps.image-base-tag.outputs.tag != ''
run: |
# retry 10 times with a 5 second delay as the images may not be
# available immediately
for i in {1..10}; do
rc=0
raw_manifests=$(docker buildx imagetools inspect --raw "${IMAGE_TAG}") || rc=$?
if [[ "$rc" -eq 0 ]]; then
break
fi
if [[ "$i" -eq 10 ]]; then
echo "Failed to pull manifests after 10 retries"
exit 1
fi
echo "Failed to pull manifests, retrying in 5 seconds"
sleep 5
done
manifests=$(
echo "$raw_manifests" | \
jq -r '.manifests[].platform | .os + "/" + .architecture + (if .variant then "/" + .variant else "" end)'
)
# Verify all 3 platforms are present.
set -euxo pipefail
echo "$manifests" | grep -q linux/amd64
echo "$manifests" | grep -q linux/arm64
echo "$manifests" | grep -q linux/arm/v7
env:
IMAGE_TAG: ${{ steps.image-base-tag.outputs.tag }}
- name: GitHub Attestation for Base Docker image
id: attest_base
if: ${{ steps.build_base_image.outputs.digest != '' }}
continue-on-error: true
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0
with:
subject-name: ghcr.io/coder/coder-base
subject-digest: ${{ steps.build_base_image.outputs.digest }}
push-to-registry: true
- name: Build Linux Docker images
id: build_docker
run: |
set -euxo pipefail
# build Docker images for each architecture
version="$(./scripts/version.sh)"
make build/coder_"$version"_linux_{amd64,arm64,armv7}.tag
# build and push multi-arch manifest, this depends on the other images
# being pushed so will automatically push them.
make push/build/coder_"$version"_linux.tag
multiarch_image="$(./scripts/image_tag.sh)"
echo "multiarch_image=${multiarch_image}" >> "$GITHUB_OUTPUT"
# For debugging, print all docker image tags
docker images
# if the current version is equal to the highest (according to semver)
# version in the repo, also create a multi-arch image as ":latest" and
# push it
if [[ "$(git tag | grep '^v' | grep -vE '(rc|dev|-|\+|\/)' | sort -r --version-sort | head -n1)" == "v$(./scripts/version.sh)" ]]; then
latest_target="$(./scripts/image_tag.sh --version latest)"
# shellcheck disable=SC2046
./scripts/build_docker_multiarch.sh \
--push \
--target "${latest_target}" \
$(cat build/coder_"$version"_linux_{amd64,arm64,armv7}.tag)
echo "created_latest_tag=true" >> "$GITHUB_OUTPUT"
echo "latest_target=${latest_target}" >> "$GITHUB_OUTPUT"
else
echo "created_latest_tag=false" >> "$GITHUB_OUTPUT"
fi
env:
CODER_BASE_IMAGE_TAG: ${{ steps.image-base-tag.outputs.tag }}
- name: SBOM Generation and Attestation
env:
COSIGN_EXPERIMENTAL: '1'
MULTIARCH_IMAGE: ${{ steps.build_docker.outputs.multiarch_image }}
VERSION: ${{ steps.version.outputs.version }}
CREATED_LATEST_TAG: ${{ steps.build_docker.outputs.created_latest_tag }}
run: |
set -euxo pipefail
# Generate SBOM for multi-arch image with version in filename
echo "Generating SBOM for multi-arch image: ${MULTIARCH_IMAGE}"
syft "${MULTIARCH_IMAGE}" -o spdx-json > "coder_${VERSION}_sbom.spdx.json"
echo "Attesting SBOM to multi-arch image: ${MULTIARCH_IMAGE}"
cosign clean --force=true "${MULTIARCH_IMAGE}"
cosign attest --type spdxjson \
--predicate "coder_${VERSION}_sbom.spdx.json" \
--yes \
"${MULTIARCH_IMAGE}"
# If latest tag was created, also attest it
if [[ "${CREATED_LATEST_TAG}" == "true" ]]; then
latest_tag="$(./scripts/image_tag.sh --version latest)"
echo "Generating SBOM for latest image: ${latest_tag}"
syft "${latest_tag}" -o spdx-json > coder_latest_sbom.spdx.json
echo "Attesting SBOM to latest image: ${latest_tag}"
cosign clean --force=true "${latest_tag}"
cosign attest --type spdxjson \
--predicate coder_latest_sbom.spdx.json \
--yes \
"${latest_tag}"
fi
- name: Resolve Docker image digests for attestation
id: docker_digests
continue-on-error: true
env:
MULTIARCH_IMAGE: ${{ steps.build_docker.outputs.multiarch_image }}
LATEST_TARGET: ${{ steps.build_docker.outputs.latest_target }}
run: |
set -euxo pipefail
if [[ -n "${MULTIARCH_IMAGE}" ]]; then
multiarch_digest=$(docker buildx imagetools inspect --raw "${MULTIARCH_IMAGE}" | sha256sum | awk '{print "sha256:"$1}')
echo "multiarch_digest=${multiarch_digest}" >> "$GITHUB_OUTPUT"
fi
if [[ -n "${LATEST_TARGET}" ]]; then
latest_digest=$(docker buildx imagetools inspect --raw "${LATEST_TARGET}" | sha256sum | awk '{print "sha256:"$1}')
echo "latest_digest=${latest_digest}" >> "$GITHUB_OUTPUT"
fi
- name: GitHub Attestation for Docker image
id: attest_main
if: ${{ steps.docker_digests.outputs.multiarch_digest != '' }}
continue-on-error: true
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0
with:
subject-name: ghcr.io/coder/coder
subject-digest: ${{ steps.docker_digests.outputs.multiarch_digest }}
push-to-registry: true
- name: GitHub Attestation for "latest" Docker image
id: attest_latest
if: ${{ steps.docker_digests.outputs.latest_digest != '' }}
continue-on-error: true
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0
with:
subject-name: ghcr.io/coder/coder
subject-digest: ${{ steps.docker_digests.outputs.latest_digest }}
push-to-registry: true
- name: GitHub Attestation for release binaries
id: attest_binaries
continue-on-error: true
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0
with:
subject-path: |
./build/*.tar.gz
./build/*.zip
./build/*.deb
./build/*.rpm
./build/*.apk
./build/*_installer.exe
./build/*_helm_*.tgz
./build/provisioner_helm_*.tgz
# Report attestation failures but don't fail the workflow
- name: Check attestation status
run: | # zizmor: ignore[template-injection] We're just reading steps.attest_x.outcome here, no risk of injection
if [[ "${{ steps.attest_base.outcome }}" == "failure" && "${{ steps.attest_base.conclusion }}" != "skipped" ]]; then
echo "::warning::GitHub attestation for base image failed"
fi
if [[ "${{ steps.attest_main.outcome }}" == "failure" ]]; then
echo "::warning::GitHub attestation for main image failed"
fi
if [[ "${{ steps.attest_latest.outcome }}" == "failure" && "${{ steps.attest_latest.conclusion }}" != "skipped" ]]; then
echo "::warning::GitHub attestation for latest image failed"
fi
if [[ "${{ steps.attest_binaries.outcome }}" == "failure" && "${{ steps.attest_binaries.conclusion }}" != "skipped" ]]; then
echo "::warning::GitHub attestation for release binaries failed"
fi
- name: Generate offline docs
run: |
version="$(./scripts/version.sh)"
make -j build/coder_docs_"$version".tgz
- name: ls build
run: ls -lh build
- name: Publish Coder CLI binaries and detached signatures to GCS
run: |
set -euxo pipefail
version="$(./scripts/version.sh)"
# Source array of slim binaries
declare -A binaries
binaries["coder-darwin-amd64"]="coder-slim_${version}_darwin_amd64"
binaries["coder-darwin-arm64"]="coder-slim_${version}_darwin_arm64"
binaries["coder-linux-amd64"]="coder-slim_${version}_linux_amd64"
binaries["coder-linux-arm64"]="coder-slim_${version}_linux_arm64"
binaries["coder-linux-armv7"]="coder-slim_${version}_linux_armv7"
binaries["coder-windows-amd64.exe"]="coder-slim_${version}_windows_amd64.exe"
binaries["coder-windows-arm64.exe"]="coder-slim_${version}_windows_arm64.exe"
for cli_name in "${!binaries[@]}"; do
slim_binary="${binaries[$cli_name]}"
detached_signature="${slim_binary}.asc"
gcloud storage cp "./build/${slim_binary}" "gs://releases.coder.com/coder-cli/${version}/${cli_name}"
gcloud storage cp "./build/${detached_signature}" "gs://releases.coder.com/coder-cli/${version}/${cli_name}.asc"
done
- name: Publish release
run: |
set -euo pipefail
# Build the list of files to publish.
files=(
./build/*_installer.exe
./build/*.zip
./build/*.tar.gz
./build/*.tgz
./build/*.apk
./build/*.deb
./build/*.rpm
"./coder_${VERSION}_sbom.spdx.json"
)
# Only include the latest SBOM file if it was created.
if [[ "${CREATED_LATEST_TAG}" == "true" ]]; then
files+=(./coder_latest_sbom.spdx.json)
fi
stable_flag=()
if [[ "$CODER_RELEASE_STABLE" == "true" ]]; then
stable_flag=(--stable)
fi
go run ./scripts/release-action publish \
--version "v${VERSION}" \
"${stable_flag[@]}" \
--release-notes-file "$CODER_RELEASE_NOTES_FILE" \
"${files[@]}"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ steps.version.outputs.version }}
CREATED_LATEST_TAG: ${{ steps.build_docker.outputs.created_latest_tag }}
# Mark the Linear release as shipped.
- name: Extract Linear release version
id: linear_version
run: |
# Skip RC releases — they must not complete the Linear release.
if [[ "$VERSION" == *-rc* ]]; then
echo "RC release (${VERSION}), skipping Linear release completion."
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
# Strip patch to get the Linear release version (e.g. 2.32.0 -> 2.32).
linear_version=$(echo "$VERSION" | cut -d. -f1,2)
echo "version=$linear_version" >> "$GITHUB_OUTPUT"
echo "skip=false" >> "$GITHUB_OUTPUT"
echo "Completing Linear release ${linear_version}"
env:
VERSION: ${{ steps.version.outputs.version }}
- name: Complete Linear release
if: ${{ steps.linear_version.outputs.skip != 'true' }}
continue-on-error: true
uses: linear/linear-release-action@0353b5fa8c00326913966f00557d68f8f30b8b6b # v0.7.0
with:
access_key: ${{ secrets.LINEAR_ACCESS_KEY }}
command: complete
version: ${{ steps.linear_version.outputs.version }}
timeout: 300
- name: Authenticate to Google Cloud
uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3.0.0
with:
workload_identity_provider: ${{ vars.GCP_WORKLOAD_ID_PROVIDER }}
service_account: ${{ vars.GCP_SERVICE_ACCOUNT }}
- name: Setup GCloud SDK
uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db # 3.0.1
- name: Publish Helm Chart
run: |
set -euo pipefail
version="$(./scripts/version.sh)"
mkdir -p build/helm
cp "build/coder_helm_${version}.tgz" build/helm
cp "build/provisioner_helm_${version}.tgz" build/helm
gsutil cp gs://helm.coder.com/v2/index.yaml build/helm/index.yaml
helm repo index build/helm --url https://helm.coder.com/v2 --merge build/helm/index.yaml
gsutil -h "Cache-Control:no-cache,max-age=0" cp "build/helm/coder_helm_${version}.tgz" gs://helm.coder.com/v2
gsutil -h "Cache-Control:no-cache,max-age=0" cp "build/helm/provisioner_helm_${version}.tgz" gs://helm.coder.com/v2
gsutil -h "Cache-Control:no-cache,max-age=0" cp "build/helm/index.yaml" gs://helm.coder.com/v2
gsutil -h "Cache-Control:no-cache,max-age=0" cp "helm/artifacthub-repo.yml" gs://helm.coder.com/v2
helm push "build/coder_helm_${version}.tgz" oci://ghcr.io/coder/chart
helm push "build/provisioner_helm_${version}.tgz" oci://ghcr.io/coder/chart
- name: Send repository-dispatch event
if: ${{ inputs.release_type != 'rc' && inputs.release_type != 'create-release-branch' }}
uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4.0.1
with:
token: ${{ secrets.CDRCI_GITHUB_TOKEN }}
repository: coder/packages
event-type: coder-release
client-payload: '{"coder_version": "${{ steps.version.outputs.version }}"}'
publish-homebrew:
name: Publish to Homebrew tap
runs-on: ubuntu-latest
needs: [release, prepare-release]
if: ${{ inputs.release_type != 'rc' && inputs.release_type != 'create-release-branch' && needs.prepare-release.outputs.stable == 'true' }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0
with:
egress-policy: audit
- name: Update homebrew
env:
GH_REPO: coder/homebrew-coder
GH_TOKEN: ${{ secrets.CDRCI_GITHUB_TOKEN }}
VERSION: ${{ needs.release.outputs.version }}
run: |
# Keep version number around for reference, removing any potential leading v
coder_version="$(echo "${VERSION}" | tr -d v)"
set -euxo pipefail
# Setup Git
git config --global user.email "ci@coder.com"
git config --global user.name "Coder CI"
git config --global credential.helper "store"
temp_dir="$(mktemp -d)"
cd "$temp_dir"
# Download checksums
checksums_url="$(gh release view --repo coder/coder "v$coder_version" --json assets \
| jq -r ".assets | map(.url) | .[]" \
| grep -e ".checksums.txt\$")"
wget "$checksums_url" -O checksums.txt
# Get the SHAs
darwin_arm_sha="$(grep "darwin_arm64.zip" checksums.txt | awk '{ print $1 }')"
darwin_intel_sha="$(grep "darwin_amd64.zip" checksums.txt | awk '{ print $1 }')"
linux_sha="$(grep "linux_amd64.tar.gz" checksums.txt | awk '{ print $1 }')"
echo "macOS arm64: $darwin_arm_sha"
echo "macOS amd64: $darwin_intel_sha"
echo "Linux amd64: $linux_sha"
# Check out the homebrew repo
git clone "https://github.com/$GH_REPO" homebrew-coder
brew_branch="auto-release/$coder_version"
cd homebrew-coder
# Check if a PR already exists.
pr_count="$(gh pr list --search "head:$brew_branch" --json id,closed | jq -r ".[] | select(.closed == false) | .id" | wc -l)"
if [ "$pr_count" -gt 0 ]; then
echo "Bailing out as PR already exists" 2>&1
exit 0
fi
# Set up cdrci credentials for pushing to homebrew-coder
echo "https://x-access-token:$GH_TOKEN@github.com" >> ~/.git-credentials
# Update the formulae and push
git checkout -b "$brew_branch"
./scripts/update-v2.sh "$coder_version" "$darwin_arm_sha" "$darwin_intel_sha" "$linux_sha"
git add .
git commit -m "coder $coder_version"
git push -u origin -f "$brew_branch"
# Create PR
gh pr create \
-B master -H "$brew_branch" \
-t "coder $coder_version" \
-b "" \
-r "${GITHUB_ACTOR}" \
-a "${GITHUB_ACTOR}" \
-b "This automatic PR was triggered by the release of Coder v$coder_version"
publish-winget:
name: Publish to winget-pkgs
runs-on: windows-latest
needs: [release, prepare-release]
if: ${{ inputs.release_type != 'rc' && inputs.release_type != 'create-release-branch' }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0
with:
egress-policy: audit
- name: Sync fork
run: gh repo sync cdrci/winget-pkgs -b master
env:
GH_TOKEN: ${{ secrets.CDRCI_GITHUB_TOKEN }}
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
persist-credentials: false
# If the event that triggered the build was an annotated tag (which our
# tags are supposed to be), actions/checkout has a bug where the tag in
# question is only a lightweight tag and not a full annotated tag. This
# command seems to fix it.
# https://github.com/actions/checkout/issues/290
- name: Fetch git tags
run: git fetch --tags --force
- name: Install wingetcreate
run: |
Invoke-WebRequest https://aka.ms/wingetcreate/latest -OutFile wingetcreate.exe
- name: Submit updated manifest to winget-pkgs
run: |
# The package version is the same as the tag minus the leading "v".
# The version in this output already has the leading "v" removed but
# we do it again to be safe.
$version = $env:VERSION.Trim('v')
$release_assets = gh release view --repo coder/coder "v${version}" --json assets | `
ConvertFrom-Json
# Get the installer URLs from the release assets.
$amd64_installer_url = $release_assets.assets | `
Where-Object name -Match ".*_windows_amd64_installer.exe$" | `
Select -ExpandProperty url
$amd64_zip_url = $release_assets.assets | `
Where-Object name -Match ".*_windows_amd64.zip$" | `
Select -ExpandProperty url
$arm64_zip_url = $release_assets.assets | `
Where-Object name -Match ".*_windows_arm64.zip$" | `
Select -ExpandProperty url
echo "amd64 Installer URL: ${amd64_installer_url}"
echo "amd64 zip URL: ${amd64_zip_url}"
echo "arm64 zip URL: ${arm64_zip_url}"
echo "Package version: ${version}"
.\wingetcreate.exe update Coder.Coder `
--submit `
--version "${version}" `
--urls "${amd64_installer_url}" "${amd64_zip_url}" "${arm64_zip_url}"
env:
# For gh CLI:
GH_TOKEN: ${{ github.token }}
# For wingetcreate. We need a real token since we're pushing a commit
# to GitHub and then making a PR in a different repo.
# wingetcreate will read the token from the environment variable defined below.
# Reference: https://aka.ms/winget-create-token
WINGET_CREATE_GITHUB_TOKEN: ${{ secrets.CDRCI_GITHUB_TOKEN }}
VERSION: ${{ needs.release.outputs.version }}
- name: Comment on PR
run: |
# wait 30 seconds
Start-Sleep -Seconds 30.0
# Find the PR that wingetcreate just made.
$version = $env:VERSION.Trim('v')
$pr_list = gh pr list --repo microsoft/winget-pkgs --search "author:cdrci Coder.Coder version ${version}" --limit 1 --json number | `
ConvertFrom-Json
$pr_number = $pr_list[0].number
gh pr comment --repo microsoft/winget-pkgs "${pr_number}" --body "🤖 cc: @deansheather @matifali"
env:
# For gh CLI. We need a real token since we're commenting on a PR in a
# different repo.
GH_TOKEN: ${{ secrets.CDRCI_GITHUB_TOKEN }}
VERSION: ${{ needs.release.outputs.version }}
update-docs:
name: Update release docs
needs: [prepare-release, release]
if: ${{ inputs.release_type != 'rc' && inputs.release_type != 'create-release-branch' }}
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-8' || 'ubuntu-latest' }}
permissions:
contents: write
pull-requests: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0
with:
egress-policy: audit
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: main
fetch-depth: 0
persist-credentials: true
- name: Fetch git tags
run: git fetch --tags --force
- name: Setup Node
uses: ./.github/actions/setup-node
- name: Update release calendar
run: ./scripts/update-release-calendar.sh
- name: Create docs update PR
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: "docs: update release docs for ${{ needs.prepare-release.outputs.version }}"
title: "docs: update release docs for ${{ needs.prepare-release.outputs.version }}"
body: "Automated docs update for release ${{ needs.prepare-release.outputs.version }}."
branch: docs/release-${{ needs.prepare-release.outputs.version }}
base: main