From 1cd960f33ff690cf0a63ccaca9606c54bfb89895 Mon Sep 17 00:00:00 2001 From: Muhammed Taha Ayan Date: Wed, 26 Aug 2026 04:40:48 +0000 Subject: [PATCH] ci(docs): build docs.qauth.dev on Netlify's own CD, and pin pnpm for it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Netlify now builds and deploys the site from `main` directly, so the GitHub Actions deploy is redundant and is removed. `.github/workflows/docs.yml` built the site in Actions and pushed the prebuilt output through the Netlify CLI, which needed a `NETLIFY_AUTH_TOKEN` and a `NETLIFY_SITE_ID` that were never set — so it had been failing on every push and every PR. The site's checks do not go with it: `ci.yml` runs `nx affected -t lint typecheck test build`, which covers `docs-site` and its drift invariants whenever the diff touches them. `netlify.toml` replaces it, and fixes two things that made the first Netlify build fail: **pnpm.** The root `package.json` declares `engines.pnpm: ">=11.0.0"` but pins the exact version only inside each workflow's `pnpm/action-setup` input, so an external builder has nothing to read it from. Netlify activated its bundled pnpm 10.30.3 and the install died with `ERR_PNPM_UNSUPPORTED_ENGINE` before a file was built. `PNPM_VERSION` is now stated where Netlify reads it, at the same 11.4.0 the workflows install. Deliberately not a `packageManager` field: every workflow passes an explicit `version` to `pnpm/action-setup`, and a second declaration would be a conflict to resolve rather than a pin. **The publish path.** The site was configured to publish `apps/docs-site/dist`, which is empty — `astro.config.mjs` redirects `outDir` to the workspace-level `dist/apps/docs-site` so it matches the Nx target's declared `outputs`. That mismatch would have deployed nothing and reported success once the install was fixed. Verified by running the configured command: `pnpm exec nx build docs-site` lands 120 files there. `netlify.toml` overrides the equivalent Netlify UI fields, so the build is reproducible from the repository rather than from dashboard settings nobody can review. The docs-site README's deployment section is rewritten to match, including why those two settings are load-bearing. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01TKAC2F5PayPstKTrF2NLp9 --- .github/workflows/docs.yml | 152 ------------------------------------- apps/docs-site/README.md | 66 ++++++++-------- netlify.toml | 52 +++++++++++++ 3 files changed, 85 insertions(+), 185 deletions(-) delete mode 100644 .github/workflows/docs.yml create mode 100644 netlify.toml diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml deleted file mode 100644 index f6bfb63..0000000 --- a/.github/workflows/docs.yml +++ /dev/null @@ -1,152 +0,0 @@ -name: Docs Site Deploy - -# Builds and deploys the QAuth documentation site (docs.qauth.dev): -# a production deploy on every push to main, a preview deploy on every -# pull request. See apps/docs-site/README.md for the full picture. -# -# The path filters below are NOT `apps/docs-site/**` alone. Task 10 (#356) -# made `apps/docs-site/src/content.config.ts`'s `records` collection load -# `docs/adr/*.md` and `docs/security/*.md` directly off disk via a `glob()` -# loader (and `apps/docs-site/src/lib/records.ts` reads the same two -# directories a second, independent time for the records index page) — both -# render into the site without anything under `apps/docs-site/` changing. A -# filter of `apps/docs-site/**` alone would let an ADR edit go live on -# GitHub while the deployed site quietly stays stale. See the Task 11 -# report for how this was derived and which changed-file examples were -# traced through it. -on: - push: - branches: - - main - # Spelled out longhand in both places on purpose. A YAML anchor/alias pair - # (`&docs-site-inputs` / `*docs-site-inputs`) is valid YAML but GitHub's - # workflow parser rejects it outright ("Anchors are not currently - # supported"), which makes the whole file unparseable and the workflow - # never runs. Keep the two lists identical by hand. - paths: - - 'apps/docs-site/**' - - 'docs/adr/**' - - 'docs/security/**' - - '.github/workflows/docs.yml' - pull_request: - paths: - - 'apps/docs-site/**' - - 'docs/adr/**' - - 'docs/security/**' - - '.github/workflows/docs.yml' - # Manual escape hatch: the path filters above are the build's true content - # inputs, not every possible influence on the built output (a root - # dependency bump in package.json/pnpm-lock.yaml, for instance, changes - # nothing the filters watch). Re-run this from the Actions tab after a - # change like that, or to redeploy without a matching content change. - workflow_dispatch: {} - -# Read-only checkout is all this job needs — it deploys via the Netlify CLI -# using NETLIFY_AUTH_TOKEN/NETLIFY_SITE_ID, not the GitHub API, so there is -# nothing here to write back to the repo. -permissions: - contents: read - -# A manual workflow_dispatch run and a push/pull_request run share no -# `github.ref`-derived identity worth serializing against each other, so -# only push/pull_request runs are grouped. Newer commits on the same PR (or -# the same branch) cancel an in-flight deploy of an older commit rather than -# letting two uploads race to publish the same preview/production target -# out of order. -concurrency: - group: docs-deploy-${{ github.event_name == 'workflow_dispatch' && github.run_id || github.ref }} - cancel-in-progress: true - -jobs: - deploy: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - - - uses: pnpm/action-setup@v6 - name: Install pnpm - with: - version: 11.4.0 - run_install: false - - - uses: actions/setup-node@v7 - with: - node-version: 24 - cache: 'pnpm' - - - run: pnpm install --frozen-lockfile - - # Same guard the site's own drift checks provide locally: a broken - # link, a bad anchor, or missing endpoint coverage fails here instead - # of publishing. - - run: pnpm exec nx test docs-site - - - run: pnpm exec nx build docs-site - - # Fail loudly and stop before spending a build's output on a deploy - # attempt that can't authenticate. Both secrets are unset in this repo - # today; a silent partial deploy (or the CLI hanging on an interactive - # prompt neither secret is there to answer) is worse than a clear, - # named failure here. - - name: Verify Netlify secrets are configured - # Scoped to exactly the two conditions below that actually attempt a - # deploy. A `workflow_dispatch` run on a non-main branch deploys - # nothing (see the last step), so it must not fail here over secrets - # it will never need. Fork pull requests are excluded for the same - # reason: GitHub withholds secrets from them, so they can never have - # these two and never reach the preview step below — failing them here - # would throw away an external contributor's passing test and build - # results over a condition they cannot fix. - if: (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork != true) || github.ref == 'refs/heads/main' - env: - NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} - NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} - run: | - missing=() - [ -z "$NETLIFY_AUTH_TOKEN" ] && missing+=("NETLIFY_AUTH_TOKEN") - [ -z "$NETLIFY_SITE_ID" ] && missing+=("NETLIFY_SITE_ID") - if [ "${#missing[@]}" -ne 0 ]; then - echo "::error::Missing required secret(s): ${missing[*]}. Configure them in the repository's Actions secrets before this workflow can deploy." >&2 - exit 1 - fi - - - name: Deploy preview (pull request) - # Non-fork pull requests only: a fork-originated `pull_request` run gets - # no repository secrets, so NETLIFY_AUTH_TOKEN/NETLIFY_SITE_ID below - # would both be empty and the deploy would hard-fail (or hang on an - # interactive prompt). Fork PRs still get the test and build gates - # above; they just publish no preview. - if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork != true - env: - NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} - NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} - run: | - pnpm dlx netlify-cli@27.0.0 deploy \ - --dir=dist/apps/docs-site \ - --message="Deploy preview for PR #${{ github.event.pull_request.number }} (${{ github.sha }})" - - - name: Deploy production (main) - # `push` is already branch-restricted to main above (`on.push.branches`), - # but `workflow_dispatch` carries no such restriction — it can be run - # against any branch from the Actions tab — so the ref is checked - # explicitly here too, rather than trusting the triggering event alone. - if: github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') - env: - NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} - NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} - run: | - pnpm dlx netlify-cli@27.0.0 deploy \ - --prod \ - --dir=dist/apps/docs-site \ - --message="Production deploy from ${{ github.sha }}" - - - name: Manual run on a non-main branch (no deploy) - # A workflow_dispatch run against anything other than main builds and - # tests (both steps above already ran) but intentionally does not - # deploy: it's not a pull request (no preview target) and it's not - # main (no production target), so there is nothing correct to - # publish to. Named and explicit rather than silently matching no - # `if:` and vanishing from the run summary unexplained. - if: github.event_name == 'workflow_dispatch' && github.ref != 'refs/heads/main' - run: | - echo "::notice::Manual run on '${{ github.ref_name }}', not main — build and tests passed, but nothing was deployed (no preview or production target applies)." diff --git a/apps/docs-site/README.md b/apps/docs-site/README.md index b0fd31e..4a7207d 100644 --- a/apps/docs-site/README.md +++ b/apps/docs-site/README.md @@ -2,7 +2,8 @@ The QAuth documentation site (Astro + Starlight), published at [docs.qauth.dev](https://docs.qauth.dev). Source lives here; deployment is -handled by `.github/workflows/docs.yml` at the repo root. +handled by Netlify's own git-based continuous deployment, configured in +`netlify.toml` at the repo root. ## Local development @@ -60,38 +61,37 @@ so a guard failure blocks the deploy — it does not publish a broken site. ## Deployment -`.github/workflows/docs.yml`: - -- **Pull requests** get a Netlify **preview deploy** (draft, with its own - URL, printed in the job log). -- **Pushes to `main`** get a **production deploy**. -- A `workflow_dispatch` run is available as a manual escape hatch — useful - after a change the path filter doesn't watch (e.g. a root dependency bump - in `package.json`/`pnpm-lock.yaml`), since it isn't a change under - `apps/docs-site/**`, `docs/adr/**`, or `docs/security/**`. - -Both triggers run the same steps: install, `nx test docs-site`, -`nx build docs-site`, then upload `dist/apps/docs-site` with the Netlify CLI -(`netlify deploy` / `netlify deploy --prod`). The workflow builds the site -itself in GitHub Actions (using this repo's normal pnpm/Nx setup and cache) -and uploads the prebuilt output — it does not rely on Netlify's own -git-connected build servers. - -This requires two repository secrets, **currently unset**: - -- `NETLIFY_AUTH_TOKEN` — a personal or CI access token - (Netlify dashboard → User settings → Applications → Personal access - tokens). -- `NETLIFY_SITE_ID` — the target site's Project ID (Netlify dashboard → - Site configuration → General → Project ID). - -If a Netlify site doesn't exist yet for this project, create one (a manual -`netlify init --manual` or `netlify deploy` from a one-off local build is -enough — Git-based continuous deployment is not needed here, since this -workflow uploads a prebuilt artifact instead) and copy its Project ID -into `NETLIFY_SITE_ID`. Without both secrets set, the workflow fails -immediately with a named "missing secret" error rather than silently -skipping the deploy. +Netlify's own git-connected build, configured by `netlify.toml` at the repo +root. Netlify clones the repository, installs the pnpm workspace, runs +`pnpm exec nx build docs-site`, and publishes `dist/apps/docs-site`. Pushes to +`main` produce a production deploy and pull requests produce a deploy preview, +both on Netlify's side — no repository secrets and no GitHub Actions step are +involved. + +There used to be one: `.github/workflows/docs.yml` built the site in Actions and +uploaded the prebuilt output through the Netlify CLI, which needed a +`NETLIFY_AUTH_TOKEN` and a `NETLIFY_SITE_ID`. It was removed in favour of +Netlify's own CD. The site's checks did not go with it — `ci.yml` runs +`nx affected -t lint typecheck test build`, which covers `docs-site` (its drift +invariants included) whenever the diff touches it or anything it reads. + +Two settings in `netlify.toml` are load-bearing, and both are documented at the +lines that set them: + +- **`PNPM_VERSION`.** The root `package.json` declares + `engines.pnpm: ">=11.0.0"` but pins the exact version only inside each + workflow's `pnpm/action-setup` input, so an external builder has nothing to + read it from. Unset, Netlify activates its bundled pnpm and the install fails + with `ERR_PNPM_UNSUPPORTED_ENGINE` before anything is built. Keep it in step + with `.github/workflows/*.yml`. +- **`publish`.** `dist/apps/docs-site`, not `apps/docs-site/dist` — see + [Build](#build) for why `outDir` points at the workspace root. A publish path + aimed at Astro's default location deploys an empty directory and reports + success. + +Because `netlify.toml` overrides the equivalent fields in the Netlify UI, the +build is reproducible from the repository rather than from dashboard settings +nobody can review. ## DNS and TLS for docs.qauth.dev diff --git a/netlify.toml b/netlify.toml new file mode 100644 index 0000000..65aaf79 --- /dev/null +++ b/netlify.toml @@ -0,0 +1,52 @@ +# Netlify build configuration for docs.qauth.dev (#347). +# +# Deployment is Netlify's OWN git-based continuous deployment. There is +# deliberately no GitHub Actions deploy workflow: `.github/workflows/docs.yml` +# built the site and pushed a prebuilt artifact through the Netlify CLI, and it +# was deleted when this file landed. The site's lint/typecheck/test/build +# coverage did not go with it — `ci.yml` runs `nx affected -t lint typecheck +# test build`, which picks up `docs-site` whenever the diff touches it or +# anything it reads. +# +# Settings here OVERRIDE the equivalents in the Netlify UI, which is the point: +# the build is reproducible from the repository rather than from a dashboard +# nobody can review. + +[build] + # The repository root. This is a pnpm WORKSPACE — the lockfile and the + # packages `docs-site` depends on live here, so the install has to happen at + # the root. Pointing `base` at `apps/docs-site` would leave Netlify installing + # from a package with no lockfile of its own. + base = "." + + command = "pnpm exec nx build docs-site" + + # `dist/apps/docs-site`, NOT `apps/docs-site/dist`. + # + # `astro.config.mjs` redirects `outDir` to the workspace-level output so it + # matches the Nx `build` target's declared `outputs` — a declared output that + # does not match reality breaks Nx caching silently. Astro's default location + # is therefore empty, and a publish path pointing at it deploys nothing while + # reporting success. + publish = "dist/apps/docs-site" + +[build.environment] + # Pinned to what the root `package.json` demands (`engines.pnpm: ">=11.0.0"`) + # and to what every workflow in `.github/workflows/` installs. + # + # Without this the build image activates its own bundled pnpm — 10.30.3 at the + # time of writing — and the install dies with ERR_PNPM_UNSUPPORTED_ENGINE + # before a single file is built. The repo pins the version in each workflow's + # `pnpm/action-setup` input rather than in a `packageManager` field, so an + # external builder has nothing to read it from; this is that pin for Netlify. + # Keep it in step with `.github/workflows/*.yml`. + PNPM_VERSION = "11.4.0" + + # `engines.node` is `>=24.7.0`. Naming the major lets Netlify take the current + # 24.x rather than freezing a patch release nobody will remember to bump. + NODE_VERSION = "24" + + # No Nx daemon in an ephemeral builder: it exists to keep a project graph warm + # across repeated invocations, and there is exactly one here. Left on, it also + # spawns a background process that can outlive the build. + NX_DAEMON = "false"