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
238 changes: 74 additions & 164 deletions .github/workflows/deploy-site.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,10 @@ run-name: Deploy project site ${{ inputs.release_tag || github.ref_name }} ${{ i
on:
push:
branches: [main, master]
paths:
- "apps/site/**"
- "apps/flutter/**"
- "packages/**"
- "scripts/check-release-publication.mjs"
- "scripts/build-demo.mjs"
- "scripts/deploy-site.mjs"
- "scripts/deploy-demo.mjs"
- "scripts/dispatch-site-deployment.mjs"
- "scripts/generate-release-manifest.mjs"
- "scripts/inspect-linux-update.mjs"
- "scripts/read-bounded-response.mjs"
- "scripts/reconcile-release-assets.mjs"
- "scripts/wait-for-release-assets.mjs"
- "package.json"
- "pnpm-lock.yaml"
- ".github/workflows/deploy-site.yml"
workflow_dispatch:
inputs:
release_tag:
description: Published release tag whose immutable source must be deployed.
description: Published release tag whose immutable source must be verified.
required: true
type: string
dispatch_nonce:
Expand All @@ -34,87 +17,19 @@ on:

permissions:
contents: read
id-token: write

concurrency:
group: t4code-net-production
cancel-in-progress: false
group: t4code-com-production-${{ github.event_name }}
cancel-in-progress: ${{ github.event_name == 'push' }}

jobs:
demo:
if: ${{ github.event_name == 'push' }}
verify-production:
if: ${{ (github.event_name == 'push' && github.ref == 'refs/heads/main') || (github.event_name == 'workflow_dispatch' && startsWith(github.ref, 'refs/tags/')) }}
runs-on: ubuntu-24.04
timeout-minutes: 30
environment:
name: production
url: https://t4code.net/demo
steps:
- name: Check out current demo source
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: ${{ github.sha }}
persist-credentials: false

- name: Install pnpm
uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
with:
version: 11.10.0

- name: Install Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 24.13.1

- name: Install Flutter
uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2
with:
flutter-version: 3.44.6
channel: stable
cache: true

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

- name: Authenticate to AWS with GitHub OIDC
uses: aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a # v4
with:
role-to-assume: ${{ vars.AWS_ROLE_ARN }}
aws-region: us-east-1

- name: Check whether the live demo permits the Flutter renderer
id: demo_csp
shell: bash
run: |
set -euo pipefail
headers=$(curl --fail --silent --show-error --head https://t4code.net/demo)
if printf '%s\n' "$headers" \
| tr -d '\r' \
| grep -i '^content-security-policy:' \
| grep -Fq "'wasm-unsafe-eval'"; then
echo "ready=true" >> "$GITHUB_OUTPUT"
else
echo "ready=false" >> "$GITHUB_OUTPUT"
echo "::warning::Flutter demo deployment is deferred until the demo CloudFront CSP permits wasm-unsafe-eval."
fi

- name: Defer Flutter demo until its response policy is active
if: ${{ steps.demo_csp.outputs.ready != 'true' }}
run: echo "Apply infra/site/cloudformation.yml, then rerun this workflow from a main push."

- name: Build and deploy current Flutter demo
if: ${{ steps.demo_csp.outputs.ready == 'true' }}
env:
T4_SITE_BUCKET: ${{ vars.T4_SITE_BUCKET }}
T4_CLOUDFRONT_DISTRIBUTION_ID: ${{ vars.T4_CLOUDFRONT_DISTRIBUTION_ID }}
run: pnpm deploy:demo

deploy:
if: ${{ github.event_name != 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/') }}
runs-on: ubuntu-24.04
timeout-minutes: 50
environment:
name: production
url: https://t4code.net
url: https://t4code.com
steps:
- name: Check out trusted workflow source
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
Expand All @@ -123,122 +38,117 @@ jobs:
fetch-depth: 0
persist-credentials: false

- name: Read trusted workflow source version
id: source
shell: bash
env:
TRUSTED_SHA: ${{ github.sha }}
run: |
set -euo pipefail
version=$(node -p "require('./package.json').version")
printf 'version=%s\ntrusted_sha=%s\n' "$version" "$TRUSTED_SHA" >> "$GITHUB_OUTPUT"

- name: Install pnpm
uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
with:
version: 11.10.0

- name: Install Node.js
- name: Install Node.js for immutable-source checks
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 24.13.1

- name: Confirm the published release from the release workflow
id: published_release
if: ${{ github.event_name == 'workflow_dispatch' }}
env:
RELEASE_VERSION: ${{ steps.source.outputs.version }}
run: node scripts/wait-for-release-assets.mjs --version "$RELEASE_VERSION" --timeout-ms 2400000 --interval-ms 15000

- name: Classify the stable release referenced by an ordinary main push
id: release_state
if: ${{ github.event_name == 'push' }}
id: release_state
env:
GH_TOKEN: ${{ github.token }}
RELEASE_VERSION: ${{ steps.source.outputs.version }}
run: node scripts/check-release-publication.mjs --version "$RELEASE_VERSION" --github-output "$GITHUB_OUTPUT"

- name: Confirm assets for an existing stable release
id: existing_release
if: ${{ github.event_name == 'push' && steps.release_state.outputs.state == 'published' }}
env:
RELEASE_VERSION: ${{ steps.source.outputs.version }}
run: node scripts/wait-for-release-assets.mjs --version "$RELEASE_VERSION" --timeout-ms 15000 --interval-ms 3000
shell: bash
run: |
set -euo pipefail
RELEASE_VERSION=$(node -p "require('./package.json').version")
node scripts/check-release-publication.mjs --version "$RELEASE_VERSION" --github-output "$GITHUB_OUTPUT"

- name: Defer a release-version site update until publication
- name: Record that the ordinary main source is not yet a published release
if: ${{ github.event_name == 'push' && steps.release_state.outputs.state == 'not-published' }}
run: echo "The referenced release is not public yet; the release workflow will deploy this site after publication."
run: echo "The mirrored main push is still expected to deploy this exact source revision."

- name: Resolve immutable deployment source
id: immutable_source
if: ${{ steps.published_release.outcome == 'success' || steps.existing_release.outcome == 'success' }}
shell: bash
env:
EVENT_NAME: ${{ github.event_name }}
GH_TOKEN: ${{ github.token }}
TRUSTED_SHA: ${{ steps.source.outputs.trusted_sha }}
REQUESTED_RELEASE_TAG: ${{ inputs.release_tag }}
TRUSTED_VERSION: ${{ steps.source.outputs.version }}
TRUSTED_SHA: ${{ github.sha }}
run: |
set -euo pipefail
TRUSTED_VERSION=$(node -p "require('./package.json').version")
expected_tag="v${TRUSTED_VERSION}"
release_tag="$expected_tag"
source_sha="$TRUSTED_SHA"

if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then
if [[ "$REQUESTED_RELEASE_TAG" != "$expected_tag" ]]; then
echo "release_tag must be the current release ${expected_tag}" >&2
echo "release deployment requested an unexpected tag" >&2
exit 1
fi
if [[ "$GITHUB_REF" != "refs/tags/${expected_tag}" ]]; then
echo "workflow_dispatch must run from the immutable release tag ${expected_tag}" >&2
echo "release deployment must run from the current immutable tag ${expected_tag}" >&2
exit 1
fi
release_tag="$REQUESTED_RELEASE_TAG"
fi
release_flags=$(gh api "repos/${GITHUB_REPOSITORY}/releases/tags/${release_tag}" --jq '[.draft, .prerelease] | @tsv')
if [[ "$release_flags" != $'false\tfalse' ]]; then
echo "release_tag must name a published, non-prerelease GitHub release" >&2
exit 1
fi
git fetch --force origin "refs/tags/${release_tag}:refs/tags/${release_tag}"
source_sha=$(git rev-parse "${release_tag}^{commit}")
tag_version=$(git show "${source_sha}:package.json" | jq -er '.version')
if [[ "$tag_version" != "$TRUSTED_VERSION" ]]; then
echo "release tag package version ${tag_version} does not match trusted source ${TRUSTED_VERSION}" >&2
exit 1
fi
if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then
release_flags=$(gh api "repos/${GITHUB_REPOSITORY}/releases/tags/${release_tag}" --jq '[.draft, .prerelease] | @tsv')
if [[ "$release_flags" != $'false\tfalse' ]]; then
echo "release_tag must name a published, non-prerelease GitHub release" >&2
exit 1
fi
git fetch --force origin "refs/tags/${expected_tag}:refs/tags/${expected_tag}"
source_sha=$(git rev-parse "${expected_tag}^{commit}")
if [[ "$source_sha" != "$TRUSTED_SHA" ]]; then
echo "release tag moved after this immutable deployment was dispatched" >&2
echo "release tag moved after this deployment was dispatched" >&2
exit 1
fi
elif ! git merge-base --is-ancestor "$source_sha" "$TRUSTED_SHA"; then
echo "release tag source is not reachable from the trusted workflow source" >&2
fi

if [[ ! "$source_sha" =~ ^[0-9a-f]{40}$ ]]; then
echo "deployment source must be an exact lowercase commit SHA" >&2
exit 1
fi
printf 'release_tag=%s\nsource_sha=%s\n' "$release_tag" "$source_sha" >> "$GITHUB_OUTPUT"
git merge-base --is-ancestor "$source_sha" "$TRUSTED_SHA"
git fetch --force origin "refs/heads/main:refs/remotes/origin/main"
git merge-base --is-ancestor "$source_sha" refs/remotes/origin/main
printf 'source_sha=%s\nrelease_tag=%s\nversion=%s\n' "$source_sha" "$release_tag" "$TRUSTED_VERSION" >> "$GITHUB_OUTPUT"

- name: Check out immutable deployment source
if: ${{ steps.published_release.outcome == 'success' || steps.existing_release.outcome == 'success' }}
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: ${{ steps.immutable_source.outputs.source_sha }}
persist-credentials: false

- name: Install pnpm
uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
with:
version: 11.10.0

- name: Install immutable-source Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 24.13.1

- name: Install dependencies
if: ${{ steps.published_release.outcome == 'success' || steps.existing_release.outcome == 'success' }}
run: pnpm install --frozen-lockfile

- name: Authenticate to AWS with GitHub OIDC
if: ${{ steps.published_release.outcome == 'success' || steps.existing_release.outcome == 'success' }}
uses: aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a # v4
with:
role-to-assume: ${{ vars.AWS_ROLE_ARN }}
aws-region: us-east-1
- name: Test deployment verification contracts
run: pnpm test:deploy:site

- name: Build and deploy immutable release site
if: ${{ steps.published_release.outcome == 'success' || steps.existing_release.outcome == 'success' }}
- name: Wait for the immutable release bundle
if: ${{ github.event_name == 'workflow_dispatch' }}
env:
GH_TOKEN: ${{ github.token }}
T4_SITE_BUCKET: ${{ vars.T4_SITE_BUCKET }}
T4_CLOUDFRONT_DISTRIBUTION_ID: ${{ vars.T4_CLOUDFRONT_DISTRIBUTION_ID }}
run: pnpm deploy:site
RELEASE_VERSION: ${{ steps.immutable_source.outputs.version }}
run: node scripts/wait-for-release-assets.mjs --version "$RELEASE_VERSION" --timeout-ms 2400000 --interval-ms 15000

- name: Validate the site build
shell: bash
run: |
set -euo pipefail
version=$(node -p "require('./package.json').version")
node scripts/generate-release-manifest.mjs \
--version "$version" \
--output apps/site/public/releases/latest.json
pnpm build:site

- name: Wait for the mirrored deployment at the public edge
env:
SOURCE_SHA: ${{ steps.immutable_source.outputs.source_sha }}
run: >-
node scripts/verify-site-revision.mjs
--target public
--expected "$SOURCE_SHA"
--timeout-ms 900000
--interval-ms 10000
--request-timeout-ms 5000
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,9 @@ test-results/
/output/playwright/
*.log
.todos/

# Local Terraform runtime artifacts
.terraform/
*.tfplan
*.tfstate
*.tfstate.*
Loading