Skip to content
Merged
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
113 changes: 37 additions & 76 deletions .github/workflows/README.md
Original file line number Diff line number Diff line change
@@ -1,95 +1,56 @@
# CI - Build and Push to ECR
# CI workflows

## Overview
Two workflows manage Cloudflare Workers deployments for this storefront:

The `build-and-push-image.yaml` workflow builds the storefront Docker image and
pushes it to Amazon ECR when changes are merged to the `main` branch via PR.
- **`preview.yml`** — uploads a versioned preview on every PR (and on pushes to `env/**`)
- **`deploy.yml`** — deploys to production on every push to `main`

## When the build runs
## Required secrets

- **Push to `main`** on relevant files:
- `Dockerfile`
- Application code: `actions/`, `apps/`, `components/`, `loaders/`, `routes/`,
`sections/`, `sdk/`, `static/`
- Config: `main.ts`, `dev.ts`, `deno.json`, `fresh.config.ts`, etc.
- **Manual**: via `workflow_dispatch` in the GitHub Actions tab
Both workflows need the following GitHub secrets (Settings → Secrets and variables → Actions):

## Required configuration
| Secret | Description |
| ----------------------- | ---------------------------------------------------------- |
| `CLOUDFLARE_API_TOKEN` | API token with `Workers Scripts:Edit` permission |
| `CLOUDFLARE_ACCOUNT_ID` | Cloudflare account ID that owns the worker |

### 1. Variables (Settings > Secrets and variables > Actions > Variables)
The worker name is set in `wrangler.jsonc` (currently `storefront-tanstack-template`). Rename it in your fork to avoid colliding with other workers on the same Cloudflare account.

| Variable | Description | Example |
| ---------------- | ------------------------------------------------------------------------------------- | -------------- |
| `AWS_ACCOUNT_ID` | AWS account ID for ECR | `123456789012` |
| `AWS_REGION` | ECR region | `sa-east-1` |
| — | ECR repository name is taken from `DECO_SITE_NAME` in the Dockerfile (no need to set) | — |
## Preview (`preview.yml`)

### 2. AWS authentication
Triggers:

**Option A - OIDC (recommended):**
- `pull_request` — opened, synchronize, reopened
- `push` to branches matching `env/**` (e.g. `env/staging`)
- `repository_dispatch` with type `preview-deploy` (for external triggers)

1. Configure OIDC in AWS per
[GitHub docs](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services)
2. Create an IAM role with ECR permissions (`ecr:GetAuthorizationToken`) and
push policy
3. Add secret `ECR_ACCESS_ROLE_ARN` = `arn:aws:iam::ACCOUNT_ID:role/ROLE_NAME`
What it does:

**Option B - Access Keys:**
1. Computes a preview alias:
- `env/staging` → `staging`
- PR #42 → `pr-42`
- any other ref → slug (`[^a-z0-9-]` replaced with `-`)
2. Runs `npm install && npm run build`
3. Runs `npx wrangler versions upload --preview-alias <alias>`
4. Parses the two URLs from wrangler output:
- **Version URL** — immutable, unique per upload
- **Alias URL** — stable, overwritten on each push for the same alias
5. Posts a sticky comment on the PR (header `preview-url`) with both URLs

1. Create an IAM user with ECR access policy
2. In **Settings > Secrets and variables > Actions > Secrets**, add:
- `ECR_CI_AWS_ACCESS_KEY_ID`
- `ECR_CI_AWS_SECRET_ACCESS_KEY`
3. The workflow uses Access Keys by default; for OIDC, comment out the Access
Keys step and uncomment the OIDC step
`wrangler versions upload` does **not** replace the production deployment — it creates an isolated version that only traffic hitting the preview URLs reaches. Production is promoted manually via the Cloudflare dashboard or `wrangler versions deploy`.

### 3. ECR repository
## Deploy (`deploy.yml`)

The repository name is taken from `DECO_SITE_NAME` in the Dockerfile. The
pipeline checks if the repo exists and **creates it automatically on first run**
if not. No need to create it manually.
Trigger: `push` to `main`.

The IAM role/user must have: `ecr:CreateRepository`, `ecr:DescribeRepositories`,
`ecr:SetRepositoryPolicy`, and standard push permissions
(`ecr:GetAuthorizationToken`, `ecr:BatchCheckLayerAvailability`, `ecr:PutImage`,
etc.).
What it does:

### 4. Cross-account policy (optional, no account IDs in repo)
1. Syncs `package-lock.json` if missing/stale (commits it back to the repo as `github-actions[bot]`)
2. Runs `npm ci && npm run build`
3. Runs `npx wrangler deploy --var BUILD_HASH:$(git rev-parse --short HEAD)` — injects the short commit SHA as a runtime var so the worker can expose its build version

