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
155 changes: 155 additions & 0 deletions .github/workflows/cloudflare-deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
name: Cloudflare deploy

on:
workflow_call:
inputs:
vault:
description: Project vault named webops-prod-<project> containing the Cloudflare account ID and Worker secrets. The caller OP token must also be scoped to webops-prod-shared for the Cloudflare API token.
required: true
type: string
secrets:
description: Multiline KEY=item/field entries resolved from the project vault (or a full op://... reference) and uploaded to the Worker.
required: false
type: string
default: ""
secrets:
OP_SERVICE_ACCOUNT_TOKEN:
description: 1Password service account token supplied by the caller and scoped to webops-prod-shared and the project vault named in vault.
required: true

jobs:
deploy:
name: Deploy to Cloudflare
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout app repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1

- name: Set up bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: 1.3.14

- name: Validate app prerequisites
run: |
set -euo pipefail
if [[ ! -f bun.lock && ! -f bun.lockb ]]; then
echo "::error::Committed bun.lock or bun.lockb is required (bun-only workflow)" >&2
exit 1
fi
if [[ ! -f package.json ]]; then
echo "::error::package.json is required" >&2
exit 1
fi
if ! bun -e 'const p=require("./package.json"); if(!p.scripts?.deploy) process.exit(1)'; then
echo "::error::package.json must define a deploy script that runs Wrangler" >&2
exit 1
fi
if ! bun -e 'const p=require("./package.json"); const d={...p.dependencies,...p.devDependencies}; if(!d?.wrangler) process.exit(1)'; then
echo "::error::wrangler must be a direct, version-pinned dependency so bunx resolves the lockfile install" >&2
exit 1
fi

- name: Install dependencies
run: bun install --frozen-lockfile

- name: Validate deployment inputs
env:
VAULT: ${{ inputs.vault }}
run: |
set -euo pipefail
if [[ -z "$VAULT" ]]; then
echo "::error::vault must name a project-specific vault" >&2
exit 1
fi
if [[ "$VAULT" != webops-prod-* || "$VAULT" == webops-prod-shared ]]; then
echo "::error::vault must name a project vault in the form webops-prod-<project>" >&2
exit 1
fi

- name: Prepare project secret references
id: prepare-project-secret-references
env:
OP_ENV: ${{ inputs.secrets }}
VAULT: ${{ inputs.vault }}
run: |
set -euo pipefail
echo "cloudflare-api-token=op://webops-prod-shared/CLOUDFLARE/CLOUDFLARE_API_TOKEN" >> "$GITHUB_OUTPUT"
echo "cloudflare-account-id=op://${VAULT}/CLOUDFLARE_ACCOUNT_ID/CLOUDFLARE_ACCOUNT_ID" >> "$GITHUB_OUTPUT"
while IFS= read -r line || [[ -n "$line" ]]; do
line="${line//$'\r'/}"
[[ "$line" =~ ^[[:space:]]*$ ]] && continue
[[ "$line" =~ ^[[:space:]]*# ]] && continue
if [[ "$line" != *=* ]]; then
echo "::error::Invalid secrets entry (expected KEY=item/field or op://...): $line" >&2
exit 1
fi
key="${line%%=*}"
key="${key//[[:space:]]/}"
reference="${line#*=}"
if [[ -z "$key" || -z "$reference" ]]; then
echo "::error::Invalid secrets entry (expected KEY=item/field or op://...): $line" >&2
exit 1
fi
case "$key" in
CLOUDFLARE_API_TOKEN|CLOUDFLARE_ACCOUNT_ID)
echo "::error::${key} is managed by the reusable workflow and must not be listed in secrets" >&2
exit 1
;;
esac
if [[ "$reference" == op://* ]]; then
printf '%s=%s\n' "$key" "$reference"
else
printf '%s=op://%s/%s\n' "$key" "$VAULT" "$reference"
fi
done <<< "$OP_ENV" >> "$GITHUB_ENV"

- name: Load secrets from 1Password
uses: 1password/load-secrets-action@3a12b0ab99d9cd590a3e9b5a90ea017210ed9556 # v4.0.1
with:
export-env: true
version: 2.30.3
env:
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
CLOUDFLARE_API_TOKEN: ${{ steps.prepare-project-secret-references.outputs.cloudflare-api-token }}
CLOUDFLARE_ACCOUNT_ID: ${{ steps.prepare-project-secret-references.outputs.cloudflare-account-id }}

- name: Validate Cloudflare credentials
run: |
set -euo pipefail
for var in CLOUDFLARE_API_TOKEN CLOUDFLARE_ACCOUNT_ID; do
if [[ -z "${!var:-}" ]]; then
echo "::error::${var} must resolve from its configured 1Password vault" >&2
exit 1
fi
done

- name: Upload secrets to Cloudflare
env:
OP_ENV: ${{ inputs.secrets }}
run: |
set -euo pipefail
secrets='{}'
count=0
while IFS= read -r line || [[ -n "$line" ]]; do
line="${line//$'\r'/}"
[[ "$line" =~ ^[[:space:]]*$ ]] && continue
[[ "$line" =~ ^[[:space:]]*# ]] && continue
key="${line%%=*}"
key="${key//[[:space:]]/}"
value="${!key:-}"
if [[ -z "$value" ]]; then
echo "::error::Secret ${key} was not loaded into the environment" >&2
exit 1
fi
secrets="$(jq --arg key "$key" --arg value "$value" '. + {($key): $value}' <<< "$secrets")"
((count += 1))
done <<< "$OP_ENV"
if (( count > 0 )); then
printf '%s\n' "$secrets" | bunx wrangler secret bulk
fi

- name: Deploy Worker
run: bun run deploy
46 changes: 45 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Vercel deploy
# Yearn reusable deployment workflows

Reusable GitHub workflow for deploying Vercel projects with secrets resolved
from 1Password by the calling workflow.
Expand Down Expand Up @@ -77,3 +77,47 @@ Consume it from a downstream job with
`${{ needs.deploy.outputs.deployment-url }}`.

See `examples/` for the current Katana APR, yvUSD APR, and fapy-hook shapes.

## Cloudflare Workers

`.github/workflows/cloudflare-deploy.yml` deploys a Bun-based Cloudflare Worker.
Callers must ship a committed `bun.lock`/`bun.lockb`, a `deploy` script, and
`wrangler` as a direct dependency. The workflow installs with
`bun install --frozen-lockfile` (before any secrets load), resolves
`CLOUDFLARE_API_TOKEN` from `webops-prod-shared/CLOUDFLARE` and
`CLOUDFLARE_ACCOUNT_ID` from the project vault, uploads declared Worker secrets
with `wrangler secret bulk`, then runs `bun run deploy`. Do not list either
Cloudflare credential in `secrets`. Named Wrangler environments (`--env`) are
not supported.

```yaml
name: Deploy Worker

on:
push:
branches: [main]
workflow_dispatch:

concurrency:
group: cloudflare-deploy-${{ github.ref }}
cancel-in-progress: false

permissions:
contents: read

jobs:
deploy:
uses: yearn/yearn-gha/.github/workflows/cloudflare-deploy.yml@main
with:
vault: webops-prod-my-worker
secrets: |
DATABASE_URL=my-worker/DATABASE_URL
API_KEY=my-worker/API_KEY
secrets:
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
```

`vault` and `secrets` use the same `KEY=item/field` (or full `op://...`)
syntax as the Vercel workflow. The caller's 1Password token needs read access
to both `webops-prod-shared` and the selected project vault. Callers must set a
concurrency group so parallel deploys do not interleave secret bulk and deploy.
Loading