Skip to content

Merge pull request #270 from objectstack-ai/claude/issue-269-deploy-v… #411

Merge pull request #270 from objectstack-ai/claude/issue-269-deploy-v…

Merge pull request #270 from objectstack-ai/claude/issue-269-deploy-v… #411

Workflow file for this run

name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
merge_group:
jobs:
# The four Node floor declarations (`engines.node` in the root, in `apps/docs`
# and in `tools/ci-scripts`, plus `.node-version`) are read by nothing in the
# install path: `.npmrc` sets no `engine-strict`, pnpm does not enforce
# `engines` by default, and every workflow here pins `node-version`
# explicitly instead of consulting them. This job is what makes them
# mechanically checkable. It needs no install — the script is zero-dependency
# and reads the lockfile as text — so it stays a seconds-long job that can
# run alongside `build`.
node-floor:
name: Node floor
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 22
# 裁决 (PR #74): a validator observed only green is indistinguishable
# from one that cannot go red. The fixtures run before the real scan, so
# a rule that stopped being able to fail fails the job on its own.
- name: Self-test
shell: bash
run: node .github/scripts/check-node-floor.mjs --self-test
# `shell: bash` is load-bearing here, not tidiness. The DEFAULT shell for
# a `run:` step is `bash -e {0}` with no pipefail, so in `node ... | tee`
# the step takes tee's exit status and a gate that exits 1 passes the job
# silently. Naming the shell gets `bash --noprofile --norc -eo pipefail
# {0}`, which propagates it.
- name: Check
shell: bash
run: node .github/scripts/check-node-floor.mjs | tee -a "$GITHUB_STEP_SUMMARY"
build:
runs-on: ubuntu-latest
# `NEXT_PRIVATE_STANDALONE` is what `@opennextjs/aws` sets before it runs
# `next build` — its own comment reads "Equivalent to setting `output:
# "standalone"` in next.config.js". Without it a plain `next build`
# produces no `.next/standalone/`, and the packaging step below fails on a
# missing `pages-manifest.json` three directories inside it. Measured on
# this branch before it was set: `ENOENT ... .next/standalone/apps/docs/
# .next/server/pages-manifest.json`.
#
# Set for the whole job rather than for the deploy path only, so that what
# a pull request builds is the same shape as what gets published. A build
# that differs from the deploy build is a small instance of the defect this
# card is about.
#
# It has to be declared in `turbo.json` as well: turbo 2 runs tasks in
# strict env mode, so an undeclared variable never reaches `next build` —
# and declaring it is also what puts it in the cache key, so a `.next`
# cached from before this line cannot be replayed without the standalone
# tree the packaging step needs.
env:
NEXT_PRIVATE_STANDALONE: 'true'
steps:
- uses: actions/checkout@v7
- uses: pnpm/action-setup@v6
- uses: actions/setup-node@v7
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
# `scripts/pm/check-half-states.mjs` is a verbatim upstream copy (#237)
# whose 1551 cases were run by nothing here. It is a STEP and not a
# registry entry because `tools/ci-scripts/run-self-tests.mjs` scans
# `.github/scripts` top level only, and that is load-bearing — it is what
# lets `.github/scripts/lib/` exist without tripping the
# unregistered-self-test rule. Widening the scan to reach one script would
# change this repo's gate topology; a step changes nothing.
#
# What it buys is a drift detector, not a hash-pin: #237's ablation
# mutated `DEFAULT_SWEEP_REPO` in this copy and 2 of the 1551 went red, so
# an edit here that changes BEHAVIOUR fails. One that changes bytes
# without changing behaviour still passes — pinning this copy to upstream
# byte for byte needs a cross-repo credential and is a separate decision.
#
# `--self-test` is the whole of it. Without the flag the script runs a
# live sweep needing a transport prerequisite this job does not have; that
# caller is `.github/workflows/half-state-patrol.yml`, on its own
# schedule. Zero-dependency and about a second, so it runs before the
# build rather than behind it.
- name: Half-state sweeper self-test
shell: bash
run: node scripts/pm/check-half-states.mjs --self-test
# `content/docs/**/*.zh-Hant.mdx` and `meta.zh-Hant.json` are generated
# from the Simplified siblings by `apps/docs/scripts/gen-zh-hant.mjs` and
# committed, because `lib/seo.ts` tells a real translation from an English
# fallback by the presence of a locale-suffixed FILE — a conversion done
# while rendering would leave the locale out of every sitemap entry and
# hreflang cluster. Committed output needs a gate or it drifts: this
# regenerates in memory and compares bytes, so a hand edit, a stale file
# whose source was retired, and a converter upgrade nobody re-ran all fail
# here with the same one-line fix.
#
# Before `type-check` on purpose: it needs no build, and a content drift
# reported as a type error is a wrong first diagnosis.
- name: Generated zh-Hant is current
run: node apps/docs/scripts/gen-zh-hant.mjs --check
- run: pnpm turbo run type-check --continue
- run: pnpm turbo run build
# Reads the BUILT sitemap and asserts its locale composition against an
# oracle derived from `content/docs/`. It has to come after `build` — the
# measurement is taken off the artifact, because importing `sitemap.ts`
# pulls in the whole MDX collection and the `@/` alias.
#
# Not a turbo task on purpose: turbo would hash it against the ci-scripts
# package's own inputs, which do not include `apps/docs/.next/`, so a
# locale regression would replay a cached green. `pnpm turbo run test`
# below still runs this script's `--self-test`, which is what keeps its
# rules provably able to fail.
#
# `shell: bash` is load-bearing, not tidiness. The DEFAULT shell for a
# `run:` step is `bash -e {0}` with no pipefail, so in `node ... | tee`
# the step takes tee's exit status and a gate that exits 1 passes the job
# silently. Naming the shell gets `bash --noprofile --norc -eo pipefail
# {0}`, which propagates it.
- name: Locale surface
shell: bash
run: node .github/scripts/check-locale-surface.mjs | tee -a "$GITHUB_STEP_SUMMARY"
- run: pnpm turbo run test
# Defect 2 of #269: the deploy used to run its own `pnpm install` and its
# own `opennextjs-cloudflare build`, so CI built the site, threw it away,
# and the deploy published a SECOND build that nothing here had checked.
# The published artifact was unverified by construction.
#
# `--skipNextBuild` packages the `.next` output `pnpm turbo run build`
# produced above — the same output `Locale surface` measured and the same
# tree every step in this job passed — and `deploy-docs.yml` uploads THIS
# bundle rather than making another one.
#
# Last in the job on purpose: the artifact then only exists for a commit
# that cleared every gate above it. And only on a push to `main`, because
# that is the only event that deploys, so a pull request pays nothing.
- name: Package the Worker from the build this job tested
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
working-directory: apps/docs
run: pnpm exec opennextjs-cloudflare build --skipNextBuild
# `include-hidden-files` is load-bearing, not tidiness: the compiled
# OpenNext config the deploy reads lives at `.open-next/.build/`, and
# upload-artifact excludes dotted paths by default. Without it the
# download succeeds, the deploy exits 1 on a missing config, and the
# cause is three directories away from the message.
- name: Upload the Worker bundle
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
uses: actions/upload-artifact@v7
with:
name: docs-worker
path: apps/docs/.open-next
include-hidden-files: true
if-no-files-found: error
retention-days: 3
# Defect 1 of #269: `deploy-docs.yml` used to hang off `push: branches:
# [main]` exactly as this workflow does, so the two ran in PARALLEL and a
# commit that failed any gate above still deployed. There was no `needs:` and
# no `workflow_run` anywhere.
#
# As a job here it cannot start until `node-floor` and `build` are green, and
# the `if:` keeps it off pull requests and merge groups. `workflow_run` would
# also have gated it, but it fires on a FAILED run too — the conclusion has
# to be re-checked by hand inside the workflow — and it runs detached from
# the run whose artifact it publishes, which is what the `with:` line here
# depends on.
#
# ⚠️ This deploy is expected to FAIL while #261 is open: `main` builds a
# Worker over Cloudflare's 64 MiB limit, so the upload is rejected at version
# creation and the serving version cannot be displaced. That is the current
# deliberate steady state, not a regression from this wiring.
deploy-docs:
name: Deploy docs
needs: [node-floor, build]
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
# Preserves what `deploy-docs.yml` declared for itself before it became a
# called workflow: one deploy at a time, and never cancel one in flight.
concurrency:
group: deploy-docs
cancel-in-progress: false
permissions:
contents: read
actions: write # dispatch rollback-docs.yml when the smoke check fails
issues: write # file or update the one deploy-failure card
uses: ./.github/workflows/deploy-docs.yml
with:
artifact_name: docs-worker
secrets: inherit