Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 164 additions & 0 deletions .github/workflows/deno-deploy-preview-cleanup.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
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 [ "$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
Comment thread
coderabbitai[bot] marked this conversation as resolved.
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
# 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
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
Comment thread
coderabbitai[bot] marked this conversation as resolved.
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
# 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"

Comment thread
coderabbitai[bot] marked this conversation as resolved.
- 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}"
106 changes: 85 additions & 21 deletions .github/workflows/deno-deploy-reusable.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,15 @@ on:
type: string
default: ""
preview_project:
description: Optional preview project name (defaults to "preview-<project>")
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
Expand Down Expand Up @@ -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 }}
Expand Down Expand Up @@ -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"
Expand All @@ -471,16 +483,65 @@ 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}"
if [ "$strategy" != "branch" ] && [ "$strategy" != "shared" ]; then
echo "::error::Invalid preview_strategy '${strategy}'. Expected 'branch' or 'shared'."
exit 1
fi
branch_slug=""

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
# 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
preview="${branch_slug}-${base}-ubq-fi"
router_host="${branch_slug}-${base}.ubq.fi"
fi
fi
Comment thread
coderabbitai[bot] marked this conversation as resolved.
# 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
preview="p-${base}-ubq-fi"
fi

if [ ${#preview} -gt 26 ]; then
Expand All @@ -492,22 +553,21 @@ 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 "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 == ''
Expand Down Expand Up @@ -966,6 +1026,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:-}"
Expand All @@ -981,7 +1042,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
Expand Down
36 changes: 35 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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-<subdomain>.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. 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:

- `preview_strategy: branch` (default) — branch-scoped preview project/domain
- `preview_strategy: shared` — legacy shared preview project/domain
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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: <subdomain>-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).
Expand Down
Loading