Skip to content
Closed
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
20 changes: 20 additions & 0 deletions .bulwark.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# .bulwark.yml — this file is yours.
#
# gt created it once and will never modify or delete it again. bulwark owns
# what goes in it; gt only makes sure it exists, because the one setting below
# is load-bearing for the pipeline and silently wrong by default.
#
# `coverage.source` says who produces the coverage bulwark gates on. In this
# pipeline ci-test produces it and uploads it as the `gt-coverage` artifact,
# which the bulwark stage extracts before bulwark runs — so `report` is
# correct. Leaving it unset means `run`, and bulwark would execute your suite a
# second time without saying so.
#
# A stage skipped by ci-preflight simply produces no report, which bulwark
# treats as no coverage for that ecosystem rather than an error.
#
# Everything else bulwark supports — per-language enablement, coverage
# tolerances, patch-coverage opt-outs, toolchain overrides — belongs here too.
# See bulwark's README.
coverage:
source: report
70 changes: 70 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Managed by gt — edit .gt-repo.yaml, then run `gt repo sync`.
#
# Shared policy (cooldown, commit-message prefix, schedule) lives in gt's
# template, not here, so changing it for every repo is a one-line edit in gt.
#
# The 7-day cooldown is the supply-chain guard: by the time a PR exists,
# the upstream release has been in the wild long enough to surface yanks and
# compromised publishers before an auto-merge-eligible PR lands on the default
# branch.
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
# Dependabot's commit message is also its PR title, and PRs are
# squash-merged, so the prefix is what keeps dependency updates inside
# Conventional Commits. `include: scope` appends the dependency scope,
# producing e.g. `ci(deps): bump …`.
commit-message:
prefix: "ci"
include: scope
cooldown:
default-days: 7
open-pull-requests-limit: 25

- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
# Dependabot's commit message is also its PR title, and PRs are
# squash-merged, so the prefix is what keeps dependency updates inside
# Conventional Commits. `include: scope` appends the dependency scope,
# producing e.g. `build(deps): bump …`.
commit-message:
prefix: "build"
include: scope
cooldown:
default-days: 7
open-pull-requests-limit: 25

- package-ecosystem: "npm"
directory: "/page"
schedule:
interval: "weekly"
# Dependabot's commit message is also its PR title, and PRs are
# squash-merged, so the prefix is what keeps dependency updates inside
# Conventional Commits. `include: scope` appends the dependency scope,
# producing e.g. `build(deps): bump …`.
commit-message:
prefix: "build"
include: scope
cooldown:
default-days: 7
open-pull-requests-limit: 25

- package-ecosystem: "npm"
directory: "/worker"
schedule:
interval: "weekly"
# Dependabot's commit message is also its PR title, and PRs are
# squash-merged, so the prefix is what keeps dependency updates inside
# Conventional Commits. `include: scope` appends the dependency scope,
# producing e.g. `build(deps): bump …`.
commit-message:
prefix: "build"
include: scope
cooldown:
default-days: 7
open-pull-requests-limit: 25
51 changes: 17 additions & 34 deletions .github/workflows/deploy.yml → .github/workflows/cd-deploy.yml
Original file line number Diff line number Diff line change
@@ -1,46 +1,29 @@
# Deploys the status Worker (page assets + prober + API) to Cloudflare on push
# to main. Topology changes need NO deploy — topology.yaml is fetched at runtime.
name: deploy
# cd-deploy — this file is yours.
#
# gt created it once and will never modify or delete it again. It is called by
# cd-orchestration.yml, which verifies the tagged tree carries a passing
# gt/validated-tree attestation before this runs.
#
# deploy.yml's own test job is deliberately gone: it re-ran the same
# type-check and suite ci-test already ran on the PR, and the attestation
# proves that exact tree passed rather than proving it again.
#
# The paths-ignore that skipped deploys for topology/docs-only pushes is gone
# too. topology.yaml is still fetched at runtime, so those changes still need
# no deploy — but with tag-triggered delivery, tagging *is* the decision to
# ship, so inferring it from paths no longer earns its place.
name: cd-deploy