To allow **cross-account pull** (and write/Lambda per your policy) **without
putting account IDs in the public repo**, use a variable:
Concurrency group `deploy-main` with `cancel-in-progress: false` — queues deploys instead of cancelling, so a fast sequence of merges never leaves production mid-deploy.

1. In **Settings > Secrets and variables > Actions > Variables**, create the
variable **`ECR_REPOSITORY_POLICY_JSON`**.
2. Paste as value the full ECR repository policy JSON (including ARNs with
account IDs and, if used, the `LambdaECRImageCrossAccountRetrievalPolicy`
condition).
## Custom domain

The workflow applies this policy **only on the first run**, when the ECR
repository is created. On subsequent runs the step is skipped. If the variable
is not set, the step is skipped and no custom policy is applied.

## Resulting image

- **Multi-arch**: linux/amd64 and linux/arm64 (manifest list created by buildx)
- **Semantic tag**: `{registry}/storefront:1.0.0` (patch auto-incremented per
release: 1.0.0 → 1.0.1 → 1.0.2)
- **Latest tag**: `{registry}/storefront:latest`
- **Build arg** `GIT_REVISION`: full commit SHA (for traceability)

## GitHub Release

On each successful build, a release is created in the repo with:

- Tag in the form `v1.0.0`
- Auto-generated release notes
- Reference to the Docker image published to ECR

The next version number is derived from the latest existing tag (patch
increment).

## Monorepo

If the storefront lives in a monorepo (subdirectory), move the workflow to
`.github/workflows/` at the repo root and adjust `paths` and Docker
`context`/`file` to include the `storefront/` prefix.
`wrangler.jsonc` ships with `routes` commented out. To map a custom domain, uncomment and fill in the pattern + `zone_name`, then run a deploy. Until then, the worker is only reachable at `<name>.<subdomain>.workers.dev`.
32 changes: 0 additions & 32 deletions .github/workflows/build-and-push-image.yaml

This file was deleted.

54 changes: 54 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: Deploy

on:
push:
branches: [main]

permissions:
contents: write

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

jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 22

- name: Generate lockfile if missing
if: hashFiles('package-lock.json') == ''
run: npm install --package-lock-only

- name: Restore npm cache
uses: actions/cache@v4
with:
path: ~/.npm
key: npm-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
restore-keys: npm-${{ runner.os }}-

- name: Sync lockfile and install
run: |
npm install --package-lock-only
if ! git diff --quiet package-lock.json 2>/dev/null; then
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add package-lock.json
git commit -m "chore: sync package-lock.json"
git push || echo "Lockfile push failed (remote ahead); proceeding with deploy"
fi
npm ci

- name: Build
run: npm run build

- name: Deploy to Cloudflare Workers
run: npx wrangler deploy --var BUILD_HASH:$(git rev-parse --short HEAD)
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
110 changes: 110 additions & 0 deletions .github/workflows/preview.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
name: Preview

on:
repository_dispatch:
types: [preview-deploy]
pull_request:
types: [opened, synchronize, reopened]
push:
branches: ['env/**']

permissions:
contents: write
pull-requests: write
statuses: write

concurrency:
group: preview-${{ github.event.client_payload.ref || github.head_ref || github.ref_name }}
cancel-in-progress: true

jobs:
preview:
runs-on: ubuntu-latest
env:
HAS_CF_SECRETS: ${{ secrets.CLOUDFLARE_API_TOKEN != '' && secrets.CLOUDFLARE_ACCOUNT_ID != '' }}
steps:
- name: Resolve ref
id: resolve
run: |
if [ "${{ github.event_name }}" = "repository_dispatch" ]; then
echo "ref=${{ github.event.client_payload.ref }}" >> "$GITHUB_OUTPUT"
else
echo "ref=${{ github.head_ref || github.ref_name }}" >> "$GITHUB_OUTPUT"
fi

- uses: actions/checkout@v4
with:
ref: ${{ steps.resolve.outputs.ref }}

- uses: actions/setup-node@v4
with:
node-version: 22

- name: Compute preview alias
id: alias
run: |
REF="${{ steps.resolve.outputs.ref }}"
if echo "$REF" | grep -q '^env/'; then
ALIAS=$(echo "$REF" | sed 's|^env/||')
elif [ "${{ github.event_name }}" = "pull_request" ]; then
ALIAS="pr-${{ github.event.pull_request.number }}"
else
ALIAS=$(echo "$REF" | sed 's|[^a-z0-9-]|-|g')
fi
echo "alias=$ALIAS" >> "$GITHUB_OUTPUT"

