From 4a5fd678bafc35ef698afdf5ff63d96541641b62 Mon Sep 17 00:00:00 2001 From: Hubert Deng Date: Mon, 2 Mar 2026 13:38:40 -0800 Subject: [PATCH 01/11] test coder preview --- .github/workflows/coder-preview.yml | 35 +++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 .github/workflows/coder-preview.yml diff --git a/.github/workflows/coder-preview.yml b/.github/workflows/coder-preview.yml new file mode 100644 index 000000000000..d3d9d281191e --- /dev/null +++ b/.github/workflows/coder-preview.yml @@ -0,0 +1,35 @@ +name: Coder PR Preview +on: + pull_request: + types: [labeled, unlabeled, synchronize, closed] + +jobs: + deploy: + if: | + (github.event.action == 'labeled' && github.event.label.name == 'deploy-preview') || + (github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'deploy-preview')) + uses: getsentry/devinfra-coder-infra/.github/workflows/coder-preview.yml@5cc8ec32cfed61df08f8ef642a0bed5a2f801ddd # main + with: + repo: sentry + caller_repo: ${{ github.repository }} + pr_number: ${{ github.event.pull_request.number }} + pr_url: ${{ github.event.pull_request.html_url }} + action: deploy + secrets: + CODER_SESSION_TOKEN: ${{ secrets.CODER_SESSION_TOKEN }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + destroy: + if: | + github.event.action == 'closed' || + (github.event.action == 'unlabeled' && github.event.label.name == 'deploy-preview') + uses: getsentry/devinfra-coder-infra/.github/workflows/coder-preview.yml@5cc8ec32cfed61df08f8ef642a0bed5a2f801ddd # main + with: + repo: sentry + caller_repo: ${{ github.repository }} + pr_number: ${{ github.event.pull_request.number }} + pr_url: ${{ github.event.pull_request.html_url }} + action: destroy + secrets: + CODER_SESSION_TOKEN: ${{ secrets.CODER_SESSION_TOKEN }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} From df1922cab3a38a8e0cce348f4fb0a06ed863de0a Mon Sep 17 00:00:00 2001 From: Hubert Deng Date: Mon, 2 Mar 2026 14:02:21 -0800 Subject: [PATCH 02/11] add deployment permissions --- .github/workflows/coder-preview.yml | 65 +++++--- scripts/coder-preview/compute-name.sh | 17 +++ scripts/coder-preview/deploy.sh | 205 ++++++++++++++++++++++++++ scripts/coder-preview/destroy.sh | 78 ++++++++++ 4 files changed, 345 insertions(+), 20 deletions(-) create mode 100755 scripts/coder-preview/compute-name.sh create mode 100755 scripts/coder-preview/deploy.sh create mode 100755 scripts/coder-preview/destroy.sh diff --git a/.github/workflows/coder-preview.yml b/.github/workflows/coder-preview.yml index d3d9d281191e..627b97f3fd07 100644 --- a/.github/workflows/coder-preview.yml +++ b/.github/workflows/coder-preview.yml @@ -3,33 +3,58 @@ on: pull_request: types: [labeled, unlabeled, synchronize, closed] +permissions: + deployments: write + contents: read + +env: + CODER_URL: https://coder.sentry.dev + jobs: deploy: if: | (github.event.action == 'labeled' && github.event.label.name == 'deploy-preview') || (github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'deploy-preview')) - uses: getsentry/devinfra-coder-infra/.github/workflows/coder-preview.yml@5cc8ec32cfed61df08f8ef642a0bed5a2f801ddd # main - with: - repo: sentry - caller_repo: ${{ github.repository }} - pr_number: ${{ github.event.pull_request.number }} - pr_url: ${{ github.event.pull_request.html_url }} - action: deploy - secrets: - CODER_SESSION_TOKEN: ${{ secrets.CODER_SESSION_TOKEN }} - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + + - name: Compute workspace name + id: name + run: | + WORKSPACE_NAME=$(scripts/coder-preview/compute-name.sh sentry "${{ github.event.pull_request.number }}") + echo "workspace_name=${WORKSPACE_NAME}" >> "$GITHUB_OUTPUT" + + - name: Deploy preview + env: + CODER_SESSION_TOKEN: ${{ secrets.CODER_SESSION_TOKEN }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_REPOSITORY: ${{ github.repository }} + PR_NUMBER: ${{ github.event.pull_request.number }} + PR_URL: ${{ github.event.pull_request.html_url }} + WORKSPACE_NAME: ${{ steps.name.outputs.workspace_name }} + TEMPLATE_NAME: sentry-devbox + run: scripts/coder-preview/deploy.sh destroy: if: | github.event.action == 'closed' || (github.event.action == 'unlabeled' && github.event.label.name == 'deploy-preview') - uses: getsentry/devinfra-coder-infra/.github/workflows/coder-preview.yml@5cc8ec32cfed61df08f8ef642a0bed5a2f801ddd # main - with: - repo: sentry - caller_repo: ${{ github.repository }} - pr_number: ${{ github.event.pull_request.number }} - pr_url: ${{ github.event.pull_request.html_url }} - action: destroy - secrets: - CODER_SESSION_TOKEN: ${{ secrets.CODER_SESSION_TOKEN }} - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + + - name: Compute workspace name + id: name + run: | + WORKSPACE_NAME=$(scripts/coder-preview/compute-name.sh sentry "${{ github.event.pull_request.number }}") + echo "workspace_name=${WORKSPACE_NAME}" >> "$GITHUB_OUTPUT" + + - name: Destroy preview + env: + CODER_SESSION_TOKEN: ${{ secrets.CODER_SESSION_TOKEN }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_REPOSITORY: ${{ github.repository }} + PR_NUMBER: ${{ github.event.pull_request.number }} + WORKSPACE_NAME: ${{ steps.name.outputs.workspace_name }} + run: scripts/coder-preview/destroy.sh diff --git a/scripts/coder-preview/compute-name.sh b/scripts/coder-preview/compute-name.sh new file mode 100755 index 000000000000..ab9ac3aeb3d3 --- /dev/null +++ b/scripts/coder-preview/compute-name.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Maps repo + PR number to a deterministic workspace name. +# Usage: compute-name.sh +# repo: "sentry" or "getsentry" +# pr_number: numeric PR number + +REPO="${1:?Usage: compute-name.sh }" +PR_NUMBER="${2:?Usage: compute-name.sh }" + +case "$REPO" in + sentry|getsentry) ;; + *) echo "Unknown repo: $REPO" >&2; exit 1 ;; +esac + +echo "pr-${REPO}-${PR_NUMBER}" diff --git a/scripts/coder-preview/deploy.sh b/scripts/coder-preview/deploy.sh new file mode 100755 index 000000000000..2e91e1b8d25d --- /dev/null +++ b/scripts/coder-preview/deploy.sh @@ -0,0 +1,205 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Deploy a Coder preview workspace for a GitHub PR. +# +# Required env vars: +# CODER_URL - Coder server URL (e.g. https://coder.sentry.dev) +# CODER_SESSION_TOKEN - API token for the ci-bot Coder user +# GH_TOKEN - GitHub token for Deployments API +# GITHUB_REPOSITORY - owner/repo (e.g. getsentry/sentry) +# PR_NUMBER - PR number +# PR_URL - Full GitHub PR URL +# WORKSPACE_NAME - Computed workspace name (e.g. pr-sentry-12345) +# TEMPLATE_NAME - Coder template name (e.g. sentry-devbox) + +CODER_API="${CODER_URL}/api/v2" +AUTH_HEADER="Coder-Session-Token: ${CODER_SESSION_TOKEN}" +MACHINE_TYPE="e2-standard-4" + +# curl wrapper that shows response body on failure +api() { + local response http_code + response=$(curl -sS -w "\n%{http_code}" "$@") + http_code=$(echo "$response" | tail -1) + response=$(echo "$response" | sed '$d') + + if [[ "$http_code" -ge 400 ]]; then + echo "ERROR: HTTP ${http_code}" >&2 + echo "$response" >&2 + return 1 + fi + echo "$response" +} + +# ---------- GitHub Deployment: in_progress ---------- + +echo "Creating GitHub Deployment (in_progress)..." +DEPLOYMENT_ID=$(api -X POST \ + -H "Authorization: token ${GH_TOKEN}" \ + -H "Accept: application/vnd.github.v3+json" \ + "https://api.github.com/repos/${GITHUB_REPOSITORY}/deployments" \ + -d "$(jq -n \ + --arg ref "refs/pull/${PR_NUMBER}/head" \ + --arg env "pr-preview-${PR_NUMBER}" \ + '{ + ref: $ref, + environment: $env, + auto_merge: false, + required_contexts: [], + transient_environment: true, + production_environment: false, + description: "Coder PR preview workspace" + }')" | jq -r '.id') + +echo "Deployment ID: ${DEPLOYMENT_ID}" + +update_deployment_status() { + local state="$1" + local url="${2:-}" + local description="${3:-}" + + local body + body=$(jq -n \ + --arg state "$state" \ + --arg url "$url" \ + --arg description "$description" \ + '{ + state: $state, + environment_url: (if $url != "" then $url else null end), + description: (if $description != "" then $description else null end), + auto_inactive: false + }') + + api -X POST \ + -H "Authorization: token ${GH_TOKEN}" \ + -H "Accept: application/vnd.github.v3+json" \ + "https://api.github.com/repos/${GITHUB_REPOSITORY}/deployments/${DEPLOYMENT_ID}/statuses" \ + -d "$body" > /dev/null +} + +# ---------- Look up template ---------- + +echo "Looking up template '${TEMPLATE_NAME}'..." +TEMPLATE_ID=$(api \ + -H "${AUTH_HEADER}" \ + "${CODER_API}/templates" | jq -r ".[] | select(.name == \"${TEMPLATE_NAME}\") | .id") + +if [ -z "$TEMPLATE_ID" ]; then + echo "ERROR: Template '${TEMPLATE_NAME}' not found" >&2 + update_deployment_status "failure" "" "Template not found" + exit 1 +fi + +ACTIVE_VERSION_ID=$(api \ + -H "${AUTH_HEADER}" \ + "${CODER_API}/templates/${TEMPLATE_ID}" | jq -r '.active_version_id') + +echo "Template ID: ${TEMPLATE_ID}, Active Version: ${ACTIVE_VERSION_ID}" + +# ---------- Build parameter payload (shared across create/start/rebuild) ---------- + +BUILD_PARAMS=$(jq -n \ + --arg version_id "$ACTIVE_VERSION_ID" \ + --arg pr_url "$PR_URL" \ + --arg machine_type "$MACHINE_TYPE" \ + '{ + template_version_id: $version_id, + transition: "start", + rich_parameter_values: [ + {name: "branch", value: $pr_url}, + {name: "start_devserver", value: "true"}, + {name: "machine_type", value: $machine_type} + ] + }') + +# ---------- Check if workspace already exists ---------- + +echo "Checking for existing workspace '${WORKSPACE_NAME}'..." +WORKSPACE_JSON=$(api \ + -H "${AUTH_HEADER}" \ + "${CODER_API}/users/me/workspaces?q=name:${WORKSPACE_NAME}" | jq -r '.workspaces[0] // empty') + +if [ -n "$WORKSPACE_JSON" ]; then + WORKSPACE_ID=$(echo "$WORKSPACE_JSON" | jq -r '.id') + WORKSPACE_STATUS=$(echo "$WORKSPACE_JSON" | jq -r '.latest_build.status') + echo "Workspace exists: ${WORKSPACE_ID} (status: ${WORKSPACE_STATUS})" + + case "$WORKSPACE_STATUS" in + running|stopped|failed) + echo "Workspace is ${WORKSPACE_STATUS}, triggering build with updated branch..." + api -X POST \ + -H "${AUTH_HEADER}" \ + -H "Content-Type: application/json" \ + "${CODER_API}/workspaces/${WORKSPACE_ID}/builds" \ + -d "$BUILD_PARAMS" > /dev/null + ;; + *) + echo "Workspace in unexpected status '${WORKSPACE_STATUS}', waiting..." + ;; + esac +else + # ---------- Create new workspace ---------- + echo "Creating workspace '${WORKSPACE_NAME}'..." + WORKSPACE_JSON=$(api -X POST \ + -H "${AUTH_HEADER}" \ + -H "Content-Type: application/json" \ + "${CODER_API}/users/me/workspaces" \ + -d "$(jq -n \ + --arg name "$WORKSPACE_NAME" \ + --argjson params "$BUILD_PARAMS" \ + '$params + {name: $name}')") + WORKSPACE_ID=$(echo "$WORKSPACE_JSON" | jq -r '.id') + echo "Created workspace: ${WORKSPACE_ID}" +fi + +# ---------- Poll until running ---------- + +echo "Waiting for workspace to be running..." +MAX_WAIT=300 # 5 minutes +ELAPSED=0 +POLL_INTERVAL=10 + +while [ "$ELAPSED" -lt "$MAX_WAIT" ]; do + STATUS=$(api \ + -H "${AUTH_HEADER}" \ + "${CODER_API}/workspaces/${WORKSPACE_ID}" | jq -r '.latest_build.status') + + echo " Status: ${STATUS} (${ELAPSED}s elapsed)" + + case "$STATUS" in + running) + echo "Workspace is running!" + break + ;; + failed|canceled) + echo "ERROR: Workspace build ${STATUS}" >&2 + update_deployment_status "failure" "" "Workspace build ${STATUS}" + exit 1 + ;; + esac + + sleep "$POLL_INTERVAL" + ELAPSED=$((ELAPSED + POLL_INTERVAL)) +done + +if [ "$ELAPSED" -ge "$MAX_WAIT" ]; then + echo "ERROR: Timed out waiting for workspace" >&2 + update_deployment_status "failure" "" "Workspace build timed out" + exit 1 +fi + +# ---------- Get owner name for URL ---------- + +OWNER_NAME=$(api \ + -H "${AUTH_HEADER}" \ + "${CODER_API}/users/me" | jq -r '.username') + +PREVIEW_URL="https://sentry-dev--${WORKSPACE_NAME}--${OWNER_NAME}.coder.sentry.dev" +echo "Preview URL: ${PREVIEW_URL}" + +# ---------- GitHub Deployment: success ---------- + +update_deployment_status "success" "$PREVIEW_URL" "Coder preview workspace running" + +echo "Done! Deployment marked as success." diff --git a/scripts/coder-preview/destroy.sh b/scripts/coder-preview/destroy.sh new file mode 100755 index 000000000000..a65f4a2d5b3c --- /dev/null +++ b/scripts/coder-preview/destroy.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Destroy a Coder preview workspace and mark GitHub Deployments inactive. +# +# Required env vars: +# CODER_URL - Coder server URL (e.g. https://coder.sentry.dev) +# CODER_SESSION_TOKEN - API token for the ci-bot Coder user +# GH_TOKEN - GitHub token for Deployments API +# GITHUB_REPOSITORY - owner/repo (e.g. getsentry/sentry) +# PR_NUMBER - PR number +# WORKSPACE_NAME - Computed workspace name (e.g. pr-sentry-12345) + +CODER_API="${CODER_URL}/api/v2" +AUTH_HEADER="Coder-Session-Token: ${CODER_SESSION_TOKEN}" + +# curl wrapper that shows response body on failure +api() { + local response http_code + response=$(curl -sS -w "\n%{http_code}" "$@") + http_code=$(echo "$response" | tail -1) + response=$(echo "$response" | sed '$d') + + if [[ "$http_code" -ge 400 ]]; then + echo "ERROR: HTTP ${http_code}" >&2 + echo "$response" >&2 + return 1 + fi + echo "$response" +} + +# ---------- Delete Coder workspace ---------- + +echo "Looking up workspace '${WORKSPACE_NAME}'..." +WORKSPACE_JSON=$(api \ + -H "${AUTH_HEADER}" \ + "${CODER_API}/users/me/workspaces?q=name:${WORKSPACE_NAME}" | jq -r '.workspaces[0] // empty') + +if [ -n "$WORKSPACE_JSON" ]; then + WORKSPACE_ID=$(echo "$WORKSPACE_JSON" | jq -r '.id') + WORKSPACE_STATUS=$(echo "$WORKSPACE_JSON" | jq -r '.latest_build.status') + echo "Found workspace ${WORKSPACE_ID} (status: ${WORKSPACE_STATUS})" + + echo "Deleting workspace..." + api -X POST \ + -H "${AUTH_HEADER}" \ + -H "Content-Type: application/json" \ + "${CODER_API}/workspaces/${WORKSPACE_ID}/builds" \ + -d '{"transition": "delete"}' > /dev/null + + echo "Workspace deletion initiated." +else + echo "Workspace '${WORKSPACE_NAME}' not found, nothing to delete." +fi + +# ---------- Mark GitHub Deployments inactive ---------- + +echo "Marking GitHub Deployments inactive for PR #${PR_NUMBER}..." +ENVIRONMENT="pr-preview-${PR_NUMBER}" + +DEPLOYMENTS=$(api \ + -H "Authorization: token ${GH_TOKEN}" \ + -H "Accept: application/vnd.github.v3+json" \ + "https://api.github.com/repos/${GITHUB_REPOSITORY}/deployments?environment=${ENVIRONMENT}") + +DEPLOYMENT_COUNT=$(echo "$DEPLOYMENTS" | jq 'length') +echo "Found ${DEPLOYMENT_COUNT} deployment(s) for environment '${ENVIRONMENT}'" + +echo "$DEPLOYMENTS" | jq -r '.[].id' | while read -r dep_id; do + echo " Marking deployment ${dep_id} as inactive..." + api -X POST \ + -H "Authorization: token ${GH_TOKEN}" \ + -H "Accept: application/vnd.github.v3+json" \ + "https://api.github.com/repos/${GITHUB_REPOSITORY}/deployments/${dep_id}/statuses" \ + -d '{"state": "inactive"}' > /dev/null +done + +echo "Done! All deployments marked inactive." From 4f1b07f214716431b7824b29e2d029b9848ac745 Mon Sep 17 00:00:00 2001 From: Hubert Deng Date: Mon, 2 Mar 2026 14:46:37 -0800 Subject: [PATCH 03/11] fix deploy scripts --- scripts/coder-preview/deploy.sh | 2 +- scripts/coder-preview/destroy.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/coder-preview/deploy.sh b/scripts/coder-preview/deploy.sh index 2e91e1b8d25d..f0fa97151409 100755 --- a/scripts/coder-preview/deploy.sh +++ b/scripts/coder-preview/deploy.sh @@ -118,7 +118,7 @@ BUILD_PARAMS=$(jq -n \ echo "Checking for existing workspace '${WORKSPACE_NAME}'..." WORKSPACE_JSON=$(api \ -H "${AUTH_HEADER}" \ - "${CODER_API}/users/me/workspaces?q=name:${WORKSPACE_NAME}" | jq -r '.workspaces[0] // empty') + "${CODER_API}/workspaces?q=owner:me+name:${WORKSPACE_NAME}" | jq -r '.workspaces[0] // empty') if [ -n "$WORKSPACE_JSON" ]; then WORKSPACE_ID=$(echo "$WORKSPACE_JSON" | jq -r '.id') diff --git a/scripts/coder-preview/destroy.sh b/scripts/coder-preview/destroy.sh index a65f4a2d5b3c..0fa951e94992 100755 --- a/scripts/coder-preview/destroy.sh +++ b/scripts/coder-preview/destroy.sh @@ -34,7 +34,7 @@ api() { echo "Looking up workspace '${WORKSPACE_NAME}'..." WORKSPACE_JSON=$(api \ -H "${AUTH_HEADER}" \ - "${CODER_API}/users/me/workspaces?q=name:${WORKSPACE_NAME}" | jq -r '.workspaces[0] // empty') + "${CODER_API}/workspaces?q=owner:me+name:${WORKSPACE_NAME}" | jq -r '.workspaces[0] // empty') if [ -n "$WORKSPACE_JSON" ]; then WORKSPACE_ID=$(echo "$WORKSPACE_JSON" | jq -r '.id') From 517ac45cf1c0773cd638a58bf77a72ac3ab2cd1c Mon Sep 17 00:00:00 2001 From: Hubert Deng Date: Mon, 2 Mar 2026 14:48:34 -0800 Subject: [PATCH 04/11] use working vm params --- scripts/coder-preview/deploy.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/coder-preview/deploy.sh b/scripts/coder-preview/deploy.sh index f0fa97151409..02dda3d47261 100755 --- a/scripts/coder-preview/deploy.sh +++ b/scripts/coder-preview/deploy.sh @@ -15,7 +15,7 @@ set -euo pipefail CODER_API="${CODER_URL}/api/v2" AUTH_HEADER="Coder-Session-Token: ${CODER_SESSION_TOKEN}" -MACHINE_TYPE="e2-standard-4" +MACHINE_TYPE="e2-standard-8" # curl wrapper that shows response body on failure api() { From 316177e2e65c38afe5c6ca423e0cc765a71043e6 Mon Sep 17 00:00:00 2001 From: Hubert Deng Date: Mon, 2 Mar 2026 14:55:10 -0800 Subject: [PATCH 05/11] fix scripts --- scripts/coder-preview/deploy.sh | 28 +++++++++++++++------------- scripts/coder-preview/destroy.sh | 26 +++++++++++++++++++------- 2 files changed, 34 insertions(+), 20 deletions(-) diff --git a/scripts/coder-preview/deploy.sh b/scripts/coder-preview/deploy.sh index 02dda3d47261..c5014bf4b83e 100755 --- a/scripts/coder-preview/deploy.sh +++ b/scripts/coder-preview/deploy.sh @@ -125,19 +125,21 @@ if [ -n "$WORKSPACE_JSON" ]; then WORKSPACE_STATUS=$(echo "$WORKSPACE_JSON" | jq -r '.latest_build.status') echo "Workspace exists: ${WORKSPACE_ID} (status: ${WORKSPACE_STATUS})" - case "$WORKSPACE_STATUS" in - running|stopped|failed) - echo "Workspace is ${WORKSPACE_STATUS}, triggering build with updated branch..." - api -X POST \ - -H "${AUTH_HEADER}" \ - -H "Content-Type: application/json" \ - "${CODER_API}/workspaces/${WORKSPACE_ID}/builds" \ - -d "$BUILD_PARAMS" > /dev/null - ;; - *) - echo "Workspace in unexpected status '${WORKSPACE_STATUS}', waiting..." - ;; - esac + # Wait for any in-progress build to finish before triggering a new one + while [[ "$WORKSPACE_STATUS" == "starting" || "$WORKSPACE_STATUS" == "stopping" || "$WORKSPACE_STATUS" == "canceling" || "$WORKSPACE_STATUS" == "pending" ]]; do + echo " Build in progress (${WORKSPACE_STATUS}), waiting..." + sleep 10 + WORKSPACE_STATUS=$(api \ + -H "${AUTH_HEADER}" \ + "${CODER_API}/workspaces/${WORKSPACE_ID}" | jq -r '.latest_build.status') + done + + echo "Workspace is ${WORKSPACE_STATUS}, triggering build with updated branch..." + api -X POST \ + -H "${AUTH_HEADER}" \ + -H "Content-Type: application/json" \ + "${CODER_API}/workspaces/${WORKSPACE_ID}/builds" \ + -d "$BUILD_PARAMS" > /dev/null else # ---------- Create new workspace ---------- echo "Creating workspace '${WORKSPACE_NAME}'..." diff --git a/scripts/coder-preview/destroy.sh b/scripts/coder-preview/destroy.sh index 0fa951e94992..5989cc88ea5a 100755 --- a/scripts/coder-preview/destroy.sh +++ b/scripts/coder-preview/destroy.sh @@ -41,14 +41,26 @@ if [ -n "$WORKSPACE_JSON" ]; then WORKSPACE_STATUS=$(echo "$WORKSPACE_JSON" | jq -r '.latest_build.status') echo "Found workspace ${WORKSPACE_ID} (status: ${WORKSPACE_STATUS})" - echo "Deleting workspace..." - api -X POST \ - -H "${AUTH_HEADER}" \ - -H "Content-Type: application/json" \ - "${CODER_API}/workspaces/${WORKSPACE_ID}/builds" \ - -d '{"transition": "delete"}' > /dev/null + # Wait for any active build to finish before deleting + while [[ "$WORKSPACE_STATUS" == "starting" || "$WORKSPACE_STATUS" == "stopping" || "$WORKSPACE_STATUS" == "deleting" ]]; do + echo " Build in progress (${WORKSPACE_STATUS}), waiting..." + sleep 10 + WORKSPACE_STATUS=$(api \ + -H "${AUTH_HEADER}" \ + "${CODER_API}/workspaces/${WORKSPACE_ID}" | jq -r '.latest_build.status') + done - echo "Workspace deletion initiated." + if [ "$WORKSPACE_STATUS" = "deleted" ]; then + echo "Workspace already deleted." + else + echo "Deleting workspace..." + api -X POST \ + -H "${AUTH_HEADER}" \ + -H "Content-Type: application/json" \ + "${CODER_API}/workspaces/${WORKSPACE_ID}/builds" \ + -d '{"transition": "delete"}' > /dev/null + echo "Workspace deletion initiated." + fi else echo "Workspace '${WORKSPACE_NAME}' not found, nothing to delete." fi From dcc6a85e13e3a9ca848aaf0e56a1706ba6ce87f3 Mon Sep 17 00:00:00 2001 From: Hubert Deng Date: Mon, 2 Mar 2026 14:58:53 -0800 Subject: [PATCH 06/11] fix deletion race --- scripts/coder-preview/deploy.sh | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/scripts/coder-preview/deploy.sh b/scripts/coder-preview/deploy.sh index c5014bf4b83e..3bbc06907303 100755 --- a/scripts/coder-preview/deploy.sh +++ b/scripts/coder-preview/deploy.sh @@ -126,7 +126,7 @@ if [ -n "$WORKSPACE_JSON" ]; then echo "Workspace exists: ${WORKSPACE_ID} (status: ${WORKSPACE_STATUS})" # Wait for any in-progress build to finish before triggering a new one - while [[ "$WORKSPACE_STATUS" == "starting" || "$WORKSPACE_STATUS" == "stopping" || "$WORKSPACE_STATUS" == "canceling" || "$WORKSPACE_STATUS" == "pending" ]]; do + while [[ "$WORKSPACE_STATUS" == "starting" || "$WORKSPACE_STATUS" == "stopping" || "$WORKSPACE_STATUS" == "canceling" || "$WORKSPACE_STATUS" == "pending" || "$WORKSPACE_STATUS" == "deleting" ]]; do echo " Build in progress (${WORKSPACE_STATUS}), waiting..." sleep 10 WORKSPACE_STATUS=$(api \ @@ -134,13 +134,20 @@ if [ -n "$WORKSPACE_JSON" ]; then "${CODER_API}/workspaces/${WORKSPACE_ID}" | jq -r '.latest_build.status') done - echo "Workspace is ${WORKSPACE_STATUS}, triggering build with updated branch..." - api -X POST \ - -H "${AUTH_HEADER}" \ - -H "Content-Type: application/json" \ - "${CODER_API}/workspaces/${WORKSPACE_ID}/builds" \ - -d "$BUILD_PARAMS" > /dev/null -else + if [ "$WORKSPACE_STATUS" = "deleted" ]; then + echo "Workspace was deleted, creating fresh..." + WORKSPACE_JSON="" + else + echo "Workspace is ${WORKSPACE_STATUS}, triggering build with updated branch..." + api -X POST \ + -H "${AUTH_HEADER}" \ + -H "Content-Type: application/json" \ + "${CODER_API}/workspaces/${WORKSPACE_ID}/builds" \ + -d "$BUILD_PARAMS" > /dev/null + fi +fi + +if [ -z "$WORKSPACE_JSON" ]; then # ---------- Create new workspace ---------- echo "Creating workspace '${WORKSPACE_NAME}'..." WORKSPACE_JSON=$(api -X POST \ From b611232ef10899b1816e21f62aa885222827bef7 Mon Sep 17 00:00:00 2001 From: Hubert Deng Date: Mon, 2 Mar 2026 15:06:42 -0800 Subject: [PATCH 07/11] wait for agent to connect --- scripts/coder-preview/deploy.sh | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/scripts/coder-preview/deploy.sh b/scripts/coder-preview/deploy.sh index 3bbc06907303..da6ff7d62603 100755 --- a/scripts/coder-preview/deploy.sh +++ b/scripts/coder-preview/deploy.sh @@ -198,6 +198,34 @@ if [ "$ELAPSED" -ge "$MAX_WAIT" ]; then exit 1 fi +# ---------- Wait for agent to connect ---------- + +echo "Waiting for agent to connect..." +MAX_AGENT_WAIT=300 # 5 minutes +AGENT_ELAPSED=0 + +while [ "$AGENT_ELAPSED" -lt "$MAX_AGENT_WAIT" ]; do + AGENT_STATUS=$(api \ + -H "${AUTH_HEADER}" \ + "${CODER_API}/workspaces/${WORKSPACE_ID}" | jq -r '.latest_build.resources[]?.agents[]?.status // empty' | head -1) + + echo " Agent: ${AGENT_STATUS:-no agent yet} (${AGENT_ELAPSED}s elapsed)" + + if [ "$AGENT_STATUS" = "connected" ]; then + echo "Agent connected!" + break + fi + + sleep 10 + AGENT_ELAPSED=$((AGENT_ELAPSED + 10)) +done + +if [ "$AGENT_ELAPSED" -ge "$MAX_AGENT_WAIT" ]; then + echo "ERROR: Agent did not connect within timeout" >&2 + update_deployment_status "failure" "" "Agent failed to connect" + exit 1 +fi + # ---------- Get owner name for URL ---------- OWNER_NAME=$(api \ From 67692883cf223caf84bb5eb1041d6095573ea9cc Mon Sep 17 00:00:00 2001 From: Hubert Deng Date: Mon, 2 Mar 2026 15:10:05 -0800 Subject: [PATCH 08/11] also wait for devserver to become healthy --- scripts/coder-preview/deploy.sh | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/scripts/coder-preview/deploy.sh b/scripts/coder-preview/deploy.sh index da6ff7d62603..c12b5807161a 100755 --- a/scripts/coder-preview/deploy.sh +++ b/scripts/coder-preview/deploy.sh @@ -226,6 +226,38 @@ if [ "$AGENT_ELAPSED" -ge "$MAX_AGENT_WAIT" ]; then exit 1 fi +# ---------- Wait for devserver to be healthy ---------- + +echo "Waiting for devserver health check..." +MAX_HEALTH_WAIT=600 # 10 minutes — devserver takes a while to start +HEALTH_ELAPSED=0 + +OWNER_NAME=$(api \ + -H "${AUTH_HEADER}" \ + "${CODER_API}/users/me" | jq -r '.username') + +PREVIEW_URL="https://sentry-dev--${WORKSPACE_NAME}--${OWNER_NAME}.coder.sentry.dev" + +while [ "$HEALTH_ELAPSED" -lt "$MAX_HEALTH_WAIT" ]; do + HTTP_CODE=$(curl -sS -o /dev/null -w "%{http_code}" "${PREVIEW_URL}/_health/" 2>/dev/null || echo "000") + + echo " Health check: HTTP ${HTTP_CODE} (${HEALTH_ELAPSED}s elapsed)" + + if [ "$HTTP_CODE" = "200" ]; then + echo "Devserver is healthy!" + break + fi + + sleep 15 + HEALTH_ELAPSED=$((HEALTH_ELAPSED + 15)) +done + +if [ "$HEALTH_ELAPSED" -ge "$MAX_HEALTH_WAIT" ]; then + echo "ERROR: Devserver did not become healthy within timeout" >&2 + update_deployment_status "failure" "" "Devserver health check timed out" + exit 1 +fi + # ---------- Get owner name for URL ---------- OWNER_NAME=$(api \ From b6b615f5b3d081c0ca5cc5e1012348ff4f4bed2a Mon Sep 17 00:00:00 2001 From: Hubert Deng Date: Mon, 2 Mar 2026 15:11:56 -0800 Subject: [PATCH 09/11] cleanup --- scripts/coder-preview/deploy.sh | 7 ------- 1 file changed, 7 deletions(-) diff --git a/scripts/coder-preview/deploy.sh b/scripts/coder-preview/deploy.sh index c12b5807161a..2a45619c9b53 100755 --- a/scripts/coder-preview/deploy.sh +++ b/scripts/coder-preview/deploy.sh @@ -258,13 +258,6 @@ if [ "$HEALTH_ELAPSED" -ge "$MAX_HEALTH_WAIT" ]; then exit 1 fi -# ---------- Get owner name for URL ---------- - -OWNER_NAME=$(api \ - -H "${AUTH_HEADER}" \ - "${CODER_API}/users/me" | jq -r '.username') - -PREVIEW_URL="https://sentry-dev--${WORKSPACE_NAME}--${OWNER_NAME}.coder.sentry.dev" echo "Preview URL: ${PREVIEW_URL}" # ---------- GitHub Deployment: success ---------- From d03f16c048e717e38572cc152be60452ffb3dcf3 Mon Sep 17 00:00:00 2001 From: Hubert Deng Date: Mon, 2 Mar 2026 15:24:16 -0800 Subject: [PATCH 10/11] use coder healthchecks --- scripts/coder-preview/deploy.sh | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/scripts/coder-preview/deploy.sh b/scripts/coder-preview/deploy.sh index 2a45619c9b53..52ffd43d055d 100755 --- a/scripts/coder-preview/deploy.sh +++ b/scripts/coder-preview/deploy.sh @@ -232,18 +232,14 @@ echo "Waiting for devserver health check..." MAX_HEALTH_WAIT=600 # 10 minutes — devserver takes a while to start HEALTH_ELAPSED=0 -OWNER_NAME=$(api \ - -H "${AUTH_HEADER}" \ - "${CODER_API}/users/me" | jq -r '.username') - -PREVIEW_URL="https://sentry-dev--${WORKSPACE_NAME}--${OWNER_NAME}.coder.sentry.dev" - while [ "$HEALTH_ELAPSED" -lt "$MAX_HEALTH_WAIT" ]; do - HTTP_CODE=$(curl -sS -o /dev/null -w "%{http_code}" "${PREVIEW_URL}/_health/" 2>/dev/null || echo "000") + APP_HEALTH=$(api \ + -H "${AUTH_HEADER}" \ + "${CODER_API}/workspaces/${WORKSPACE_ID}" | jq -r '.latest_build.resources[]?.agents[]?.apps[]? | select(.slug == "sentry-dev") | .health' | head -1) - echo " Health check: HTTP ${HTTP_CODE} (${HEALTH_ELAPSED}s elapsed)" + echo " Devserver app health: ${APP_HEALTH:-unknown} (${HEALTH_ELAPSED}s elapsed)" - if [ "$HTTP_CODE" = "200" ]; then + if [ "$APP_HEALTH" = "healthy" ]; then echo "Devserver is healthy!" break fi @@ -258,6 +254,11 @@ if [ "$HEALTH_ELAPSED" -ge "$MAX_HEALTH_WAIT" ]; then exit 1 fi +OWNER_NAME=$(api \ + -H "${AUTH_HEADER}" \ + "${CODER_API}/users/me" | jq -r '.username') + +PREVIEW_URL="https://sentry-dev--${WORKSPACE_NAME}--${OWNER_NAME}.coder.sentry.dev" echo "Preview URL: ${PREVIEW_URL}" # ---------- GitHub Deployment: success ---------- From ecfda09407f157395fd0d27e6921ce6c8d060ab4 Mon Sep 17 00:00:00 2001 From: Hubert Deng Date: Mon, 2 Mar 2026 16:36:25 -0800 Subject: [PATCH 11/11] don't error on 410 --- scripts/coder-preview/deploy.sh | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/scripts/coder-preview/deploy.sh b/scripts/coder-preview/deploy.sh index 52ffd43d055d..fac43ff2c149 100755 --- a/scripts/coder-preview/deploy.sh +++ b/scripts/coder-preview/deploy.sh @@ -129,9 +129,12 @@ if [ -n "$WORKSPACE_JSON" ]; then while [[ "$WORKSPACE_STATUS" == "starting" || "$WORKSPACE_STATUS" == "stopping" || "$WORKSPACE_STATUS" == "canceling" || "$WORKSPACE_STATUS" == "pending" || "$WORKSPACE_STATUS" == "deleting" ]]; do echo " Build in progress (${WORKSPACE_STATUS}), waiting..." sleep 10 - WORKSPACE_STATUS=$(api \ + # 410 means workspace was fully deleted + if ! WORKSPACE_STATUS=$(api \ -H "${AUTH_HEADER}" \ - "${CODER_API}/workspaces/${WORKSPACE_ID}" | jq -r '.latest_build.status') + "${CODER_API}/workspaces/${WORKSPACE_ID}" | jq -r '.latest_build.status'); then + WORKSPACE_STATUS="deleted" + fi done if [ "$WORKSPACE_STATUS" = "deleted" ]; then