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
19 changes: 19 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
version: 2
updates:
# Keep the third-party actions pinned inside this repo's composite actions and reusable
# workflows up to date. Dependabot opens bump PRs (preserving SHA pins) when new versions ship.
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
commit-message:
prefix: "chore(actions)"
groups:
actions:
patterns:
- "*"

# Consumer repos should add the same `github-actions` ecosystem block so Dependabot tracks their
# `aragon/github-templates/...@<sha>` pins and auto-opens PRs to adopt new releases. That is how a
# security patch here propagates quickly while every consumer stays SHA-pinned.
71 changes: 71 additions & 0 deletions .github/workflows/_selftest.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Smoke test for the composite actions in this repo. Manual-dispatch only. Exercises the leaf
# actions that need no secrets — proving they load and produce outputs — via relative ./steps/*
# paths (valid here because this workflow runs in-repo, not via workflow_call).

name: Self-test (steps)

on:
workflow_dispatch:

permissions:
contents: read

jobs:
steps:
runs-on: ubuntu-latest
steps:
- name: Setup (install disabled — no package in this repo)
uses: ./steps/setup
with:
install: "false"

- name: extract-slack-ts
id: ets
uses: ./steps/extract-slack-ts
with:
body: "Some PR body\n<!-- slack_ts: 1700000000.123456 -->"

- name: assert extract-slack-ts
env:
TS: ${{ steps.ets.outputs.ts }}
run: |
set -euo pipefail
[ "$TS" = "1700000000.123456" ] || { echo "::error::extract-slack-ts failed: '$TS'"; exit 1; }

- name: build-release-notes
id: notes
uses: ./steps/build-release-notes
with:
changes: "- a change"
slack-ts: "1700000000.123456"

- name: assert build-release-notes
env:
NOTES_PATH: ${{ steps.notes.outputs.path }}
run: |
set -euo pipefail
grep -q "slack_ts: 1700000000.123456" "$NOTES_PATH" || { echo "::error::notes missing marker"; exit 1; }

- name: make a fake playwright report
run: |
mkdir -p e2e/test-results
echo '{"stats":{"expected":5,"flaky":1,"unexpected":0,"skipped":0}}' > e2e/test-results/results.json

- name: parse-playwright-results
id: pw
uses: ./steps/parse-playwright-results
with:
report-path: e2e/test-results/results.json
step-outcome: success

- name: assert parse-playwright-results
env:
RESULT: ${{ steps.pw.outputs.result }}
SUMMARY: ${{ steps.pw.outputs.summary }}
run: |
set -euo pipefail
[ "$RESULT" = "flaky" ] || { echo "::error::expected flaky, got '$RESULT'"; exit 1; }
echo "summary: $SUMMARY"

- name: generate-release-summary (git log of this repo)
uses: ./steps/generate-release-summary
126 changes: 126 additions & 0 deletions .github/workflows/deploy-docker.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# Reusable build-on-server Docker deploy over SSH. Generic transport + environment gate; the repo
# supplies its own remote deploy command (e.g. scripts/app-deploy.sh) because that orchestration is
# service-specific. SSH credentials are resolved from 1Password paths only inside this workflow.
#
# SELF-REFERENCE NOTE: `aragon/github-templates/steps/*@main` — see e2e.yml header.

name: Deploy (Docker over SSH)

on:
workflow_call:
inputs:
env:
description: "Target environment (drives the GitHub environment gate)"
required: true
type: string
ref:
description: "Branch, tag or SHA to deploy"
required: false
type: string
remote-path:
description: "Directory on the server to sync the repo into"
required: false
type: string
default: "~/app"
deploy-command:
description: "Command executed on the server, inside remote-path, after sync"
required: true
type: string
op-ssh-key-path:
description: "1Password path to the SSH private key"
required: true
type: string
op-ssh-user-path:
description: "1Password path to the SSH user"
required: true
type: string
op-ssh-host-path:
description: "1Password path to the SSH host"
required: true
type: string
secrets:
OP_SERVICE_ACCOUNT_TOKEN:
description: "1Password service account token scoped to the caller repo's vault"
required: true

