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
14 changes: 14 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ name: Release
# 3. `publish`: waits for the `npm-publish` environment's approval (one click, the maintainer's
# 2FA), then scripts/publish-ci.ts builds, smoke-tests the packed tarballs, publishes them with
# npm trusted publishing (OIDC, provenance), and creates one `v<version>` tag and GitHub Release.
# 4. `template`: after publishing, template-sync.yml opens or updates the cloudhal-template PR
# (the Deploy to Cloudflare button's repository; CONTRIBUTING.md "Releasing").
# npm's trusted-publisher entry for each package names this file and the `npm-publish`
# environment: renaming either breaks publishing.

Expand Down Expand Up @@ -96,4 +98,16 @@ jobs:
- run: node scripts/publish-ci.ts
env:
GH_TOKEN: ${{ github.token }}

template:
name: cloudhal-template PR for ${{ needs.plan.outputs.version }}
needs: [plan, publish]
if: needs.publish.result == 'success'
permissions:
contents: read
uses: ./.github/workflows/template-sync.yml
with:
version: ${{ needs.plan.outputs.version }}
secrets:
TEMPLATE_SYNC_APP_PRIVATE_KEY: ${{ secrets.TEMPLATE_SYNC_APP_PRIVATE_KEY }}
# crates.io publishing is added once cloudhal-schema reaches 0.1 (see docs/adr/0006).
216 changes: 216 additions & 0 deletions .github/workflows/template-sync.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
name: Template sync

# Keeps cloudhal-dev/cloudhal-template, the repository the Deploy to Cloudflare button copies
# (docs/guides/deploy-button.md), on the latest release (issue #111). release.yml calls this after
# a successful npm publish; to re-sync by hand, run Actions › Template sync › Run workflow with a
# version npm has.
# scripts/template-sync.ts copies src/, public/, astro.config.mjs and tsconfig.json from the blog
# starter `@cloudhal/cli@<version>` ships and moves @cloudhal/* to ^<version>; the lockfile is
# refreshed with every @cloudhal/* at exactly <version>, without running package code; then one PR
# is opened or updated from cloudhal/v<version>, and older cloudhal/v* PRs the App opened are
# closed as superseded. Only PRs from cloudhal-template's own branches count: anyone can open a
# PR from a fork branch of the same name. Before any write token exists, a copy of the synced
# template is installed (npm ci) and deployed dry (`cloudhal deploy --dry-run`, WORKERS_CI=1); the
# result is the PR's `cloudhal/template-check` commit status, and a person merges it. The template
# holds no workflow files of its own: the button copies it with a GitHub App, and whether that
# copy may contain .github/workflows/ is not documented.
# Needs the TEMPLATE_SYNC_APP_CLIENT_ID variable and the TEMPLATE_SYNC_APP_PRIVATE_KEY secret, a
# GitHub App installed on cloudhal-template only (CONTRIBUTING.md "Releasing"). Without them the
# job logs a notice and does nothing. The App token is created only after the npm steps, and only
# the last step sees it.

on:
workflow_call:
inputs:
version:
description: Cloudhal version on npm, x.y.z
type: string
required: true
secrets:
TEMPLATE_SYNC_APP_PRIVATE_KEY:
required: false
workflow_dispatch:
inputs:
version:
description: Cloudhal version on npm, x.y.z
type: string
required: true

permissions: {}

jobs:
sync:
name: Open the cloudhal-template PR
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
# Job level: a workflow-level group could wait on the calling release run forever.
concurrency:
group: template-sync
cancel-in-progress: false
env:
VERSION: ${{ inputs.version }}
TEMPLATE: cloudhal-dev/cloudhal-template
BRANCH: cloudhal/v${{ inputs.version }}
steps:
- id: gate
name: Is the GitHub App set up?
env:
CLIENT_ID: ${{ vars.TEMPLATE_SYNC_APP_CLIENT_ID }}
HAS_KEY: ${{ secrets.TEMPLATE_SYNC_APP_PRIVATE_KEY != '' }}
run: |
if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::The version must be x.y.z, not '$VERSION'"
exit 1
fi
if [ -n "$CLIENT_ID" ] && [ "$HAS_KEY" = "true" ]; then
echo "enabled=true" >> "$GITHUB_OUTPUT"
else
echo "::notice title=cloudhal-template not synced::Set the TEMPLATE_SYNC_APP_CLIENT_ID variable and the TEMPLATE_SYNC_APP_PRIVATE_KEY secret (CONTRIBUTING.md, Releasing)"
echo "enabled=false" >> "$GITHUB_OUTPUT"
fi

