Set pnpm to 11 and Node to 26. Verified every action is pinned to its latest release commit SHA with the correct version comment (checkout v7.0.0, setup-go v6.5.0, setup-tinygo v3.0.0, pnpm/action-setup v6.0.9, setup-node v6.4.0, pulumi/actions v7.0.0); Go 1.26 and TinyGo 0.41.1 are the latest. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
145 lines
6.6 KiB
YAML
145 lines
6.6 KiB
YAML
# Continuous deployment for the LibreMail bug-report ingest Worker + infra.
|
|
#
|
|
# MANUAL ONLY: this workflow never runs on push/PR. A maintainer triggers it from
|
|
# the Actions tab (workflow_dispatch), choosing a stack. It builds the TinyGo/Wasm
|
|
# Worker end to end (same setup as ci.yml) and then runs `pulumi up` over the
|
|
# infra/ program to deploy the Worker (with its R2 + Secrets Store + var bindings
|
|
# and Cron Triggers), the R2 bucket, and the Google Cloud DNS record.
|
|
#
|
|
# It is gated to the `production` GitHub Actions environment, so that environment's
|
|
# secrets and any required-reviewer / branch protection rules apply, and to the
|
|
# `main` branch (a guard step fails the run otherwise). Deploying is real and
|
|
# billable, hence manual + environment-gated + maintainer-run-from-main.
|
|
#
|
|
# Supply-chain note: every action (first- and third-party) is pinned to a full
|
|
# commit SHA with a trailing "# vX.Y.Z" comment, matching ci.yml / autoupdate.yml.
|
|
#
|
|
# Secrets/config the maintainer must set BEFORE the first deploy (see infra/README.md):
|
|
# production environment SECRETS (Settings > Environments > production):
|
|
# - PULUMI_ACCESS_TOKEN Pulumi Cloud access token (state backend). For a
|
|
# self-managed backend instead, set the `cloud-url`
|
|
# input + a PULUMI_CONFIG_PASSPHRASE secret.
|
|
# - CLOUDFLARE_API_TOKEN Cloudflare token scoped to Workers Scripts + R2 (+ Cron).
|
|
# - CLOUDFLARE_ACCOUNT_ID Cloudflare account id (also set as stack config).
|
|
# - GOOGLE_CREDENTIALS GCP service-account JSON with Cloud DNS admin on the zone.
|
|
# stack CONFIG (infra/Pulumi.<stack>.yaml — replace every REPLACE_ME_* first):
|
|
# cloudflareAccountId, secretsStoreId, dnsManagedZone, dnsRecordName,
|
|
# dnsRecordTarget, gcp:project (+ optional r2/otel/dns overrides).
|
|
# Cloudflare Secrets Store must already hold the four secret values
|
|
# (bugreport-enc-keyring, bugreport-admin-token, github-token,
|
|
# otel-exporter-otlp-headers) under the configured secretsStoreId.
|
|
|
|
name: CD
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
inputs:
|
|
stack:
|
|
description: 'Pulumi stack to deploy'
|
|
required: true
|
|
default: prod
|
|
type: choice
|
|
options:
|
|
- prod
|
|
- dev
|
|
|
|
# Least privilege: the job only needs to read the repo out; Pulumi auth is via env.
|
|
permissions:
|
|
contents: read
|
|
|
|
# Never run two deploys of the same stack concurrently; do not cancel an in-flight
|
|
# deploy (interrupting `pulumi up` can leave a stack mid-update).
|
|
concurrency:
|
|
group: cd-${{ github.event.inputs.stack }}
|
|
cancel-in-progress: false
|
|
|
|
jobs:
|
|
deploy:
|
|
name: deploy
|
|
runs-on: ubuntu-latest
|
|
# Gate on the production environment so its secrets + protection rules apply.
|
|
environment: production
|
|
steps:
|
|
# Deploys must be cut from main. workflow_dispatch lets a user pick any ref,
|
|
# so fail loudly if this was launched from a non-main branch.
|
|
- name: Guard - deploy only from main
|
|
if: github.ref != 'refs/heads/main'
|
|
run: |
|
|
echo "::error::Deploy must be run from the 'main' branch (got '${{ github.ref }}')."
|
|
exit 1
|
|
|
|
- name: Check out repository
|
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
|
|
# Mirror ci.yml: cache both the root and infra/ module go.sum (infra pulls the
|
|
# heavy Pulumi SDKs) so warm runs restore deps instead of re-downloading.
|
|
- name: Set up Go
|
|
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
|
with:
|
|
go-version: '1.26'
|
|
cache-dependency-path: |
|
|
go.sum
|
|
infra/go.sum
|
|
|
|
# TinyGo builds the Wasm Worker (pnpm run build). Same version as ci.yml.
|
|
- name: Set up TinyGo
|
|
uses: acifani/setup-tinygo@dd8a7075d951a7595b2ef2123ed0ab1af0c13e56 # v3.0.0
|
|
with:
|
|
tinygo-version: '0.41.1'
|
|
|
|
- name: Set up pnpm
|
|
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
|
with:
|
|
version: '11'
|
|
|
|
- name: Set up Node
|
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
with:
|
|
node-version: '26'
|
|
cache: pnpm
|
|
|
|
- name: Install Node dependencies
|
|
run: pnpm install --frozen-lockfile
|
|
|
|
# TEMPORARY (tracking #26; tinygo-org/tinygo#5467): identical to ci.yml. TinyGo
|
|
# 0.41.1 vendors a net/http js/wasm overlay that fails to compile on Go 1.26;
|
|
# apply the exact upstream fix to the installed TinyGo source before building.
|
|
# git apply exits non-zero (failing loudly) if the source has drifted.
|
|
- name: Patch TinyGo net/http (temporary)
|
|
run: |
|
|
patch_file="$PWD/.ci/tinygo-net-roundtrip.patch"
|
|
tinygoroot="$(tinygo env TINYGOROOT)"
|
|
echo "Applying $patch_file to $tinygoroot/src/net/http/roundtrip_js.go"
|
|
git -C "$tinygoroot" apply --verbose "$patch_file" || {
|
|
echo "::error::TinyGo net/http patch did not apply cleanly; TinyGo source may have changed. Update or remove .ci/tinygo-net-roundtrip.patch (see #26)."
|
|
exit 1
|
|
}
|
|
|
|
# Produce build/worker.mjs (ES-module shim) + build/app.wasm. The infra program
|
|
# uploads the shim as the Worker's main module via the workerScriptPath config
|
|
# injected below.
|
|
- name: Build Wasm Worker
|
|
run: pnpm run build
|
|
|
|
# Install the Pulumi CLI and run `pulumi up` over infra/. config-map injects the
|
|
# freshly built artifact path so the WorkersScript uploads the real module
|
|
# (ContentFile) instead of the placeholder. Provider + backend credentials come
|
|
# from the production environment secrets below; nothing secret is committed.
|
|
- name: Pulumi up
|
|
uses: pulumi/actions@8e5e406f4007fca908480587cb9893c07090f58d # v7.0.0
|
|
with:
|
|
command: up
|
|
stack-name: ${{ github.event.inputs.stack }}
|
|
work-dir: infra
|
|
upsert: false
|
|
config-map: '{ "libremail-bug-report-ingest-infra:workerScriptPath": { value: "../build/worker.mjs", secret: false } }'
|
|
env:
|
|
# Pulumi state backend (Pulumi Cloud). For a self-managed backend, drop this,
|
|
# set the action's `cloud-url` input, and add PULUMI_CONFIG_PASSPHRASE.
|
|
PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }}
|
|
# Cloudflare provider (Workers + R2 + Cron Triggers).
|
|
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
|
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
|
# GCP provider (Cloud DNS record).
|
|
GOOGLE_CREDENTIALS: ${{ secrets.GOOGLE_CREDENTIALS }}
|