permissions:
contents: read

concurrency:
group: deploy-${{ github.repository }}-${{ inputs.env }}
cancel-in-progress: false

jobs:
deploy:
runs-on: ubuntu-latest
environment: ${{ inputs.env }}
steps:
- name: Validate 1Password inputs
env:
PATHS: |
${{ inputs.op-ssh-key-path }}
${{ inputs.op-ssh-user-path }}
${{ inputs.op-ssh-host-path }}
run: |
set -euo pipefail
while IFS= read -r p; do
[ -z "${p// }" ] && continue
if ! [[ "$p" =~ ^op://[A-Za-z0-9\ ._-]+/[A-Za-z0-9\ ._-]+/[A-Za-z0-9\ ._-]+$ ]]; then
echo "::error::Invalid op:// path: $p" >&2
exit 1
fi
done <<< "$PATHS"

- name: Checkout
uses: actions/checkout@9f698171ed81b15d1823a05fc7211befd50c8ae0 # v6.0.3
with:
ref: ${{ inputs.ref }}
fetch-depth: 1

- name: Load SSH credentials
id: secrets
uses: 1password/load-secrets-action@860a5d856924d259f6dcc493575a0b1bb0ae2dde # v4.0.1
with:
export-env: false
env:
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
SSH_KEY: ${{ inputs.op-ssh-key-path }}
SSH_USER: ${{ inputs.op-ssh-user-path }}
SSH_HOST: ${{ inputs.op-ssh-host-path }}

- name: Configure SSH
env:
SSH_KEY: ${{ steps.secrets.outputs.SSH_KEY }}
SSH_HOST: ${{ steps.secrets.outputs.SSH_HOST }}
run: |
set -euo pipefail
mkdir -p ~/.ssh
printf '%s\n' "$SSH_KEY" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
ssh-keyscan -H "$SSH_HOST" >> ~/.ssh/known_hosts 2>/dev/null

- name: Sync repo to server
env:
SSH_USER: ${{ steps.secrets.outputs.SSH_USER }}
SSH_HOST: ${{ steps.secrets.outputs.SSH_HOST }}
REMOTE_PATH: ${{ inputs.remote-path }}
run: |
set -euo pipefail
ssh -i ~/.ssh/deploy_key "${SSH_USER}@${SSH_HOST}" "mkdir -p ${REMOTE_PATH}"
rsync -az --delete --exclude '.git' -e "ssh -i ~/.ssh/deploy_key" \
./ "${SSH_USER}@${SSH_HOST}:${REMOTE_PATH}/"

- name: Run remote deploy command
env:
SSH_USER: ${{ steps.secrets.outputs.SSH_USER }}
SSH_HOST: ${{ steps.secrets.outputs.SSH_HOST }}
REMOTE_PATH: ${{ inputs.remote-path }}
DEPLOY_COMMAND: ${{ inputs.deploy-command }}
run: |
set -euo pipefail
ssh -i ~/.ssh/deploy_key "${SSH_USER}@${SSH_HOST}" \
"cd ${REMOTE_PATH} && ${DEPLOY_COMMAND}"

- name: Cleanup SSH key
if: always()
run: rm -f ~/.ssh/deploy_key
129 changes: 129 additions & 0 deletions .github/workflows/deploy-vercel.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# Reusable Vercel deploy. The broad-permission VERCEL_TOKEN is resolved from a 1Password path
# ONLY inside this workflow and exposed solely as an env var to the Vercel CLI — no consumer
# workflow ever references it. This is how the release system "restricts access to Vercel":
# the token has exactly one place it can be read, behind the caller's protected environment.
#
# SELF-REFERENCE NOTE: `aragon/github-templates/steps/*@main` — see e2e.yml header.

name: Deploy (Vercel)

on:
workflow_call:
inputs:
env:
description: "Target environment (used for the GitHub environment gate + Vercel prod flag)"
required: true
type: string
ref:
description: "Branch, tag or SHA to build/deploy"
required: false
type: string
domain:
description: "If set, alias the deployment URL to this domain"
required: false
type: string
default: ""
vercel-scope:
description: "Vercel scope/org (e.g. aragon-app)"
required: true
type: string
op-vercel-token-path:
description: "1Password path to the Vercel token (op://vault/item/field)"
required: true
type: string
op-env-vault:
description: "Optional 1Password vault to bulk-load environment secrets into .env before build"
required: false
type: string
default: ""
setup-command:
description: "Optional command run before the Vercel build (e.g. 'pnpm run setup production')"
required: false
type: string
default: ""
prod:
description: "Force a production build/deploy. Defaults to true when env == 'production'."
required: false
type: boolean
default: false
secrets:
OP_SERVICE_ACCOUNT_TOKEN:
description: "1Password service account token scoped to the caller repo's vault"
required: true
outputs:
deploymentUrl:
description: "The URL of the Vercel deployment"
value: ${{ jobs.deploy.outputs.deploymentUrl }}

permissions:
contents: read

jobs:
deploy:
runs-on: ubuntu-latest
environment: ${{ inputs.env }}
outputs:
deploymentUrl: ${{ steps.deploy.outputs.deploymentUrl }}
env:
VERCEL_SCOPE: ${{ inputs.vercel-scope }}
ENABLE_EXPERIMENTAL_COREPACK: "1"
PROD: ${{ (inputs.prod || inputs.env == 'production') && 'true' || 'false' }}
steps:
- name: Validate 1Password input
env:
OP_VERCEL_TOKEN_PATH: ${{ inputs.op-vercel-token-path }}
run: |
set -euo pipefail
if ! [[ "$OP_VERCEL_TOKEN_PATH" =~ ^op://[A-Za-z0-9\ ._-]+/[A-Za-z0-9\ ._-]+/[A-Za-z0-9\ ._-]+$ ]]; then
echo "::error::op-vercel-token-path is not a valid op:// path" >&2
exit 1
fi

- name: Setup
uses: aragon/github-templates/steps/setup@main
with:
ref: ${{ inputs.ref }}

- name: Resolve Vercel token
id: vercel-secret
uses: 1password/load-secrets-action@860a5d856924d259f6dcc493575a0b1bb0ae2dde # v4.0.1
with:
export-env: false
env:
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
VERCEL_TOKEN: ${{ inputs.op-vercel-token-path }}

- name: Run setup command
if: inputs.setup-command != ''
run: ${{ inputs.setup-command }}

- name: Load environment secrets into .env
if: inputs.op-env-vault != ''
uses: aragon/github-templates/steps/credential-retrieval@main
with:
op-token: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
op-vault: ${{ inputs.op-env-vault }}
secret-filepath: ".env"
mode: alltofile

- name: Build & deploy to Vercel
id: deploy
env:
VERCEL_TOKEN: ${{ steps.vercel-secret.outputs.VERCEL_TOKEN }}
run: |
set -euo pipefail
PROD_ARG=""
if [ "$PROD" = "true" ]; then PROD_ARG="--prod"; fi
pnpm vercel build $PROD_ARG --yes --scope "$VERCEL_SCOPE"
SKIP_DOMAIN=""
if [ "$PROD" = "true" ]; then SKIP_DOMAIN="--skip-domain"; fi
URL="$(pnpm vercel deploy --prebuilt $PROD_ARG $SKIP_DOMAIN --yes --scope "$VERCEL_SCOPE")"
echo "deploymentUrl=$URL" >> "$GITHUB_OUTPUT"

- name: Alias deployment to domain
if: inputs.domain != ''
env:
VERCEL_TOKEN: ${{ steps.vercel-secret.outputs.VERCEL_TOKEN }}
DEPLOYMENT_URL: ${{ steps.deploy.outputs.deploymentUrl }}
DOMAIN: ${{ inputs.domain }}
run: pnpm vercel alias "$DEPLOYMENT_URL" "$DOMAIN" --scope "$VERCEL_SCOPE"
Loading