- if: steps.gate.outputs.enabled == 'true'
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

# Public, so the job's read-only token is enough; the App token comes later.
- if: steps.gate.outputs.enabled == 'true'
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: cloudhal-dev/cloudhal-template
path: template
persist-credentials: false

- if: steps.gate.outputs.enabled == 'true'
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 24

- name: Copy the starter, move @cloudhal/* to ^${{ inputs.version }}
if: steps.gate.outputs.enabled == 'true'
run: node scripts/template-sync.ts --template template --version "$VERSION" --wait 900 --summary "$RUNNER_TEMP/pr-body.md"

# A plain refresh keeps (or picks) the newest @cloudhal/* in range, not this version: pin it,
# then fail unless the lock holds it everywhere. Retries while npm catches up.
- name: Refresh package-lock.json at ${{ inputs.version }} (no install scripts)
if: steps.gate.outputs.enabled == 'true'
run: node scripts/template-sync.ts --template template --version "$VERSION" --lock --wait 300

- id: diff
name: Anything to change?
if: steps.gate.outputs.enabled == 'true'
working-directory: template
run: |
if [ -z "$(git status --porcelain)" ]; then
echo "::notice title=cloudhal-template is current::main already matches $VERSION"
echo "changed=false" >> "$GITHUB_OUTPUT"
else
git status --short
echo "changed=true" >> "$GITHUB_OUTPUT"
fi

# Package code runs here (install scripts, the CLI): in a copy, so nothing it writes reaches
# the commit, and before any write token exists. A failure still opens the PR, marked failed.
- id: check
name: Install and dry-run deploy a copy (WORKERS_CI=1)
if: steps.diff.outputs.changed == 'true'
continue-on-error: true
run: |
set -euo pipefail
CHECK="$RUNNER_TEMP/template-check"
rsync -a --exclude .git template/ "$CHECK/"
cd "$CHECK"
npm ci --no-audit --no-fund
WORKERS_CI=1 npx --no-install cloudhal deploy --dry-run

- id: app
if: steps.diff.outputs.changed == 'true'
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
with:
client-id: ${{ vars.TEMPLATE_SYNC_APP_CLIENT_ID }}
private-key: ${{ secrets.TEMPLATE_SYNC_APP_PRIVATE_KEY }}
owner: cloudhal-dev
repositories: cloudhal-template
permission-contents: write
permission-pull-requests: write
permission-statuses: write

- name: Push cloudhal/v${{ inputs.version }} and open or update the PR
if: steps.diff.outputs.changed == 'true'
working-directory: template
env:
GH_TOKEN: ${{ steps.app.outputs.token }}
SLUG: ${{ steps.app.outputs.app-slug }}
BODY: ${{ runner.temp }}/pr-body.md
CHECK_OUTCOME: ${{ steps.check.outcome }}
run: |
set -euo pipefail
TITLE="chore: Cloudhal $VERSION and the $VERSION starter theme"
gh auth setup-git
ID=$(gh api "/users/${SLUG}%5Bbot%5D" --jq .id)
git config user.name "${SLUG}[bot]"
git config user.email "${ID}+${SLUG}[bot]@users.noreply.github.com"

# The branch is rebuilt from main on every run, so nobody pushes to it by hand.
BASE=$(git rev-parse HEAD)
git switch -c "$BRANCH"
git add -A
git commit -q -s -m "$TITLE" \
-m "src/, public/, astro.config.mjs and tsconfig.json from the blog starter in @cloudhal/cli@$VERSION; @cloudhal/* at ^$VERSION and a package-lock.json that installs exactly $VERSION." \
-m "Opened by $GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID"
if git fetch -q origin "$BRANCH" 2>/dev/null \
&& [ "$(git rev-parse 'FETCH_HEAD^{tree}')" = "$(git rev-parse 'HEAD^{tree}')" ] \
&& [ "$(git rev-parse 'FETCH_HEAD^')" = "$BASE" ]; then
echo "origin/$BRANCH already has this change on the same main"
SHA=$(git rev-parse FETCH_HEAD)
else
git push -q --force origin "HEAD:refs/heads/$BRANCH"
SHA=$(git rev-parse HEAD)
fi

# REST with owner:branch, not `gh pr list --head`, which matches fork PRs from a branch
# of the same name too; the select drops anything else.
PR=$(gh api "repos/$TEMPLATE/pulls?state=open&head=${TEMPLATE%%/*}:$BRANCH" \
--jq '[.[] | select(.head.repo.full_name == env.TEMPLATE)][0].number // empty')
if [ -n "$PR" ]; then
# REST, not `gh pr edit`: that one also queries projects over GraphQL.
gh api -X PATCH "repos/$TEMPLATE/pulls/$PR" -f title="$TITLE" -F "body=@$BODY" > /dev/null
echo "Updated $GITHUB_SERVER_URL/$TEMPLATE/pull/$PR"
else
URL=$(gh pr create --repo "$TEMPLATE" --base main --head "$BRANCH" --title "$TITLE" --body-file "$BODY")
PR=${URL##*/}
echo "Opened $URL"
fi