on:
push:
branches: [main]
# Skip deploys for data/docs-only changes.
paths-ignore:
- "topology.yaml"
- "docs/**"
- "*.md"
workflow_dispatch:
workflow_call:

permissions:
contents: read
# @wardnet/* resolve from GitHub Packages during install.
packages: read

concurrency:
group: deploy
cancel-in-progress: false

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- name: Install
run: pnpm install --frozen-lockfile
env:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Type-check
run: pnpm type-check
- name: Test
run: pnpm test

deploy:
needs: test
runs-on: ubuntu-latest
environment: production
steps:
Expand Down
104 changes: 104 additions & 0 deletions .github/workflows/cd-orchestration.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# Managed by gt — edit .gt-repo.yaml, then run `gt repo sync`.
#
# gt owns the orchestration; the cd-* stages it calls are yours. It mirrors
# ci-orchestration deliberately, so there is one pipeline idea to learn.
#
# `cd-gate` is not a branch-protection check — nothing merges here. It exists so
# a release has one status meaning "the whole delivery succeeded" rather than
# four job results to read.
name: CD

on:
push:
tags:
- "v*.*.*"
workflow_dispatch:

# One delivery at a time per repository, and never cancelled.
#
# Deliberately not grouped by ref. wardnet-design-system pushes one tag per
# package for a single release, and `changeset publish` ships every pending
# version on the first run — so per-ref grouping would have those runs racing
# to publish the same versions and 409-ing each other. Its release.yml
# serialises for exactly that reason.
#
# The asymmetry decides it: over-serialising costs latency, under-serialising
# corrupts a release. A repo that genuinely needs parallel component delivery
# can make this a setting when one actually does.
concurrency:
group: cd-${{ github.workflow }}
cancel-in-progress: false

permissions:
contents: read

jobs:
# Refuses to ship a tree that never passed the gate. Stronger than trusting
# that CI ran: a re-run would only say the code passes now, on a runner,
# again — this says this exact tree passed the full gate.
verify-attestation:
uses: pedromvgomes/gt/.github/workflows/reusable-attest.yml@v1
with:
require: true
# Must cover everything reusable-attest declares; a caller granting less
# fails the run at parse time.
permissions:
contents: read
statuses: read
pull-requests: read

deploy:
needs: [verify-attestation]
uses: ./.github/workflows/cd-deploy.yml
secrets: inherit
# Delivery writes: a publish stage creates a release, a deploy stage moves
# artifacts. A called workflow can only narrow this, so a stage needing
# less should say so in its own permissions block. A stage needing *more*
# — OIDC id-token, package registries — is a gt change today; no consumer
# has asked for one yet.
permissions:
contents: write
# write, not read: delivery both consumes private @scope dependencies and
# publishes to GitHub Packages — wardnet-design-system's changeset
# publish needs it. A called workflow can only narrow, so a stage that
# merely installs can drop this to read in its own block.
packages: write

verify:
needs: [verify-attestation, deploy]
uses: ./.github/workflows/cd-verify.yml
secrets: inherit
# Delivery writes: a publish stage creates a release, a deploy stage moves
# artifacts. A called workflow can only narrow this, so a stage needing
# less should say so in its own permissions block. A stage needing *more*
# — OIDC id-token, package registries — is a gt change today; no consumer
# has asked for one yet.
permissions:
contents: write
# write, not read: delivery both consumes private @scope dependencies and
# publishes to GitHub Packages — wardnet-design-system's changeset
# publish needs it. A called workflow can only narrow, so a stage that
# merely installs can drop this to read in its own block.
packages: write

