Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
143 changes: 143 additions & 0 deletions .github/workflows/deno-deploy-preview-cleanup.yml
Original file line number Diff line number Diff line change
@@ -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
Comment thread
coderabbitai[bot] marked this conversation as resolved.
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
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

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}"
91 changes: 72 additions & 19 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,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"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid hard-failing branch previews for longer project names

Because preview_strategy now defaults to branch, this check causes preview deployments to fail for any valid project whose base name is longer than 12 characters (for example notifications-ubq-fi, where max_branch_len becomes 5). Existing repos that do not explicitly opt back to shared will start failing on non-production branches, even though those previews previously deployed successfully.

Useful? React with 👍 / 👎.

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

if [ ${#preview} -gt 26 ]; then
Expand All @@ -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 == ''
Expand Down Expand Up @@ -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:-}"
Expand All @@ -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
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.

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