WIP: ci: check, preview and deploy the site on CircleCI #10

Draft
drcrow wants to merge 15 commits from drcrow/Site:ci/circleci into main
Member

Why

Nothing checks the site before it deploys, and every merge deploys, even a README change. This moves the checks, preview and deploy to CircleCI, so a broken link or page fails the PR instead of the live site.

What changed

  • ci: .circleci/config.yml has one small setup job per event (pull request, push to main, manual API run). Each one looks at which files changed and continues with .circleci/shared.yml plus that event's file in .circleci/events/. Changes under public/, wrangler.jsonc or the package files run lint, checks and a preview; anything else runs lint only.
  • ci: the shared setup, fork handling and gate come from one CircleCI orb, magicbeandev/ci, used by all three repos (source in a new MagicBeanDev/orb repo). This repo keeps only its own jobs.
  • test: mise run test uses standard tools: lychee for links, html-validate, jq on the Matrix .well-known file, and wrangler deploy --dry-run. Results show up as CircleCI test results.
  • ci: preview: each PR's public/ is saved as a CircleCI artifact, and one PR comment links to it, updated on every push. No Cloudflare token is used on PRs. A Cloudflare preview is still available as a manual run.
  • ci: a push to main deploys with the production context. Every run ends in ci/circleci: gate, the one check branch protection needs.
  • ci: fork PRs: a maintainer adds the ci: approved label (Forgejo only lets people with write access do that). A separate CircleCI project then runs the same checks on that exact commit with no secrets, and posts the gate status. The label stays on, so new pushes keep running. If a fork PR goes 14 days without activity, the label is removed automatically.
  • build(dev): mise.toml pins the tools and has lint, test and validate tasks. Also adds pre-commit hooks (Conventional Commit messages included), a devcontainer, and a Chunk sidecar config. public/index.html gets <!DOCTYPE html>.
  • PR title and description checks stay on Forgejo. The Forgejo workflows stay until CircleCI has been green for a while.

Testing

Tested: circleci config validate on every event's merged config with the orb inlined; the orb's conftest policies (16 pass); mise run lint and mise run test in a clean container and on a fresh Chunk sidecar.
Not tested: a live CircleCI run (it starts once this is on main and the orb is published), a real deploy (needs the Cloudflare token in CircleCI), and the fork flow end to end (needs the org webhook).

Feedback very welcome, especially on whether an artifact link works as a preview for you, or whether you'd rather keep a Cloudflare preview on every PR.

## Why Nothing checks the site before it deploys, and every merge deploys, even a README change. This moves the checks, preview and deploy to CircleCI, so a broken link or page fails the PR instead of the live site. ## What changed - ci: `.circleci/config.yml` has one small setup job per event (pull request, push to main, manual API run). Each one looks at which files changed and continues with `.circleci/shared.yml` plus that event's file in `.circleci/events/`. Changes under `public/`, `wrangler.jsonc` or the package files run lint, checks and a preview; anything else runs lint only. - ci: the shared setup, fork handling and gate come from one CircleCI orb, `magicbeandev/ci`, used by all three repos (source in a new `MagicBeanDev/orb` repo). This repo keeps only its own jobs. - test: `mise run test` uses standard tools: `lychee` for links, `html-validate`, `jq` on the Matrix `.well-known` file, and `wrangler deploy --dry-run`. Results show up as CircleCI test results. - ci: preview: each PR's `public/` is saved as a CircleCI artifact, and one PR comment links to it, updated on every push. No Cloudflare token is used on PRs. A Cloudflare preview is still available as a manual run. - ci: a push to main deploys with the production context. Every run ends in `ci/circleci: gate`, the one check branch protection needs. - ci: fork PRs: a maintainer adds the `ci: approved` label (Forgejo only lets people with write access do that). A separate CircleCI project then runs the same checks on that exact commit with no secrets, and posts the gate status. The label stays on, so new pushes keep running. If a fork PR goes 14 days without activity, the label is removed automatically. - build(dev): `mise.toml` pins the tools and has `lint`, `test` and `validate` tasks. Also adds pre-commit hooks (Conventional Commit messages included), a devcontainer, and a Chunk sidecar config. `public/index.html` gets `<!DOCTYPE html>`. - PR title and description checks stay on Forgejo. The Forgejo workflows stay until CircleCI has been green for a while. ## Testing Tested: `circleci config validate` on every event's merged config with the orb inlined; the orb's conftest policies (16 pass); `mise run lint` and `mise run test` in a clean container and on a fresh Chunk sidecar. Not tested: a live CircleCI run (it starts once this is on main and the orb is published), a real deploy (needs the Cloudflare token in CircleCI), and the fork flow end to end (needs the org webhook). Feedback very welcome, especially on whether an artifact link works as a preview for you, or whether you'd rather keep a Cloudflare preview on every PR.
drcrow force-pushed ci/circleci from a4e6d3085d
Some checks failed
Preview / Preview on Cloudflare (pull_request) Has been cancelled
to a41c7991bd
Some checks failed
Preview / Preview on Cloudflare (pull_request) Has been cancelled
2026-11-14 14:14:58 +00:00
Compare
drcrow force-pushed ci/circleci from a41c7991bd
Some checks failed
Preview / Preview on Cloudflare (pull_request) Has been cancelled
to d07dd27288
Some checks failed
Preview / Preview on Cloudflare (pull_request) Has been cancelled
2026-11-14 14:37:28 +00:00
Compare
mooki left a comment