cd-gate:
name: cd-gate
needs: [verify-attestation, deploy, verify]
if: always()
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Verify every stage succeeded or was legitimately skipped
env:
RESULTS: ${{ toJSON(needs.*.result) }}
run: |
set -euo pipefail
echo "stage results: ${RESULTS}"
if [[ "${{ contains(needs.*.result, 'failure') }}" == "true" ]]; then
echo "::error::a delivery stage failed"
exit 1
fi
if [[ "${{ contains(needs.*.result, 'cancelled') }}" == "true" ]]; then
echo "::error::a delivery stage was cancelled"
exit 1
fi
101 changes: 101 additions & 0 deletions .github/workflows/cd-verify.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# cd-verify — this file is yours.
#
# gt created it once and will never modify or delete it again. It is called by
# cd-orchestration.yml after cd-deploy.
#
# Confirms the deploy actually took, rather than assuming wrangler exiting zero
# means the site is serving. Two endpoints are checked, and they fail
# differently on purpose:
#
# / — the page assets. Served from the edge.
# /api/status — the worker itself, which reads D1. A broken migration
# returns 500 here while the page still returns 200, so
# checking only the root would miss exactly the failure this
# deploy is most likely to introduce.
#
# Two *consecutive* passes are required. One success can be a lucky hit on an
# edge node still serving the previous version; requiring two spaced apart
# makes a half-propagated or flapping deploy fail rather than squeak through.
# It does not prove global propagation — nothing reachable from one runner
# does — it proves the deploy is not obviously broken.
#
# Requests bypass cache for the reason 6e0e383 records for the probes
# themselves: a cached 200 from before the deploy tells you nothing about it.
name: cd-verify

on:
workflow_call:

permissions:
contents: read

jobs:
smoke:
name: smoke
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Wait for the deploy to serve
env:
BASE: https://status.wardnet.network
ATTEMPTS: "18"
INTERVAL: "10"
REQUIRED_CONSECUTIVE: "2"
run: |
set -uo pipefail

# Returns 0 when both endpoints answer 200 and the API returns JSON.
#
# No `|| echo 000` on the curl calls: -w already prints 000 when the
# connection fails, and the fallback appended a second one, yielding
# a nonsense "000000" in the diagnostics. There is no `set -e` here,
# so a failed curl does not abort the loop.
probe() {
local stamp="$1" code
code=$(curl -sS -o /tmp/page.out -w '%{http_code}' \
--max-time 15 -H 'Cache-Control: no-cache' \
"${BASE}/?cb=${stamp}")
if [[ "$code" != "200" ]]; then
echo " page ${code}"
return 1
fi

code=$(curl -sS -o /tmp/api.out -w '%{http_code}' \
--max-time 15 -H 'Cache-Control: no-cache' \
"${BASE}/api/status?cb=${stamp}")
if [[ "$code" != "200" ]]; then
echo " page 200, api ${code}"
return 1
fi

# A 200 carrying an error body is still a broken deploy.
if ! jq -e . /tmp/api.out >/dev/null 2>&1; then
echo " page 200, api 200 but the body is not JSON"
return 1
fi
echo " page 200, api 200, JSON ok"
return 0
}

consecutive=0
for attempt in $(seq 1 "$ATTEMPTS"); do
echo "attempt ${attempt}/${ATTEMPTS}:"
if probe "$(date +%s)-${attempt}"; then
consecutive=$((consecutive + 1))
if [[ "$consecutive" -ge "$REQUIRED_CONSECUTIVE" ]]; then
echo "✅ ${consecutive} consecutive healthy responses — deploy is serving"
exit 0
fi
else
# Reset rather than decrement: the point is *consecutive* health,
# so a failure between two passes means the deploy is flapping.
consecutive=0
fi
sleep "$INTERVAL"
done

echo "::error::status.wardnet.network did not return ${REQUIRED_CONSECUTIVE} consecutive healthy responses"
echo "Last page body:" >&2; head -c 500 /tmp/page.out >&2 || true
echo "" >&2
echo "Last api body:" >&2; head -c 500 /tmp/api.out >&2 || true
exit 1
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
# Topology changes deliberately skip the deploy workflow (they're runtime data),
# but main's topology.yaml is exactly what every prober fetches — an invalid
# file would pin monitoring to last-known-good forever. This workflow is the
# validation gate deploy.yml's paths-ignore opts out of.
name: validate-topology
# ci-build — this file is yours.
#
# gt created it once and will never modify or delete it again. It is called by
# ci-orchestration.yml and must keep `workflow_call`.
#
# Validates topology.yaml against its schema.
#
name: ci-build

on:
push:
branches: [main]
paths: ["topology.yaml"]
pull_request:
paths: ["topology.yaml"]
workflow_call:

permissions:
contents: read
# @wardnet/* resolve from GitHub Packages during install.
packages: read

jobs:
Expand Down
Loading