From 0e18a3d0cea664b692834f3528ecd9860f37be1c Mon Sep 17 00:00:00 2001 From: RyanAI Date: Wed, 4 Mar 2026 07:44:41 +0800 Subject: [PATCH 1/5] feat: support branch-scoped preview projects and cleanup --- .../workflows/deno-deploy-preview-cleanup.yml | 143 ++++++++++++++++++ .github/workflows/deno-deploy-reusable.yml | 91 ++++++++--- README.md | 36 ++++- 3 files changed, 250 insertions(+), 20 deletions(-) create mode 100644 .github/workflows/deno-deploy-preview-cleanup.yml diff --git a/.github/workflows/deno-deploy-preview-cleanup.yml b/.github/workflows/deno-deploy-preview-cleanup.yml new file mode 100644 index 0000000..8ee7e9e --- /dev/null +++ b/.github/workflows/deno-deploy-preview-cleanup.yml @@ -0,0 +1,143 @@ +name: Reusable - Deno Deploy Preview Cleanup + +on: + workflow_call: + inputs: + project: + description: Production project name (must end with -ubq-fi) + required: true + type: string + ref_name: + description: Deleted branch ref name (for example github.event.ref from a delete event) + required: true + type: string + preview_project: + description: Explicit preview project name to delete (overrides auto-generated name) + required: false + type: string + default: "" + preview_strategy: + description: Preview naming strategy when preview_project is not provided (branch|shared) + required: false + type: string + default: "branch" + prod_branch: + description: Production branch name (skips deletion when ref_name equals this branch) + required: false + type: string + default: "" + secrets: + DENO_DEPLOY_TOKEN: + required: true + +permissions: + contents: read + +jobs: + cleanup: + name: Delete preview project + runs-on: ubuntu-22.04 + env: + PROJECT: ${{ inputs.project }} + REF_NAME: ${{ inputs.ref_name }} + PREVIEW_PROJECT: ${{ inputs.preview_project }} + PREVIEW_STRATEGY: ${{ inputs.preview_strategy }} + PROD_BRANCH: ${{ inputs.prod_branch != '' && inputs.prod_branch || github.event.repository.default_branch }} + DENO_DEPLOY_TOKEN: ${{ secrets.DENO_DEPLOY_TOKEN }} + steps: + - name: Determine preview project name + id: target + run: | + set -euo pipefail + + slugify() { + printf '%s' "$1" \ + | tr '[:upper:]' '[:lower:]' \ + | sed -E 's#[^a-z0-9]+#-#g; s#-+#-#g; s#^-+##; s#-+$##' + } + + project="${PROJECT:-}" + if [[ "$project" != *-ubq-fi ]]; then + echo "::error::Project name must end with '-ubq-fi'. Got: $project" + exit 1 + fi + + ref_name="${REF_NAME:-}" + if [ -z "$ref_name" ]; then + echo "::notice::Empty ref_name, skipping cleanup" + echo "skip=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + + if [ "$ref_name" = "$PROD_BRANCH" ]; then + echo "::notice::Deleted ref matches production branch (${PROD_BRANCH}), skipping cleanup" + echo "skip=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + + base="${project%-ubq-fi}" + target="${PREVIEW_PROJECT:-}" + strategy="${PREVIEW_STRATEGY:-branch}" + + if [ -z "$target" ]; then + if [ "$strategy" = "branch" ]; then + branch_slug="$(slugify "$ref_name")" + if [ -z "$branch_slug" ]; then + echo "::notice::Unable to derive branch slug from ref '${ref_name}', skipping cleanup" + echo "skip=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + + suffix="-${base}-ubq-fi" + max_branch_len=$((26 - ${#suffix})) + if [ $max_branch_len -lt 6 ]; then + echo "::error::Cannot build branch preview name for base '${base}' within Deno project length limits" + exit 1 + fi + if [ ${#branch_slug} -gt $max_branch_len ]; then + hash=$(printf '%s' "$branch_slug" | sha1sum | cut -c1-4) + head_len=$((max_branch_len - 5)) + branch_slug="${branch_slug:0:${head_len}}-${hash}" + fi + + target="${branch_slug}-${base}-ubq-fi" + else + if [ ${#base} -gt 17 ]; then + hash=$(printf '%s' "$base" | sha1sum | cut -c1-4) + base="${base:0:12}-${hash}" + fi + target="p-${base}-ubq-fi" + fi + fi + + if ! [[ "$target" =~ ^[a-z0-9]([a-z0-9-]*[a-z0-9])?$ ]]; then + echo "::error::Derived preview project name is invalid: $target" + exit 1 + fi + + echo "skip=false" >> "$GITHUB_OUTPUT" + echo "project=$target" >> "$GITHUB_OUTPUT" + + - name: Delete preview project + if: steps.target.outputs.skip != 'true' + env: + TARGET_PROJECT: ${{ steps.target.outputs.project }} + run: | + set -euo pipefail + api="https://dash.deno.com/api/projects/${TARGET_PROJECT}" + code=$(curl -sS -o /tmp/delete-preview-project.json -w "%{http_code}" -X DELETE \ + -H "Authorization: Bearer ${DENO_DEPLOY_TOKEN}" \ + "$api" || echo "000") + + if [ "$code" = "404" ]; then + echo "Preview project ${TARGET_PROJECT} already removed" + exit 0 + fi + + if [ "$code" -lt 200 ] || [ "$code" -ge 300 ]; then + echo "::error::Failed to delete preview project ${TARGET_PROJECT} (HTTP ${code})" + cat /tmp/delete-preview-project.json || true + exit 1 + fi + + echo "Deleted preview project ${TARGET_PROJECT}" diff --git a/.github/workflows/deno-deploy-reusable.yml b/.github/workflows/deno-deploy-reusable.yml index 826d6ba..574a92b 100644 --- a/.github/workflows/deno-deploy-reusable.yml +++ b/.github/workflows/deno-deploy-reusable.yml @@ -9,10 +9,15 @@ on: type: string default: "" preview_project: - description: Optional preview project name (defaults to "preview-") + description: Optional preview project name (overrides auto-generated preview project) required: false type: string default: "" + preview_strategy: + description: Preview naming strategy when preview_project is not provided (branch|shared) + required: false + type: string + default: "branch" entrypoint: description: Entrypoint file passed to deployctl (relative to root) required: true @@ -252,6 +257,7 @@ jobs: PROD_BRANCH: ${{ inputs.prod_branch != '' && inputs.prod_branch || github.event.repository.default_branch }} PROJECT: ${{ inputs.project }} PREVIEW_PROJECT: ${{ inputs.preview_project }} + PREVIEW_STRATEGY: ${{ inputs.preview_strategy }} ENTRYPOINT: ${{ inputs.entrypoint }} ROOT_DIR: ${{ inputs.root }} ARTIFACT_NAME: ${{ inputs.artifact_name }} @@ -452,11 +458,17 @@ jobs: id: target env: REF_NAME: ${{ github.ref_name }} + HEAD_REF: ${{ github.head_ref || '' }} EVENT_NAME: ${{ github.event_name }} WORKFLOW_EVENT: ${{ github.event.workflow_run.event || '' }} HEAD_BRANCH: ${{ github.event.workflow_run.head_branch || '' }} run: | set -euo pipefail + + slugify() { + printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | sed -E 's#[^a-z0-9]+#-#g; s#-+#-#g; s#^-+##; s#-+$##' + } + project="${PROJECT:-}" if [[ "$project" != *-ubq-fi ]]; then echo "::error::Project name must end with '-ubq-fi' to match router mapping. Got: $project" @@ -471,16 +483,56 @@ jobs: exit 1 fi + base="${project%-ubq-fi}" + ref_name="${REF_NAME:-}" + if [ "$EVENT_NAME" = "pull_request" ] && [ -n "$HEAD_REF" ]; then + ref_name="$HEAD_REF" + fi + if [ "$EVENT_NAME" = "workflow_run" ] && [ -n "$HEAD_BRANCH" ]; then + ref_name="$HEAD_BRANCH" + fi + + mode="preview" + if [ "$EVENT_NAME" = "workflow_run" ] && [ "$WORKFLOW_EVENT" = "pull_request" ]; then + : + elif [ "$ref_name" = "$PROD_BRANCH" ]; then + mode="production" + fi + preview="${PREVIEW_PROJECT:-}" + strategy="${PREVIEW_STRATEGY:-branch}" + branch_slug="" + router_host="" + if [ -z "$preview" ]; then - base="${project%-ubq-fi}" - # Clamp base to keep preview <=26 with prefix/suffix: p- + base + -ubq-fi - if [ ${#base} -gt 17 ]; then - # 12 chars + dash + 4-char hash = 17 - hash=$(printf '%s' "$base" | sha1sum | cut -c1-4) - base="${base:0:12}-${hash}" + if [ "$strategy" = "branch" ] && [ "$mode" = "preview" ]; then + branch_slug="$(slugify "$ref_name")" + if [ -z "$branch_slug" ]; then + branch_slug="preview" + fi + + suffix="-${base}-ubq-fi" + max_branch_len=$((26 - ${#suffix})) + if [ $max_branch_len -lt 6 ]; then + echo "::error::Cannot build branch preview name for base '${base}' within Deno project length limits" + exit 1 + fi + if [ ${#branch_slug} -gt $max_branch_len ]; then + hash=$(printf '%s' "$branch_slug" | sha1sum | cut -c1-4) + head_len=$((max_branch_len - 5)) + branch_slug="${branch_slug:0:${head_len}}-${hash}" + fi + preview="${branch_slug}-${base}-ubq-fi" + router_host="${branch_slug}-${base}.ubq.fi" + else + # Legacy shared preview project and host + if [ ${#base} -gt 17 ]; then + hash=$(printf '%s' "$base" | sha1sum | cut -c1-4) + base="${base:0:12}-${hash}" + fi + preview="p-${base}-ubq-fi" + router_host="preview-${base}.ubq.fi" fi - preview="p-${base}-ubq-fi" fi if [ ${#preview} -gt 26 ]; then @@ -492,22 +544,19 @@ jobs: exit 1 fi - ref_name="${REF_NAME:-}" - if [ "$EVENT_NAME" = "workflow_run" ] && [ -n "$HEAD_BRANCH" ]; then - ref_name="$HEAD_BRANCH" - fi - - mode="preview" target="$preview" - if [ "$EVENT_NAME" = "workflow_run" ] && [ "$WORKFLOW_EVENT" = "pull_request" ]; then - : - elif [ "$ref_name" = "$PROD_BRANCH" ]; then - mode="production" + if [ "$mode" = "production" ]; then target="$project" + router_host="${base}.ubq.fi" + elif [ -z "$router_host" ]; then + # preview_project was provided explicitly + router_host="preview-${base}.ubq.fi" fi echo "mode=$mode" >> "$GITHUB_OUTPUT" echo "project=$target" >> "$GITHUB_OUTPUT" + echo "router_host=$router_host" >> "$GITHUB_OUTPUT" + echo "branch_slug=$branch_slug" >> "$GITHUB_OUTPUT" - name: Export build env if: steps.preflight.outputs.deploy == 'yes' && env.BUILD_ENV != '' && env.ARTIFACT_NAME == '' @@ -966,6 +1015,7 @@ jobs: MODE: ${{ steps.deploy.outputs.mode }} TARGET_PROJECT: ${{ steps.deploy.outputs.project }} DEPLOYMENT_URL: ${{ steps.deploy.outputs.deployment_url || '' }} + ROUTER_HOST: ${{ steps.target.outputs.router_host || '' }} run: | set -euo pipefail mode="${MODE:-}" @@ -981,7 +1031,10 @@ jobs: base="${prod_project%-ubq-fi}" fi - if [ -n "$base" ]; then + router_host="${ROUTER_HOST:-}" + if [ -n "$router_host" ]; then + router_url="https://${router_host}" + elif [ -n "$base" ]; then if [ "$mode" = "production" ]; then router_url="https://${base}.ubq.fi" else diff --git a/README.md b/README.md index 1c13215..b0c9a95 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ This repository provides a standardized, reusable Deno Deploy workflow at `.gith - Supports Deno 2.x (default) with configurable versions. - Optional Node.js and Bun setup for builds (uses official install scripts). - Configurable install/build commands (multi-line supported). -- Branch-aware deployments: production on specified branch (default: `development`), preview on others. +- Branch-aware deployments: production on specified branch, branch-scoped previews on others (default strategy: `branch`; can be switched to shared preview mode). - Automatic preview project creation if missing. - Optional project existence check. `project_secrets` are forwarded as runtime env for the deploy (Deno Deploy secrets API is no longer supported). - Gitignore-based excludes with custom includes for build outputs. @@ -56,6 +56,40 @@ Notes: - `forward_all_secrets: true` (opt-in) forwards all available GitHub secrets as runtime env vars; defaults exclude `DENO_DEPLOY_TOKEN` and `GITHUB_TOKEN`. - Secrets managed in GitHub UI—update secret, next deploy forwards it. +### Branch-specific preview domains + cleanup + +By default, previews are now generated per branch (instead of collapsing everything into `preview-.ubq.fi`). + +- Branch `feat/widget` for `pay-ubq-fi` resolves to project `feat-widget-pay-ubq-fi` +- Router URL becomes `https://feat-widget-pay.ubq.fi` +- If a branch slug is too long, it is trimmed and hash-suffixed to stay within Deno's 26-char project-name limit. + +Optional input on the deploy reusable workflow: + +- `preview_strategy: branch` (default) — branch-scoped preview project/domain +- `preview_strategy: shared` — legacy shared preview project/domain + +To clean up branch preview projects when branches are deleted, add a delete-triggered workflow in the consumer repo: + +```yaml +name: Deno Deploy (cleanup preview project) + +on: + delete: + +jobs: + cleanup: + if: ${{ github.event.ref_type == 'branch' }} + uses: ubiquity/deno-deploy-workflow/.github/workflows/deno-deploy-preview-cleanup.yml@main + with: + project: -ubq-fi + ref_name: ${{ github.event.ref }} + prod_branch: development + preview_strategy: branch + secrets: + DENO_DEPLOY_TOKEN: ${{ secrets.DENO_DEPLOY_TOKEN }} +``` + ## Fork PR previews (artifact pipeline) Forked PRs cannot access secrets or org/repo vars in `pull_request` runs, so deployments must happen in a second workflow. Use the build-only reusable workflow to create an artifact, then a `workflow_run` deploy that downloads the artifact and deploys it. Use `build_env_fork`/`runtime_env_fork` for public values (never service/admin keys). From 659062f3c290b73331c61ee824b988dc1659ef26 Mon Sep 17 00:00:00 2001 From: RyanAI Date: Wed, 4 Mar 2026 07:56:41 +0800 Subject: [PATCH 2/5] style(ci): satisfy actionlint shellcheck output grouping --- .github/workflows/deno-deploy-reusable.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/deno-deploy-reusable.yml b/.github/workflows/deno-deploy-reusable.yml index 574a92b..e921110 100644 --- a/.github/workflows/deno-deploy-reusable.yml +++ b/.github/workflows/deno-deploy-reusable.yml @@ -553,10 +553,12 @@ jobs: router_host="preview-${base}.ubq.fi" fi - echo "mode=$mode" >> "$GITHUB_OUTPUT" - echo "project=$target" >> "$GITHUB_OUTPUT" - echo "router_host=$router_host" >> "$GITHUB_OUTPUT" - echo "branch_slug=$branch_slug" >> "$GITHUB_OUTPUT" + { + echo "mode=$mode" + echo "project=$target" + echo "router_host=$router_host" + echo "branch_slug=$branch_slug" + } >> "$GITHUB_OUTPUT" - name: Export build env if: steps.preflight.outputs.deploy == 'yes' && env.BUILD_ENV != '' && env.ARTIFACT_NAME == '' From 27cbead7240c4595e3612a3b7ef6372c2493c0bf Mon Sep 17 00:00:00 2001 From: Luna Ops Date: Thu, 9 Apr 2026 04:41:21 +0800 Subject: [PATCH 3/5] fix(workflow): gracefully fall back to shared preview for long project names When a project's base name is too long (e.g. notifications-ubq-fi with base='notifications'), max_branch_len becomes <6, making branch-based preview naming impractical (0-char branch slug + hash = ugly leading hyphens like '-a1b2c-notifications-ubq-fi'). Instead of exiting with an error, fall back to shared preview naming silently so repos with long names don't break after upgrading without having to explicitly set strategy=shared. P1 feedback from chatgpt-codex-connector[bot] on PR #16. --- .github/workflows/deno-deploy-reusable.yml | 36 ++++++++++++---------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/.github/workflows/deno-deploy-reusable.yml b/.github/workflows/deno-deploy-reusable.yml index e921110..139e40b 100644 --- a/.github/workflows/deno-deploy-reusable.yml +++ b/.github/workflows/deno-deploy-reusable.yml @@ -514,24 +514,28 @@ jobs: suffix="-${base}-ubq-fi" max_branch_len=$((26 - ${#suffix})) if [ $max_branch_len -lt 6 ]; then - echo "::error::Cannot build branch preview name for base '${base}' within Deno project length limits" - exit 1 - fi - if [ ${#branch_slug} -gt $max_branch_len ]; then - hash=$(printf '%s' "$branch_slug" | sha1sum | cut -c1-4) - head_len=$((max_branch_len - 5)) - branch_slug="${branch_slug:0:${head_len}}-${hash}" + # Project name is too long for branch-based previews; fall back to + # shared preview naming so repos with long names (e.g. notifications-ubq-fi) + # don't break after upgrading without having to explicitly set strategy=shared. + strategy="shared" fi - preview="${branch_slug}-${base}-ubq-fi" - router_host="${branch_slug}-${base}.ubq.fi" - else - # Legacy shared preview project and host - if [ ${#base} -gt 17 ]; then - hash=$(printf '%s' "$base" | sha1sum | cut -c1-4) - base="${base:0:12}-${hash}" + if [ "$strategy" = "branch" ]; then + if [ ${#branch_slug} -gt $max_branch_len ]; then + hash=$(printf '%s' "$branch_slug" | sha1sum | cut -c1-4) + head_len=$((max_branch_len - 5)) + branch_slug="${branch_slug:0:${head_len}}-${hash}" + fi + preview="${branch_slug}-${base}-ubq-fi" + router_host="${branch_slug}-${base}.ubq.fi" + else + # Legacy shared preview project and host + if [ ${#base} -gt 17 ]; then + hash=$(printf '%s' "$base" | sha1sum | cut -c1-4) + base="${base:0:12}-${hash}" + fi + preview="p-${base}-ubq-fi" + router_host="preview-${base}.ubq.fi" fi - preview="p-${base}-ubq-fi" - router_host="preview-${base}.ubq.fi" fi fi From c8e14008c530ed0eef7622a4d65f13043db707a8 Mon Sep 17 00:00:00 2001 From: Luna Ops Date: Thu, 9 Apr 2026 07:36:49 +0800 Subject: [PATCH 4/5] fix(workflow): handle shared strategy in preview mode + align cleanup naming - deno-deploy-reusable.yml: Add explicit 'strategy=shared' block to handle the case where strategy is explicitly set to 'shared' with mode=preview. Previously the preview variable was never set in this case, causing DNS-validation to fail on an empty string. Now shared strategy (whether explicit or auto-switched due to long branch names) is handled consistently. - deno-deploy-preview-cleanup.yml: Align empty-slug handling with deploy workflow. When slugify returns empty, fall back to 'preview' slug (like deploy does) instead of silently skipping. Also add auto-switch to shared strategy when max_branch_len < 6, consistent with deploy behavior. - README.md: Document the auto-fallback-to-shared behavior for repos with very long base names that cannot fit any branch preview prefix. --- .../workflows/deno-deploy-preview-cleanup.yml | 32 +++++++++++++------ .github/workflows/deno-deploy-reusable.yml | 18 ++++++----- README.md | 2 +- 3 files changed, 33 insertions(+), 19 deletions(-) diff --git a/.github/workflows/deno-deploy-preview-cleanup.yml b/.github/workflows/deno-deploy-preview-cleanup.yml index 8ee7e9e..815776d 100644 --- a/.github/workflows/deno-deploy-preview-cleanup.yml +++ b/.github/workflows/deno-deploy-preview-cleanup.yml @@ -83,24 +83,36 @@ jobs: if [ "$strategy" = "branch" ]; then branch_slug="$(slugify "$ref_name")" if [ -z "$branch_slug" ]; then - echo "::notice::Unable to derive branch slug from ref '${ref_name}', skipping cleanup" - echo "skip=true" >> "$GITHUB_OUTPUT" - exit 0 + branch_slug="preview" fi suffix="-${base}-ubq-fi" max_branch_len=$((26 - ${#suffix})) if [ $max_branch_len -lt 6 ]; then + # Project name is too long for branch-based previews; fall back to + # shared preview naming so repos with long names (e.g. notifications-ubq-fi) + # don't break after upgrading without having to explicitly set strategy=shared. + strategy="shared" + fi + if [ "$strategy" = "branch" ]; then + if [ ${#branch_slug} -gt $max_branch_len ]; then + hash=$(printf '%s' "$branch_slug" | sha1sum | cut -c1-4) + head_len=$((max_branch_len - 5)) + branch_slug="${branch_slug:0:${head_len}}-${hash}" + fi + target="${branch_slug}-${base}-ubq-fi" + fi + if [ "$strategy" = "shared" ] && [ -z "$target" ]; then + if [ ${#base} -gt 17 ]; then + hash=$(printf '%s' "$base" | sha1sum | cut -c1-4) + base="${base:0:12}-${hash}" + fi + target="p-${base}-ubq-fi" + fi + if [ "$strategy" = "branch" ] && [ -z "$target" ]; then echo "::error::Cannot build branch preview name for base '${base}' within Deno project length limits" exit 1 fi - if [ ${#branch_slug} -gt $max_branch_len ]; then - hash=$(printf '%s' "$branch_slug" | sha1sum | cut -c1-4) - head_len=$((max_branch_len - 5)) - branch_slug="${branch_slug:0:${head_len}}-${hash}" - fi - - target="${branch_slug}-${base}-ubq-fi" else if [ ${#base} -gt 17 ]; then hash=$(printf '%s' "$base" | sha1sum | cut -c1-4) diff --git a/.github/workflows/deno-deploy-reusable.yml b/.github/workflows/deno-deploy-reusable.yml index 139e40b..df40920 100644 --- a/.github/workflows/deno-deploy-reusable.yml +++ b/.github/workflows/deno-deploy-reusable.yml @@ -527,16 +527,18 @@ jobs: fi preview="${branch_slug}-${base}-ubq-fi" router_host="${branch_slug}-${base}.ubq.fi" - else - # Legacy shared preview project and host - if [ ${#base} -gt 17 ]; then - hash=$(printf '%s' "$base" | sha1sum | cut -c1-4) - base="${base:0:12}-${hash}" - fi - preview="p-${base}-ubq-fi" - router_host="preview-${base}.ubq.fi" fi fi + # Handle shared strategy (whether explicitly set or auto-switched due to long branch name) + if [ "$strategy" = "shared" ] && [ -z "$preview" ]; then + # Legacy shared preview project and host + if [ ${#base} -gt 17 ]; then + hash=$(printf '%s' "$base" | sha1sum | cut -c1-4) + base="${base:0:12}-${hash}" + fi + preview="p-${base}-ubq-fi" + router_host="preview-${base}.ubq.fi" + fi fi if [ ${#preview} -gt 26 ]; then diff --git a/README.md b/README.md index b0c9a95..9002704 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ By default, previews are now generated per branch (instead of collapsing everyth - Branch `feat/widget` for `pay-ubq-fi` resolves to project `feat-widget-pay-ubq-fi` - Router URL becomes `https://feat-widget-pay.ubq.fi` -- If a branch slug is too long, it is trimmed and hash-suffixed to stay within Deno's 26-char project-name limit. +- If a branch slug is too long, it is trimmed and hash-suffixed to stay within Deno's 26-char project-name limit. If even a 6-char prefix cannot fit (i.e. base name is too long for any branch preview), the workflow automatically falls back to shared preview naming so the deployment still succeeds without requiring manual configuration. Optional input on the deploy reusable workflow: From c750009d198ffd13ef00ef14cf189f08501485ce Mon Sep 17 00:00:00 2001 From: Luna Ops Date: Thu, 9 Apr 2026 13:35:52 +0800 Subject: [PATCH 5/5] fix(workflow): validate preview_strategy and guard against production project deletion Address CodeRabbit feedback: - Validate preview_strategy is 'branch' or 'shared' in both deno-deploy-reusable.yml and deno-deploy-preview-cleanup.yml; fail fast on unknown values to prevent the wrong preview project from being deleted. - Add explicit guard in cleanup workflow: refuse to DELETE the production project if target resolves to the base project name (prevents catastrophic deletion). Fixes remaining issues from ubiquity/deno-deploy-workflow#16 CodeRabbit review. --- .github/workflows/deno-deploy-preview-cleanup.yml | 9 +++++++++ .github/workflows/deno-deploy-reusable.yml | 5 ++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deno-deploy-preview-cleanup.yml b/.github/workflows/deno-deploy-preview-cleanup.yml index 815776d..1358739 100644 --- a/.github/workflows/deno-deploy-preview-cleanup.yml +++ b/.github/workflows/deno-deploy-preview-cleanup.yml @@ -78,6 +78,10 @@ jobs: base="${project%-ubq-fi}" target="${PREVIEW_PROJECT:-}" strategy="${PREVIEW_STRATEGY:-branch}" + if [ "$strategy" != "branch" ] && [ "$strategy" != "shared" ]; then + echo "::error::Invalid preview_strategy '${strategy}'. Expected 'branch' or 'shared'." + exit 1 + fi if [ -z "$target" ]; then if [ "$strategy" = "branch" ]; then @@ -126,6 +130,11 @@ jobs: echo "::error::Derived preview project name is invalid: $target" exit 1 fi + # Guard: refuse to delete the production project + if [ "$target" = "$project" ]; then + echo "::error::Refusing to delete production project '${project}' via preview cleanup" + exit 1 + fi echo "skip=false" >> "$GITHUB_OUTPUT" echo "project=$target" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/deno-deploy-reusable.yml b/.github/workflows/deno-deploy-reusable.yml index df40920..07fc049 100644 --- a/.github/workflows/deno-deploy-reusable.yml +++ b/.github/workflows/deno-deploy-reusable.yml @@ -501,8 +501,11 @@ jobs: preview="${PREVIEW_PROJECT:-}" strategy="${PREVIEW_STRATEGY:-branch}" + if [ "$strategy" != "branch" ] && [ "$strategy" != "shared" ]; then + echo "::error::Invalid preview_strategy '${strategy}'. Expected 'branch' or 'shared'." + exit 1 + fi branch_slug="" - router_host="" if [ -z "$preview" ]; then if [ "$strategy" = "branch" ] && [ "$mode" = "preview" ]; then