if [ "$CHECK_OUTCOME" = "success" ]; then
STATE=success DESC="npm ci and cloudhal deploy --dry-run (WORKERS_CI=1) passed"
else
STATE=failure DESC="npm ci or cloudhal deploy --dry-run (WORKERS_CI=1) failed"
fi
gh api -X POST "repos/$TEMPLATE/statuses/$SHA" -f state="$STATE" \
-f context=cloudhal/template-check -f description="$DESC" \
-f target_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" > /dev/null

# Older cloudhal/v* PRs this App opened from this repository are superseded; newer ones,
# fork PRs and everyone else's are left alone.
gh api --paginate "repos/$TEMPLATE/pulls?state=open&per_page=100" \
--jq '.[] | select(.head.repo.full_name == env.TEMPLATE
and .user.login == "\(env.SLUG)[bot]"
and (.head.ref | startswith("cloudhal/v")))
| "\(.number) \(.head.ref)"' |
while read -r NUM HEAD; do
OLD=${HEAD#cloudhal/v}
if [ "$NUM" = "$PR" ]; then continue; fi
if [ "$(printf '%s\n%s\n' "$OLD" "$VERSION" | sort -V | tail -n 1)" = "$VERSION" ]; then
gh pr close "$NUM" --repo "$TEMPLATE" --delete-branch --comment "Superseded by #$PR (Cloudhal $VERSION)."
fi
done

- name: Fail when the dry-run deploy failed
if: steps.check.outcome == 'failure'
run: |
echo "::error title=cloudhal-template check failed::npm ci or cloudhal deploy --dry-run failed on the synced template; its PR carries a failing cloudhal/template-check status"
exit 1
9 changes: 7 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ Commit the regenerated files; CI fails on drift. Add a valid and an invalid fixt
- **Conventional commits**: `feat(contracts): add Redirect node`, `fix(cli): …`, `docs: …`, `chore: …`.
- **DCO sign-off**: every commit needs `Signed-off-by:` — use `git commit -s`. This certifies the [Developer Certificate of Origin](https://developercertificate.org/); there is no CLA.
- Add a changeset (`pnpm changeset`) when a published package changes behaviour.
- A PR that changes what the renderer gives themes freezes the last released starter in the same PR: copy it under `packages/cli/test/fixtures/theme-v<last release>/` and add that version to `FROZEN` in `packages/cli/test/old-theme-compat.test.ts` (see `theme-v0.5.0/README.md` there), so the release that ships the change is already tested against the theme existing sites have.
- Keep modules small and single-purpose: [`docs/engineering/code-structure.md`](docs/engineering/code-structure.md) sets file/function limits that `pnpm size:check` enforces with a shrink-only baseline.
- CI must be green: lint, size check, typecheck, tests (TS + Rust), contract drift.

Expand All @@ -49,10 +50,14 @@ All publishable packages (`@cloudhal/*` and `create-cloudhal`) share one version
2. **Release PR.** On each push to `main`, the Changesets action opens or updates `chore(release): version packages` with the version bump and changelogs. GitHub does not run CI on PRs opened by Actions; the publish job runs the full checks instead. Merging it is the decision to release.
3. **Approval.** The merge starts the `publish` job, which waits for the `npm-publish` environment's approval (the maintainer, with 2FA). Approve it in the run's page on GitHub.
4. **Publish.** `scripts/publish-ci.ts` checks the build, packs every package and installs the tarballs from scratch with npm (`scripts/smoke-pack.ts`), publishes exactly those tarballs with npm trusted publishing (OIDC, with provenance), waits until npm serves all of them, then creates the `vX.Y.Z` tag and the GitHub Release from the changelogs.
5. **After the release.** Bring the rest along:
- **cloudhal-template** (the Deploy to Cloudflare button): the workflow's `template` job (`.github/workflows/template-sync.yml`) opens `chore: Cloudhal X.Y.Z and the X.Y.Z starter theme` from `cloudhal/vX.Y.Z`, with `src/`, `public/`, `astro.config.mjs` and `tsconfig.json` from the published starter, `@cloudhal/*` at `^X.Y.Z` and a lockfile that installs exactly `X.Y.Z`. Merge it once its `cloudhal/template-check` status is green: the same run installs a copy of the synced template with `npm ci` and runs `cloudhal deploy --dry-run` with `WORKERS_CI=1`. To sync again, run Actions › Template sync › Run workflow. Without the `TEMPLATE_SYNC_APP_CLIENT_ID` variable and the `TEMPLATE_SYNC_APP_PRIVATE_KEY` secret (a GitHub App installed on cloudhal-template only, with Contents, Pull requests and Commit statuses write) the job only logs a notice; Dependabot's `@cloudhal/*` PR on the template is the fallback, and it brings dependencies only, no theme.
- **cloudhal.com**: Dependabot opens the `@cloudhal/*` PR the next morning (06:00 Europe/Istanbul). A patch release merges itself after its check; a minor bump waits for a person.
- **try.cloudhal.com** (`cloudhal-dev/try`): by hand. Bump its exact `@cloudhal/*` pins, then `pnpm install` and `pnpm run deploy`.

**If something fails:** re-run the workflow (Actions › Release › Run workflow). The script skips packages npm already has and an existing tag or release, so a re-run finishes a half-done release. npm's trusted-publisher entry for each package names `release.yml` and the `npm-publish` environment: renaming either breaks publishing.
**If something fails:** use **Re-run failed jobs** on the failed run. The script skips packages npm already has and an existing tag or release, so it finishes a half-done release. A new run (Actions › Release › Run workflow) does not: once npm has every package, `plan` skips `publish`, so a missing tag or GitHub Release stays missing. To redo only the template PR, run Actions › Template sync › Run workflow. npm's trusted-publisher entry for each package names `release.yml` and the `npm-publish` environment: renaming either breaks publishing.

**Turning it off:** the repository variable `RELEASE_ENABLED` gates the workflow. Without it, release by hand: `pnpm changeset version` in a PR, merge, then `pnpm release` from an up-to-date `main` and push one `vX.Y.Z` tag.
**Turning it off:** the repository variable `RELEASE_ENABLED` gates the workflow. Without it, release by hand: `pnpm changeset version` in a PR, merge, then `pnpm release` from an up-to-date `main` and push one `vX.Y.Z` tag. Nothing then opens the cloudhal-template PR: run Actions › Template sync › Run workflow with `X.Y.Z` (it needs the GitHub App, not `RELEASE_ENABLED`), and bring cloudhal.com and try.cloudhal.com along as in step 5.

## Where to start

Expand Down
8 changes: 8 additions & 0 deletions docs/guides/deploy-button.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,14 @@ Start from `npm create cloudhal@latest`, then:
- commit `cloudhal.json` and a root `wrangler.jsonc` that names the site Worker, a `DB` D1 database (placeholder id `00000000-0000-0000-0000-000000000000`) and a `BLOBS` R2 bucket
- remove the `build` script (Workers Builds would run it before `deploy`), keep `"deploy": "cloudhal deploy"`
- commit a lockfile and a `.node-version` of `24`. The CLI runs on Node 24.
- copy cloudhal-template's `.github/dependabot.yml` to get a PR for each release, and keep `src/` in step with the starter yourself: Dependabot moves dependencies only. Keep workflow files (`.github/workflows/`) out of a template: whether the button's copy may contain them is not documented.

## Keeping the template current

- Each Cloudhal release opens a PR on cloudhal-template that moves it to the new version and that version's starter theme (`src/`, `public/`, `astro.config.mjs`, `tsconfig.json`). It is merged once its `cloudhal/template-check` status is green (the sync installs a copy with `npm ci` and runs `cloudhal deploy --dry-run` with `WORKERS_CI=1`), so the button deploys the latest release.
- If your copy has the template's `.github/dependabot.yml`, Dependabot opens a PR in it for each `@cloudhal/*` release. Nothing checks that PR in your copy, and merging it deploys: run `npm ci` and `WORKERS_CI=1 npx cloudhal deploy --dry-run` on its branch first, or add a check workflow of your own.
- Your theme stays as it was when the copy was made: Dependabot moves the dependencies only, and releases keep older themes working. In 0.x a minor version may break, so read the release notes before merging a minor bump.
- Delete `.github/dependabot.yml` to stop the PRs.

## Before 0.3.2: the site Worker shows Studio

Expand Down
Loading
Loading