- name: Install dependencies
run: npm install

- name: Build
run: npm run build

- name: Upload preview version
id: deploy
if: env.HAS_CF_SECRETS == 'true'
run: |
set +e
OUTPUT=$(npx wrangler versions upload --preview-alias ${{ steps.alias.outputs.alias }} 2>&1)
EXIT_CODE=$?
set -e
echo "$OUTPUT"
if [ $EXIT_CODE -ne 0 ]; then
echo "::error::wrangler versions upload failed with exit code $EXIT_CODE"
exit $EXIT_CODE
fi
PREVIEW_URL=$(echo "$OUTPUT" | grep 'Version Preview URL:' | sed 's/.*Version Preview URL: //')
ALIAS_URL=$(echo "$OUTPUT" | grep 'Version Preview Alias URL:' | sed 's/.*Version Preview Alias URL: //')
echo "preview_url=${PREVIEW_URL}" >> "$GITHUB_OUTPUT"
echo "alias_url=${ALIAS_URL}" >> "$GITHUB_OUTPUT"
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}

- name: Comment preview URL on PR
if: github.event_name == 'pull_request' && env.HAS_CF_SECRETS == 'true'
uses: marocchino/sticky-pull-request-comment@v2
with:
header: preview-url
message: |
### Preview deployed

| | URL |
|---|---|
| **Version** | ${{ steps.deploy.outputs.preview_url }} |
| **Alias** | ${{ steps.deploy.outputs.alias_url }} |

- name: Comment missing-secrets notice on PR
if: github.event_name == 'pull_request' && env.HAS_CF_SECRETS != 'true'
uses: marocchino/sticky-pull-request-comment@v2
with:
header: preview-url
message: |
### Preview skipped

Cloudflare secrets are not configured for this repo. Previews are disabled until an admin sets them.

**To enable:** Settings → Secrets and variables → Actions, add:
- `CLOUDFLARE_API_TOKEN` — token with `Workers Scripts:Edit` permission
- `CLOUDFLARE_ACCOUNT_ID` — target Cloudflare account ID

The build itself passed, so this PR's code is still validated.
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ Open `http://localhost:5173`.
| `npm run deploy` | `npm run build` then `wrangler deploy` |
| `npm run typecheck` | `tsc --noEmit` |
| `npm run format` / `format:check` | Prettier on `src/**/*.{ts,tsx}` |
| `npm run knip` | Find unused exports/files |
| `npm run knip` / `knip:fix` | Find / auto-fix unused exports and files |
| `npm run tailwind:lint` / `tailwind:fix` | Lint/auto-fix Tailwind class usage |
| `npm run generate:*` | Re-run a single codegen step (blocks, schema, sections, loaders, routes, invoke) |

Expand Down Expand Up @@ -193,6 +193,15 @@ Override per-route in `src/cache-config.ts`.

Cloudflare Workers via Wrangler. Configuration is in `wrangler.jsonc` (entry: `src/worker-entry.ts`).

Two GitHub Actions workflows handle CI/CD automatically (see [`.github/workflows/README.md`](./.github/workflows/README.md)):

- **`preview.yml`** — on every PR, uploads a versioned preview via `wrangler versions upload --preview-alias` and posts the URL as a sticky comment. Skips gracefully if Cloudflare secrets aren't configured.
- **`deploy.yml`** — on push to `main`, runs `wrangler deploy` with `BUILD_HASH` injected.

Required repo secrets: `CLOUDFLARE_API_TOKEN`, `CLOUDFLARE_ACCOUNT_ID`.

To deploy manually from your machine:

```sh
npm run deploy
```
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"format": "prettier --write \"src/**/*.{ts,tsx}\"",
"format:check": "prettier --check \"src/**/*.{ts,tsx}\"",
"knip": "knip",
"knip:fix": "knip --fix",
"clean": "rm -rf node_modules .cache dist .wrangler/state node_modules/.vite && npm install",
"tailwind:lint": "tsx scripts/tailwind-lint.ts",
"tailwind:fix": "tsx scripts/tailwind-lint.ts --fix"
Expand Down
2 changes: 1 addition & 1 deletion wrangler.jsonc
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "storefront-tanstack-tanstack",
"name": "storefront-tanstack-template",
"compatibility_date": "2026-02-14",
"compatibility_flags": ["nodejs_compat", "no_handle_cross_request_promise_resolution"],
"main": "./src/worker-entry.ts",
Expand Down
Loading