Looked over what runs when, at d07dd272 (read at a41c7991; the push since then adds diagrams to docs/ci.md and changes nothing else). Seven things in the files, then points that depend on the orb or on settings I cannot see.

1. The manual dry run carries the production Cloudflare context onto any branch and runs package scripts there. .circleci/events/manual.yml:21-37: manual-release-dry-run gives deploy the cloudflare-prod context with no branch condition. The deploy job (shared.yml:203-217) checks out the selected branch and runs install-node-deps before the dry-run guard, and that command only passes --ignore-scripts when asked (shared.yml:46-54); deploy does not ask. So a dry run on a branch runs that branch's npm lifecycle scripts with the production token in the environment. A dry run needs neither the context nor the scripts: a separate job without the context, or ignore_scripts: true plus no context, closes it.

2. Lint fails on mise.toml as committed. The file has no final newline (it ends in """), .pre-commit-config.yaml:9 enables end-of-file-fixer, and lint runs pre-commit run --all-files (shared.yml:73-75, and mise run lint). That hook rewrites the file and exits non-zero. It is the only file in the diff without a final newline.

3. The preview's link rewrite never matches. shared.yml:116-117: in s/(href|src)=(\"|')\/([^/\"' ][^\"']*)\1/\1=\2\3\1/g the closing quote is written as \1, which is href or src; the quote is \2. On <link href="/style.css"><img src="/img/logo.png"> the command changes nothing; with \2 for the closing quote it gives href="style.css" and src="img/logo.png". public/index.html has five root-relative references (the three icons, logo.webp, longshot.webp), which in the artifact preview point at the artifact host's root.

4. The manual Cloudflare preview keeps files the pull request deleted. shared.yml:155-169 resets to main and then extracts the pull request's public/ over main's. A file removed or renamed in the pull request stays in the upload. Clearing public/ before the extract fixes it.

5. The manual preview looks at the first 50 open pull requests only. shared.yml:176-183 asks for pulls?state=open&limit=50 and takes no further pages; a pull request beyond that is treated as absent and the job ends "No PR number; skipping upload" with success.

6. Docs changes are "full". mapping.conf:12-14 maps docs/, CONTRIBUTING.md and AGENTS.md to full, so they run tests and a preview on a pull request and deploy on main (events/main.yml:3-20). The description says anything outside public/, wrangler.jsonc and the package files runs lint only; that holds for README.md, which has no rule.

7. The fork workflow's condition is always true. .circleci/events/fork.yml:4-5 has when: greater: [<< pipeline.parameters.fork_pr >>, 0]. greater is not one of CircleCI's logic statements, and the compiler does not reject it: an unknown operator counts as true. A small config with exactly that condition compiles as valid and keeps the workflow with fork_pr at 0 as well as at 7; with when: pipeline.parameters.fork_pr > 0 it is dropped at 0. So the workflow runs whenever this file is continued into, not only for fork_pr > 0 as docs/ci.md has it. The same line is in Longshot and the launcher.

Depends on the orb or on settings:

8. Which pull request does the preview comment go to? shared.yml:141-144 passes pr: << pipeline.parameters.fork_pr >> and commit-sha: << pipeline.parameters.fork_sha >> to mb/preview-comment in the same-repository workflow too, where those are 0 and empty. Does the orb look the number up itself in that case?

9. Can a tester open the artifact link without a CircleCI account? shared.yml:119-125 builds a plain output.circle-artifacts.com link. If the project is private, that link asks for a sign-in.

10. A failed check and the gate. The gates use plain requires (events/pr.yml:32-34), so when lint, test or the comment job fails the gate job does not run. Does something else then post a failed ci/circleci: gate, or does the check stay missing?

11. "Never has secrets." The new text in docs/ci.md says the forks project never has secrets. events/fork.yml:15-19 gives fork-preview-comment the forgejo-comment context and :23-42 gives the two report jobs forgejo-bot; forks.yml gives the doorbell forgejo-bot. If the point is that jobs running fork code ask for no context, the sentence could say that. Also still in the new text: "A change outside public/, wrangler.jsonc and the package files runs lint and gate only", which point 6 contradicts.

12. Two deploys while both run. .forgejo/workflows/deploy.yml still deploys on every push to main and preview.yml still makes Cloudflare previews with the token on pull requests. Until they are removed, a site change deploys twice and a README-only merge still deploys.

Checked and fine: the orb is pinned to an exact version; the automatic pull request and fork workflows ask for no Cloudflare context; the main deploy requires lint and test; the link check runs offline, so an outside site being down cannot fail a pull request; a README-only change on main selects lint and gate only; the manual Cloudflare preview installs with --ignore-scripts and takes only public/ from the pull request.

One request: https://git.magicbean.dev/MagicBeanDev/orb is not readable (404), and the setup, fork handling, preview comment and gate live in the orb. Those parts cannot be reviewed from this pull request until its source can be read.

Looked over what runs when, at `d07dd272` (read at `a41c7991`; the push since then adds diagrams to `docs/ci.md` and changes nothing else). Seven things in the files, then points that depend on the orb or on settings I cannot see. **1. The manual dry run carries the production Cloudflare context onto any branch and runs package scripts there.** `.circleci/events/manual.yml:21-37`: `manual-release-dry-run` gives `deploy` the `cloudflare-prod` context with no branch condition. The `deploy` job (`shared.yml:203-217`) checks out the selected branch and runs `install-node-deps` before the dry-run guard, and that command only passes `--ignore-scripts` when asked (`shared.yml:46-54`); `deploy` does not ask. So a dry run on a branch runs that branch's npm lifecycle scripts with the production token in the environment. A dry run needs neither the context nor the scripts: a separate job without the context, or `ignore_scripts: true` plus no context, closes it. **2. Lint fails on `mise.toml` as committed.** The file has no final newline (it ends in `"""`), `.pre-commit-config.yaml:9` enables `end-of-file-fixer`, and lint runs `pre-commit run --all-files` (`shared.yml:73-75`, and `mise run lint`). That hook rewrites the file and exits non-zero. It is the only file in the diff without a final newline. **3. The preview's link rewrite never matches.** `shared.yml:116-117`: in `s/(href|src)=(\"|')\/([^/\"' ][^\"']*)\1/\1=\2\3\1/g` the closing quote is written as `\1`, which is `href` or `src`; the quote is `\2`. On `<link href="/style.css"><img src="/img/logo.png">` the command changes nothing; with `\2` for the closing quote it gives `href="style.css"` and `src="img/logo.png"`. `public/index.html` has five root-relative references (the three icons, `logo.webp`, `longshot.webp`), which in the artifact preview point at the artifact host's root. **4. The manual Cloudflare preview keeps files the pull request deleted.** `shared.yml:155-169` resets to main and then extracts the pull request's `public/` over main's. A file removed or renamed in the pull request stays in the upload. Clearing `public/` before the extract fixes it. **5. The manual preview looks at the first 50 open pull requests only.** `shared.yml:176-183` asks for `pulls?state=open&limit=50` and takes no further pages; a pull request beyond that is treated as absent and the job ends "No PR number; skipping upload" with success. **6. Docs changes are "full".** `mapping.conf:12-14` maps `docs/`, `CONTRIBUTING.md` and `AGENTS.md` to `full`, so they run tests and a preview on a pull request and deploy on main (`events/main.yml:3-20`). The description says anything outside `public/`, `wrangler.jsonc` and the package files runs lint only; that holds for `README.md`, which has no rule. **7. The fork workflow's condition is always true.** `.circleci/events/fork.yml:4-5` has `when: greater: [<< pipeline.parameters.fork_pr >>, 0]`. `greater` is not one of CircleCI's logic statements, and the compiler does not reject it: an unknown operator counts as true. A small config with exactly that condition compiles as valid and keeps the workflow with `fork_pr` at 0 as well as at 7; with `when: pipeline.parameters.fork_pr > 0` it is dropped at 0. So the workflow runs whenever this file is continued into, not only for `fork_pr` > 0 as `docs/ci.md` has it. The same line is in Longshot and the launcher. Depends on the orb or on settings: **8. Which pull request does the preview comment go to?** `shared.yml:141-144` passes `pr: << pipeline.parameters.fork_pr >>` and `commit-sha: << pipeline.parameters.fork_sha >>` to `mb/preview-comment` in the same-repository workflow too, where those are 0 and empty. Does the orb look the number up itself in that case? **9. Can a tester open the artifact link without a CircleCI account?** `shared.yml:119-125` builds a plain `output.circle-artifacts.com` link. If the project is private, that link asks for a sign-in. **10. A failed check and the gate.** The gates use plain `requires` (`events/pr.yml:32-34`), so when lint, test or the comment job fails the gate job does not run. Does something else then post a failed `ci/circleci: gate`, or does the check stay missing? **11. "Never has secrets."** The new text in `docs/ci.md` says the forks project never has secrets. `events/fork.yml:15-19` gives `fork-preview-comment` the `forgejo-comment` context and `:23-42` gives the two report jobs `forgejo-bot`; `forks.yml` gives the doorbell `forgejo-bot`. If the point is that jobs running fork code ask for no context, the sentence could say that. Also still in the new text: "A change outside `public/`, `wrangler.jsonc` and the package files runs lint and `gate` only", which point 6 contradicts. **12. Two deploys while both run.** `.forgejo/workflows/deploy.yml` still deploys on every push to main and `preview.yml` still makes Cloudflare previews with the token on pull requests. Until they are removed, a site change deploys twice and a README-only merge still deploys. Checked and fine: the orb is pinned to an exact version; the automatic pull request and fork workflows ask for no Cloudflare context; the main deploy requires lint and test; the link check runs offline, so an outside site being down cannot fail a pull request; a README-only change on main selects lint and gate only; the manual Cloudflare preview installs with `--ignore-scripts` and takes only `public/` from the pull request. One request: https://git.magicbean.dev/MagicBeanDev/orb is not readable (404), and the setup, fork handling, preview comment and gate live in the orb. Those parts cannot be reviewed from this pull request until its source can be read.
Author
Member

Thanks @mooki, this is a really useful review. You caught things circleci config validate can't. Fixed in d07dd272..a7cc146:

  1. Manual dry run with the prod context: the dry-run workflow now asks for no context, and deploy installs with --ignore-scripts whenever it's a dry run (a04620b).
  2. mise.toml final newline: fixed (0bb8dca). .gitignore had the same problem (a7cc146).
  3. Preview rewrite: the closing quote is now \2. Your sample becomes href="style.css" and src="img/logo.png" (9c51ccd).
  4. Deleted files in the Cloudflare preview: public/ is cleared before the PR's copy is extracted (697e4a2).
  5. First 50 PRs only: it now pages through every open PR (243c1ae).
  6. Docs were "full": docs/, CONTRIBUTING.md and AGENTS.md are lint-only now, as the description says (6c8e618).
  7. greater:: confirmed it counts as always true, so the fork workflow now uses when: pipeline.parameters.fork_pr > 0. Checked with circleci config process: the workflow is dropped at 0 and kept at 7 (5965b00).

Orb and settings:

  1. Which PR gets the comment: with pr: 0, mb/preview-comment looks up the open same-repo PR for the branch, paging through all of them, and skips quietly if there isn't one.
  2. Artifact links: good question, and I haven't confirmed it yet. It's in our test plan for the first live run. If the link needs a login, I'll switch the preview to something testers can open.
  3. A failed job and the gate: each job posts its own ci/circleci: <job> status, so a failed lint shows red on the PR with the cause. gate doesn't appear, and once it's a required check that blocks the merge. I'd rather not give gate an API token on PR pipelines just to turn it red. Happy to revisit if that's confusing in practice.
  4. "Never has secrets": fair, that was too broad. The docs now say jobs that run fork code get no secrets; only the status-report and preview-comment jobs hold tokens, and they run main's code and the orb, never fork code (b8d5b96).
  5. Two deploys: until cutover, CircleCI's main deploy runs as a dry run, so Forgejo's deploy.yml stays the only deployer (da5a2e8). The cutover PR flips that and removes deploy.yml and preview.yml in the same commit.

Orb source: MagicBeanDev/orb is being created now, and I'll open the first PR there with the source as soon as it exists. Until then I can share a tarball if that helps.

Thanks @mooki, this is a really useful review. You caught things `circleci config validate` can't. Fixed in `d07dd272..a7cc146`: 1. **Manual dry run with the prod context:** the dry-run workflow now asks for no context, and `deploy` installs with `--ignore-scripts` whenever it's a dry run (a04620b). 2. **`mise.toml` final newline:** fixed (0bb8dca). `.gitignore` had the same problem (a7cc146). 3. **Preview rewrite:** the closing quote is now `\2`. Your sample becomes `href="style.css"` and `src="img/logo.png"` (9c51ccd). 4. **Deleted files in the Cloudflare preview:** `public/` is cleared before the PR's copy is extracted (697e4a2). 5. **First 50 PRs only:** it now pages through every open PR (243c1ae). 6. **Docs were "full":** `docs/`, `CONTRIBUTING.md` and `AGENTS.md` are lint-only now, as the description says (6c8e618). 7. **`greater:`:** confirmed it counts as always true, so the fork workflow now uses `when: pipeline.parameters.fork_pr > 0`. Checked with `circleci config process`: the workflow is dropped at 0 and kept at 7 (5965b00). Orb and settings: 8. **Which PR gets the comment:** with `pr: 0`, `mb/preview-comment` looks up the open same-repo PR for the branch, paging through all of them, and skips quietly if there isn't one. 9. **Artifact links:** good question, and I haven't confirmed it yet. It's in our test plan for the first live run. If the link needs a login, I'll switch the preview to something testers can open. 10. **A failed job and the gate:** each job posts its own `ci/circleci: <job>` status, so a failed lint shows red on the PR with the cause. `gate` doesn't appear, and once it's a required check that blocks the merge. I'd rather not give `gate` an API token on PR pipelines just to turn it red. Happy to revisit if that's confusing in practice. 11. **"Never has secrets":** fair, that was too broad. The docs now say jobs that run fork code get no secrets; only the status-report and preview-comment jobs hold tokens, and they run main's code and the orb, never fork code (b8d5b96). 12. **Two deploys:** until cutover, CircleCI's main deploy runs as a dry run, so Forgejo's `deploy.yml` stays the only deployer (da5a2e8). The cutover PR flips that and removes `deploy.yml` and `preview.yml` in the same commit. **Orb source:** MagicBeanDev/orb is being created now, and I'll open the first PR there with the source as soon as it exists. Until then I can share a tarball if that helps.
Some checks are pending
Preview / Preview on Cloudflare (pull_request) Blocked by required conditions
This pull request is marked as a work in progress.
Some workflows are waiting to be reviewed.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u 'ci/circleci:drcrow-ci/circleci'
git switch 'drcrow-ci/circleci'

Merge

Merge the changes and update on Forgejo.

Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.

git switch 'main'
git merge --no-ff 'drcrow-ci/circleci'
git switch 'drcrow-ci/circleci'
git rebase 'main'
git switch 'main'
git merge --ff-only 'drcrow-ci/circleci'
git switch 'drcrow-ci/circleci'
git rebase 'main'
git switch 'main'
git merge --no-ff 'drcrow-ci/circleci'
git switch 'main'
git merge --squash 'drcrow-ci/circleci'
git switch 'main'
git merge --ff-only 'drcrow-ci/circleci'
git switch 'main'
git merge 'drcrow-ci/circleci'
git push origin 'main'
Sign in to join this conversation.
No reviewers
No labels
preview
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
MagicBeanDev/Site!10
No description provided.