diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6fbba7883a..eb2ba62e6b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -4,6 +4,7 @@ on: push: branches: - main + - release/* pull_request: workflow_dispatch: @@ -969,7 +970,7 @@ jobs: needs: changes # We always build the dylibs on Go changes to verify we're not merging unbuildable code, # but they need only be signed and uploaded on coder/coder main. - if: needs.changes.outputs.go == 'true' || needs.changes.outputs.ci == 'true' || github.ref == 'refs/heads/main' + if: needs.changes.outputs.go == 'true' || needs.changes.outputs.ci == 'true' || github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/') runs-on: ${{ github.repository_owner == 'coder' && 'depot-macos-latest' || 'macos-latest' }} steps: # Harden Runner doesn't work on macOS @@ -997,7 +998,7 @@ jobs: uses: ./.github/actions/setup-go - name: Install rcodesign - if: ${{ github.repository_owner == 'coder' && github.ref == 'refs/heads/main' }} + if: ${{ github.repository_owner == 'coder' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/')) }} 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-macos-universal.tar.gz @@ -1008,7 +1009,7 @@ jobs: rm /tmp/rcodesign.tar.gz - name: Setup Apple Developer certificate and API key - if: ${{ github.repository_owner == 'coder' && github.ref == 'refs/heads/main' }} + if: ${{ github.repository_owner == 'coder' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/')) }} run: | set -euo pipefail touch /tmp/{apple_cert.p12,apple_cert_password.txt,apple_apikey.p8} @@ -1029,12 +1030,12 @@ jobs: make gen/mark-fresh make build/coder-dylib env: - CODER_SIGN_DARWIN: ${{ github.ref == 'refs/heads/main' && '1' || '0' }} + CODER_SIGN_DARWIN: ${{ (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/')) && '1' || '0' }} AC_CERTIFICATE_FILE: /tmp/apple_cert.p12 AC_CERTIFICATE_PASSWORD_FILE: /tmp/apple_cert_password.txt - name: Upload build artifacts - if: ${{ github.repository_owner == 'coder' && github.ref == 'refs/heads/main' }} + if: ${{ github.repository_owner == 'coder' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/')) }} uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: dylibs @@ -1044,7 +1045,7 @@ jobs: retention-days: 7 - name: Delete Apple Developer certificate and API key - if: ${{ github.repository_owner == 'coder' && github.ref == 'refs/heads/main' }} + if: ${{ github.repository_owner == 'coder' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/')) }} run: rm -f /tmp/{apple_cert.p12,apple_cert_password.txt,apple_apikey.p8} check-build: @@ -1094,7 +1095,7 @@ jobs: needs: - changes - build-dylib - if: github.ref == 'refs/heads/main' && needs.changes.outputs.docs-only == 'false' && !github.event.pull_request.head.repo.fork + if: (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/')) && needs.changes.outputs.docs-only == 'false' && !github.event.pull_request.head.repo.fork runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-8' || 'ubuntu-22.04' }} permissions: # Necessary to push docker images to ghcr.io. @@ -1247,40 +1248,45 @@ jobs: id: build-docker env: CODER_IMAGE_BASE: ghcr.io/coder/coder-preview - CODER_IMAGE_TAG_PREFIX: main DOCKER_CLI_EXPERIMENTAL: "enabled" run: | set -euxo pipefail # build Docker images for each architecture version="$(./scripts/version.sh)" - tag="main-${version//+/-}" + tag="${version//+/-}" echo "tag=$tag" >> "$GITHUB_OUTPUT" # build images for each architecture # note: omitting the -j argument to avoid race conditions when pushing make build/coder_"$version"_linux_{amd64,arm64,armv7}.tag - # only push if we are on main branch - if [ "${GITHUB_REF}" == "refs/heads/main" ]; then + # only push if we are on main branch or release branch + if [[ "${GITHUB_REF}" == "refs/heads/main" || "${GITHUB_REF}" == refs/heads/release/* ]]; then # build and push multi-arch manifest, this depends on the other images # being pushed so will automatically push them # note: omitting the -j argument to avoid race conditions when pushing make push/build/coder_"$version"_linux_{amd64,arm64,armv7}.tag # Define specific tags - tags=("$tag" "main" "latest") + tags=("$tag") + if [ "${GITHUB_REF}" == "refs/heads/main" ]; then + tags+=("main" "latest") + elif [[ "${GITHUB_REF}" == refs/heads/release/* ]]; then + tags+=("release-${GITHUB_REF#refs/heads/release/}") + fi # Create and push a multi-arch manifest for each tag # we are adding `latest` tag and keeping `main` for backward # compatibality for t in "${tags[@]}"; do - # shellcheck disable=SC2046 - ./scripts/build_docker_multiarch.sh \ - --push \ - --target "ghcr.io/coder/coder-preview:$t" \ - --version "$version" \ - $(cat build/coder_"$version"_linux_{amd64,arm64,armv7}.tag) + echo "Pushing multi-arch manifest for tag: $t" + # shellcheck disable=SC2046 + ./scripts/build_docker_multiarch.sh \ + --push \ + --target "ghcr.io/coder/coder-preview:$t" \ + --version "$version" \ + $(cat build/coder_"$version"_linux_{amd64,arm64,armv7}.tag) done fi @@ -1471,112 +1477,28 @@ jobs: ./build/*.deb retention-days: 7 + # Deploy is handled in deploy.yaml so we can apply concurrency limits. deploy: - name: "deploy" - runs-on: ubuntu-latest - timeout-minutes: 30 needs: - changes - build if: | - github.ref == 'refs/heads/main' && !github.event.pull_request.head.repo.fork + (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/')) && needs.changes.outputs.docs-only == 'false' + && !github.event.pull_request.head.repo.fork + uses: ./.github/workflows/deploy.yaml + with: + image: ${{ needs.build.outputs.IMAGE }} permissions: contents: read id-token: write - steps: - - name: Harden Runner - uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 - with: - egress-policy: audit - - - name: Checkout - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - with: - fetch-depth: 0 - persist-credentials: false - - - 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: Set up Google Cloud SDK - uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db # v3.0.1 - - - name: Set up Flux CLI - uses: fluxcd/flux2/action@6bf37f6a560fd84982d67f853162e4b3c2235edb # v2.6.4 - with: - # Keep this and the github action up to date with the version of flux installed in dogfood cluster - version: "2.5.1" - - - name: Get Cluster Credentials - uses: google-github-actions/get-gke-credentials@3da1e46a907576cefaa90c484278bb5b259dd395 # v3.0.0 - with: - cluster_name: dogfood-v2 - location: us-central1-a - project_id: coder-dogfood-v2 - - - name: Reconcile Flux - run: | - set -euxo pipefail - flux --namespace flux-system reconcile source git flux-system - flux --namespace flux-system reconcile source git coder-main - flux --namespace flux-system reconcile kustomization flux-system - flux --namespace flux-system reconcile kustomization coder - flux --namespace flux-system reconcile source chart coder-coder - flux --namespace flux-system reconcile source chart coder-coder-provisioner - flux --namespace coder reconcile helmrelease coder - flux --namespace coder reconcile helmrelease coder-provisioner - - # Just updating Flux is usually not enough. The Helm release may get - # redeployed, but unless something causes the Deployment to update the - # pods won't be recreated. It's important that the pods get recreated, - # since we use `imagePullPolicy: Always` to ensure we're running the - # latest image. - - name: Rollout Deployment - run: | - set -euxo pipefail - kubectl --namespace coder rollout restart deployment/coder - kubectl --namespace coder rollout status deployment/coder - kubectl --namespace coder rollout restart deployment/coder-provisioner - kubectl --namespace coder rollout status deployment/coder-provisioner - kubectl --namespace coder rollout restart deployment/coder-provisioner-tagged - kubectl --namespace coder rollout status deployment/coder-provisioner-tagged - - deploy-wsproxies: - runs-on: ubuntu-latest - needs: build - if: github.ref == 'refs/heads/main' && !github.event.pull_request.head.repo.fork - steps: - - name: Harden Runner - uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 - with: - egress-policy: audit - - - name: Checkout - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - with: - fetch-depth: 0 - persist-credentials: false - - - name: Setup flyctl - uses: superfly/flyctl-actions/setup-flyctl@fc53c09e1bc3be6f54706524e3b82c4f462f77be # v1.5 - - - name: Deploy workspace proxies - run: | - flyctl deploy --image "$IMAGE" --app paris-coder --config ./.github/fly-wsproxies/paris-coder.toml --env "CODER_PROXY_SESSION_TOKEN=$TOKEN_PARIS" --yes - flyctl deploy --image "$IMAGE" --app sydney-coder --config ./.github/fly-wsproxies/sydney-coder.toml --env "CODER_PROXY_SESSION_TOKEN=$TOKEN_SYDNEY" --yes - flyctl deploy --image "$IMAGE" --app sao-paulo-coder --config ./.github/fly-wsproxies/sao-paulo-coder.toml --env "CODER_PROXY_SESSION_TOKEN=$TOKEN_SAO_PAULO" --yes - flyctl deploy --image "$IMAGE" --app jnb-coder --config ./.github/fly-wsproxies/jnb-coder.toml --env "CODER_PROXY_SESSION_TOKEN=$TOKEN_JNB" --yes - env: - FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} - IMAGE: ${{ needs.build.outputs.IMAGE }} - TOKEN_PARIS: ${{ secrets.FLY_PARIS_CODER_PROXY_SESSION_TOKEN }} - TOKEN_SYDNEY: ${{ secrets.FLY_SYDNEY_CODER_PROXY_SESSION_TOKEN }} - TOKEN_SAO_PAULO: ${{ secrets.FLY_SAO_PAULO_CODER_PROXY_SESSION_TOKEN }} - TOKEN_JNB: ${{ secrets.FLY_JNB_CODER_PROXY_SESSION_TOKEN }} + packages: write # to retag image as dogfood + secrets: + FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} + FLY_PARIS_CODER_PROXY_SESSION_TOKEN: ${{ secrets.FLY_PARIS_CODER_PROXY_SESSION_TOKEN }} + FLY_SYDNEY_CODER_PROXY_SESSION_TOKEN: ${{ secrets.FLY_SYDNEY_CODER_PROXY_SESSION_TOKEN }} + FLY_SAO_PAULO_CODER_PROXY_SESSION_TOKEN: ${{ secrets.FLY_SAO_PAULO_CODER_PROXY_SESSION_TOKEN }} + FLY_JNB_CODER_PROXY_SESSION_TOKEN: ${{ secrets.FLY_JNB_CODER_PROXY_SESSION_TOKEN }} # sqlc-vet runs a postgres docker container, runs Coder migrations, and then # runs sqlc-vet to ensure all queries are valid. This catches any mistakes diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml new file mode 100644 index 0000000000..7983591246 --- /dev/null +++ b/.github/workflows/deploy.yaml @@ -0,0 +1,170 @@ +name: deploy + +on: + # Via workflow_call, called from ci.yaml + workflow_call: + inputs: + image: + description: "Image and tag to potentially deploy. Current branch will be validated against should-deploy check." + required: true + type: string + secrets: + FLY_API_TOKEN: + required: true + FLY_PARIS_CODER_PROXY_SESSION_TOKEN: + required: true + FLY_SYDNEY_CODER_PROXY_SESSION_TOKEN: + required: true + FLY_SAO_PAULO_CODER_PROXY_SESSION_TOKEN: + required: true + FLY_JNB_CODER_PROXY_SESSION_TOKEN: + required: true + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }} # no per-branch concurrency + cancel-in-progress: false + +jobs: + # Determines if the given branch should be deployed to dogfood. + should-deploy: + name: should-deploy + runs-on: ubuntu-latest + outputs: + verdict: ${{ steps.check.outputs.verdict }} # DEPLOY or NOOP + steps: + - name: Harden Runner + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 + with: + egress-policy: audit + + - name: Checkout + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Check if deploy is enabled + id: check + run: | + set -euo pipefail + verdict="$(./scripts/should_deploy.sh)" + echo "verdict=$verdict" >> "$GITHUB_OUTPUT" + + deploy: + name: "deploy" + runs-on: ubuntu-latest + timeout-minutes: 30 + needs: should-deploy + if: needs.should-deploy.outputs.verdict == 'DEPLOY' + permissions: + contents: read + id-token: write + packages: write # to retag image as dogfood + steps: + - name: Harden Runner + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 + with: + egress-policy: audit + + - name: Checkout + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + fetch-depth: 0 + persist-credentials: false + + - name: GHCR Login + uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - 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: Set up Google Cloud SDK + uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db # v3.0.1 + + - name: Set up Flux CLI + uses: fluxcd/flux2/action@6bf37f6a560fd84982d67f853162e4b3c2235edb # v2.6.4 + with: + # Keep this and the github action up to date with the version of flux installed in dogfood cluster + version: "2.7.0" + + - name: Get Cluster Credentials + uses: google-github-actions/get-gke-credentials@3da1e46a907576cefaa90c484278bb5b259dd395 # v3.0.0 + with: + cluster_name: dogfood-v2 + location: us-central1-a + project_id: coder-dogfood-v2 + + - name: Tag image as dogfood + run: | + set -euxo pipefail + # Retag image as dogfood while maintaining the multi-arch manifest + docker buildx imagetools create --tag "ghcr.io/coder/coder-preview:dogfood" "$IMAGE" + + - name: Reconcile Flux + run: | + set -euxo pipefail + flux --namespace flux-system reconcile source git flux-system + flux --namespace flux-system reconcile source git coder-main + flux --namespace flux-system reconcile kustomization flux-system + flux --namespace flux-system reconcile kustomization coder + flux --namespace flux-system reconcile source chart coder-coder + flux --namespace flux-system reconcile source chart coder-coder-provisioner + flux --namespace coder reconcile helmrelease coder + flux --namespace coder reconcile helmrelease coder-provisioner + + # Just updating Flux is usually not enough. The Helm release may get + # redeployed, but unless something causes the Deployment to update the + # pods won't be recreated. It's important that the pods get recreated, + # since we use `imagePullPolicy: Always` to ensure we're running the + # latest image. + - name: Rollout Deployment + run: | + set -euxo pipefail + kubectl --namespace coder rollout restart deployment/coder + kubectl --namespace coder rollout status deployment/coder + kubectl --namespace coder rollout restart deployment/coder-provisioner + kubectl --namespace coder rollout status deployment/coder-provisioner + kubectl --namespace coder rollout restart deployment/coder-provisioner-tagged + kubectl --namespace coder rollout status deployment/coder-provisioner-tagged + + deploy-wsproxies: + runs-on: ubuntu-latest + needs: deploy + steps: + - name: Harden Runner + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 + with: + egress-policy: audit + + - name: Checkout + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Setup flyctl + uses: superfly/flyctl-actions/setup-flyctl@fc53c09e1bc3be6f54706524e3b82c4f462f77be # v1.5 + + - name: Deploy workspace proxies + run: | + flyctl deploy --image "$IMAGE" --app paris-coder --config ./.github/fly-wsproxies/paris-coder.toml --env "CODER_PROXY_SESSION_TOKEN=$TOKEN_PARIS" --yes + flyctl deploy --image "$IMAGE" --app sydney-coder --config ./.github/fly-wsproxies/sydney-coder.toml --env "CODER_PROXY_SESSION_TOKEN=$TOKEN_SYDNEY" --yes + flyctl deploy --image "$IMAGE" --app sao-paulo-coder --config ./.github/fly-wsproxies/sao-paulo-coder.toml --env "CODER_PROXY_SESSION_TOKEN=$TOKEN_SAO_PAULO" --yes + flyctl deploy --image "$IMAGE" --app jnb-coder --config ./.github/fly-wsproxies/jnb-coder.toml --env "CODER_PROXY_SESSION_TOKEN=$TOKEN_JNB" --yes + env: + FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} + IMAGE: ${{ inputs.image }} + TOKEN_PARIS: ${{ secrets.FLY_PARIS_CODER_PROXY_SESSION_TOKEN }} + TOKEN_SYDNEY: ${{ secrets.FLY_SYDNEY_CODER_PROXY_SESSION_TOKEN }} + TOKEN_SAO_PAULO: ${{ secrets.FLY_SAO_PAULO_CODER_PROXY_SESSION_TOKEN }} + TOKEN_JNB: ${{ secrets.FLY_JNB_CODER_PROXY_SESSION_TOKEN }} diff --git a/.github/zizmor.yml b/.github/zizmor.yml new file mode 100644 index 0000000000..e125592cfd --- /dev/null +++ b/.github/zizmor.yml @@ -0,0 +1,4 @@ +rules: + cache-poisoning: + ignore: + - "ci.yaml:184" diff --git a/scripts/image_tag.sh b/scripts/image_tag.sh index 68dfbcebf9..8767a22cb1 100755 --- a/scripts/image_tag.sh +++ b/scripts/image_tag.sh @@ -51,10 +51,7 @@ fi image="${CODER_IMAGE_BASE:-ghcr.io/coder/coder}" -# use CODER_IMAGE_TAG_PREFIX if set as a prefix for the tag -tag_prefix="${CODER_IMAGE_TAG_PREFIX:-}" - -tag="${tag_prefix:+$tag_prefix-}v$version" +tag="v$version" if [[ "$version" == "latest" ]]; then tag="latest" diff --git a/scripts/should_deploy.sh b/scripts/should_deploy.sh new file mode 100755 index 0000000000..3122192956 --- /dev/null +++ b/scripts/should_deploy.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash + +# This script determines if a commit in either the main branch or a +# `release/x.y` branch should be deployed to dogfood. +# +# To avoid masking unrelated failures, this script will return 0 in either case, +# and will print `DEPLOY` or `NOOP` to stdout. + +set -euo pipefail +# shellcheck source=scripts/lib.sh +source "$(dirname "${BASH_SOURCE[0]}")/lib.sh" +cdroot + +deploy_branch=main + +# Determine the current branch name and check that it is one of the supported +# branch names. +branch_name=$(git branch --show-current) +if [[ "$branch_name" != "main" && ! "$branch_name" =~ ^release/[0-9]+\.[0-9]+$ ]]; then + error "Current branch '$branch_name' is not a supported branch name for dogfood, must be 'main' or 'release/x.y'" +fi +log "Current branch '$branch_name'" + +# Determine the remote name +remote=$(git remote -v | grep coder/coder | awk '{print $1}' | head -n1) +if [[ -z "${remote}" ]]; then + error "Could not find remote for coder/coder" +fi +log "Using remote '$remote'" + +# Step 1: List all release branches and sort them by major/minor so we can find +# the latest release branch. +release_branches=$( + git branch -r --format='%(refname:short)' | + grep -E "${remote}/release/[0-9]+\.[0-9]+$" | + sed "s|${remote}/||" | + sort -V +) + +# As a sanity check, release/2.26 should exist. +if ! echo "$release_branches" | grep "release/2.26" >/dev/null; then + error "Could not find existing release branches. Did you run 'git fetch -ap ${remote}'?" +fi + +latest_release_branch=$(echo "$release_branches" | tail -n 1) +latest_release_branch_version=${latest_release_branch#release/} +log "Latest release branch: $latest_release_branch" +log "Latest release branch version: $latest_release_branch_version" + +# Step 2: check if a matching tag `v.0` exists. If it does not, we will +# use the release branch as the deploy branch. +if ! git rev-parse "refs/tags/v${latest_release_branch_version}.0" >/dev/null 2>&1; then + log "Tag 'v${latest_release_branch_version}.0' does not exist, using release branch as deploy branch" + deploy_branch=$latest_release_branch +else + log "Matching tag 'v${latest_release_branch_version}.0' exists, using main as deploy branch" +fi +log "Deploy branch: $deploy_branch" + +# Finally, check if the current branch is the deploy branch. +log +if [[ "$branch_name" != "$deploy_branch" ]]; then + log "VERDICT: DO NOT DEPLOY" + echo "NOOP" # stdout +else + log "VERDICT: DEPLOY" + echo "DEPLOY" # stdout +fi