Skip to content

Commit 971302e

Browse files
docs(ci): 工作流清单按实际枚举,ci.yml 任务表重写,并补上反向断言 (#3212) (#3253)
* docs(ci): document every workflow, drop the hand-maintained count, pin the reverse direction `ci-cd-pipeline.md` opened with "11 GitHub Actions workflows" against a directory that held 13, and three of them had never been documented at all: `lint.yml` (a real PR gate), `cross-repo-issue-closer.yml`, and `changeset-guard.yml`. The `ci.yml` section listed five jobs, of which three (Lint, Build Core, and an unsharded Test) do not exist. - Replace the count with an enumerated inventory table: file, the name each workflow appears under in the checks list, its trigger, and whether it can block a merge. The number is gone on purpose — it was wrong three times. - Drop the ASCII overview box; it was a second hand-maintained copy of the same set and the older of the two drifted first. - Rewrite the `ci.yml` section against the real job keys (`changeset-check`, `type-check`, `test` as a 4-shard PR-only matrix, `test-coverage` on push only, `e2e`, `docs`, `dev-server`), plus an explicit "what is not in ci.yml" for the two jobs readers were sent to look for. - Add sections for `lint.yml` (errors gate, warnings do not) and `cross-repo-issue-closer.yml`, and its `CROSS_REPO_ISSUE_TOKEN` secret. #3197 pinned only the forward direction (every workflow the page names must exist); the reverse was left out because it would have gone red on exactly these omissions. It is added here: every file in `.github/workflows/` must be named in a heading on the page, with a failure message that tells the next author what to write. `changeset-guard.yml` landing between #3212 being filed and being fixed is why the snapshot alone is not enough. Fixes #3212 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NVPjPzmmAJ2Ngtvgg5MSRa * docs(ci): the fence-stripping comment no longer points at a deleted diagram The previous commit removed the ASCII overview box the comment cited as the sole reason for excluding fenced blocks. The exclusion is still right — the "Adding a New Workflow" section shows YAML for workflows that do not exist — so say that instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NVPjPzmmAJ2Ngtvgg5MSRa --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 74be2cb commit 971302e

2 files changed

Lines changed: 191 additions & 62 deletions

File tree

content/docs/guide/ci-cd-pipeline.md

Lines changed: 124 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -5,72 +5,96 @@ description: Overview of the ObjectUI continuous integration and deployment work
55

66
# CI/CD Pipeline
77

8-
ObjectUI uses **11 GitHub Actions workflows** to automate testing, quality checks, security scanning, releases, and repository maintenance. All workflow files live in `.github/workflows/`.
8+
ObjectUI automates testing, quality checks, releases, and repository maintenance with GitHub Actions. All workflow files live in `.github/workflows/`.
9+
10+
This page deliberately states **no workflow count**. It used to open with "11 GitHub Actions
11+
workflows"; the directory held 12 when [#3212](https://github.com/objectstack-ai/objectui/issues/3212)
12+
was filed and 13 by the time it was fixed. A hand-maintained number drifts by construction, and a
13+
stale one still reads as authoritative. What is pinned instead is the *set*:
14+
`scripts/__tests__/ci-cd-pipeline-doc.test.ts` fails `pnpm test` when a file in
15+
`.github/workflows/` has no section on this page, **and** when this page names a `.yml` that is not
16+
in that directory. Adding a workflow without documenting it is a red test, not a silent omission.
17+
18+
## Workflow Inventory
19+
20+
Every workflow, the name it appears under in the checks list (they are not the same string —
21+
`performance-budget.yml` shows up as **Bundle Analysis**), and whether it can block a merge. Each
22+
one has its own section below.
23+
24+
| Workflow file | Appears as | Runs on | Blocks a PR? |
25+
|---|---|---|---|
26+
| `ci.yml` | CI | Push / PR to `main`, `develop` | **Yes** — 6 of its 7 jobs run on PRs |
27+
| `lint.yml` | Lint | Push / PR to `main`, `develop`; manual | **Yes** — ESLint **errors** only |
28+
| `changeset-guard.yml` | Changeset Bump Policy | PR / push touching `.changeset/**` | **Yes** |
29+
| `performance-budget.yml` | Bundle Analysis | Push / PR touching `packages/**`, `apps/console/**`, `pnpm-lock.yaml` | **Yes** — the console entry gzip budget |
30+
| `labeler.yml` | Auto Label PRs | PR `opened`, `synchronize`, `reopened` | No |
31+
| `dependabot-auto-merge.yml` | Dependabot Auto-merge | PR to `main`/`develop` authored by `dependabot[bot]` | No |
32+
| `cross-repo-issue-closer.yml` | Cross-repo Issue Closer | PR `closed` (acts only when merged) | No — runs after merge |
33+
| `changeset-release.yml` | Changeset Release | Push to `main` | n/a |
34+
| `release.yml` | Release | Push of a `v*` tag | n/a |
35+
| `changelog.yml` | Auto Changelog | GitHub Release published; manual | n/a |
36+
| `stale.yml` | Stale Issues & PRs | Daily cron `0 0 * * *`; manual | n/a |
37+
| `shadcn-check.yml` | Check Shadcn Components | Weekly cron `0 9 * * 1`; manual | n/a |
38+
| `check-links.yml` | Check Links | Manual dispatch only | n/a |
39+
40+
Two path-filter facts explain most "why did nothing run on my PR?" questions:
41+
42+
- `ci.yml` and `lint.yml` both list `**/*.md`, `content/**`, `docs/**` and `.changeset/**` under
43+
`paths-ignore` (`ci.yml` also ignores `apps/site/**`). A docs-only or changeset-only PR starts
44+
neither of them.
45+
- `changeset-guard.yml` carries the inverse filter — it runs *only* when `.changeset/**` changes,
46+
which is precisely why it is a separate workflow instead of a job inside `ci.yml`.
947

10-
## Workflow Overview
48+
## Core CI Workflow (`ci.yml`)
1149

12-
```
13-
┌─────────────────────────────────────────────────────────────────┐
14-
│ Push / PR to main/develop │
15-
├─────────────────────────────────────────────────────────────────┤
16-
│ │
17-
│ ┌──────────┐ ┌──────────────┐ │
18-
│ │ ci.yml │ │ performance- │ │
19-
│ │ (test, │ │ budget.yml │ │
20-
│ │ lint, │ │ (bundle size)│ │
21-
│ │ build) │ │ │ │
22-
│ └──────────┘ └──────────────┘ │
23-
│ │
24-
│ ┌──────────────┐ │
25-
│ │ labeler.yml │ │
26-
│ │ │ │
27-
│ └──────────────┘ │
28-
│ │
29-
├─────────────────────────────────────────────────────────────────┤
30-
│ Push to main │
31-
│ ┌───────────────────┐ │
32-
│ │ changeset-release │ → npm publish via changesets │
33-
│ │ .yml │ │
34-
│ └───────────────────┘ │
35-
│ │
36-
├─────────────────────────────────────────────────────────────────┤
37-
│ Tag push (v*) │
38-
│ ┌──────────┐ ┌───────────────┐ │
39-
│ │ release │ │ changelog.yml │ │
40-
│ │ .yml │ │ (git-cliff) │ │
41-
│ └──────────┘ └───────────────┘ │
42-
│ │
43-
├─────────────────────────────────────────────────────────────────┤
44-
│ Scheduled │
45-
│ ┌──────────┐ ┌───────────────────┐ ┌──────────────────┐ │
46-
│ │ stale │ │ shadcn-check.yml │ │ dependabot- │ │
47-
│ │ .yml │ │ (weekly Mon 9AM) │ │ auto-merge.yml │ │
48-
│ └──────────┘ └───────────────────┘ └──────────────────┘ │
49-
│ │
50-
├─────────────────────────────────────────────────────────────────┤
51-
│ Manual dispatch │
52-
│ ┌──────────────┐ │
53-
│ │ check-links │ → Lychee link validation │
54-
│ │ .yml │ │
55-
│ └──────────────┘ │
56-
└─────────────────────────────────────────────────────────────────┘
57-
```
50+
**Triggers:** Push and PR to `main` and `develop`, unless the change touches only `**/*.md`,
51+
`content/**`, `docs/**`, `apps/site/**` or `.changeset/**` (`paths-ignore`).
5852

59-
## Core CI Workflow (`ci.yml`)
53+
Seven jobs, all parallel — there are no `needs:` edges between them:
54+
55+
| Job key | Appears as | What it runs | When |
56+
|---|---|---|---|
57+
| `changeset-check` | Changeset Fixed Group Check | `scripts/check-changeset-fixed.mjs` — every workspace package must be in the changeset `fixed` group or explicitly ignored. It checks group *membership*; it does **not** check whether the PR added a changeset. | Every run |
58+
| `type-check` | Type Check | `scripts/check-type-check-coverage.mjs`, then `pnpm check:spec-symbols`, then `pnpm type-check`. The coverage guard runs first because turbo silently skips packages that have no `type-check` script, so a package without one would otherwise read as passing (#2911). | Every run |
59+
| `test` | Test (shard N/4) | `pnpm test --shard=N/4` across a 4-runner matrix with `fail-fast: false`, so every shard reports its own failures. No coverage instrumentation — v8 adds 40–100% overhead. | **Pull requests only** |
60+
| `test-coverage` | Test (coverage) | One unsharded `pnpm test:coverage`, uploaded to Codecov. Nothing blocks on it, which is why it is not sharded. | **Push only** |
61+
| `e2e` | Build & E2E | Builds the console with `vite build` (`VITE_BASE_PATH=/console/`), verifies the artifact, then `pnpm test:e2e --project=chromium`. Uploads the Playwright report on failure. | Every run |
62+
| `docs` | Build Docs | `turbo run build --filter='@object-ui/site'`. On a PR it first diffs against the base and skips the build when nothing under `apps/site/` or `content/` changed. | Every run (build itself conditional) |
63+
| `dev-server` | Dev-server fixture build | `pnpm --filter @object-ui/dev-server build` — guards `apps/dev-server`'s `objectstack.config.ts` against fixture / `@objectstack/spec` drift. | Every run |
64+
65+
Uses: Node 22.x, pnpm via `corepack`, `actions/cache` over `.turbo/cache`.
66+
67+
### What is *not* in `ci.yml`
68+
69+
Two jobs this page used to list have never existed under those names, and looking for them in
70+
`ci.yml` is a dead end:
71+
72+
- **Lint** is not a `ci.yml` job. ESLint runs in its own workflow, `lint.yml` (next section), and
73+
shows up as a separate **Lint** check on the PR.
74+
- **Build Core** does not exist. `ci.yml` builds only the console SPA that Playwright consumes;
75+
building the packages and measuring their size belongs to the Bundle Analysis workflow
76+
(`performance-budget.yml`), as the comment on the `e2e` job states.
77+
78+
## Lint (`lint.yml`)
6079

61-
**Triggers:** Push and PR to `main` and `develop` branches.
80+
**Triggers:** Push and PR to `main`/`develop` (same `paths-ignore` as `ci.yml`, minus
81+
`apps/site/**`), plus manual dispatch.
6282

63-
Runs five parallel jobs:
83+
This is a **real PR gate**, and it is easy to miss because it is not part of CI — it is its own
84+
**Lint** entry in the checks list.
6485

65-
| Job | Description |
66-
|-----|-------------|
67-
| **Test** | Runs `vitest` across all packages with Turbo caching. Uploads coverage to Codecov. |
68-
| **Lint** | Runs ESLint via `eslint.config.js` (flat config) and TypeScript type-checking. |
69-
| **Build Core** | Builds all packages using `turbo run build`. |
70-
| **E2E Tests** | Runs Playwright end-to-end tests from the `e2e/` directory. |
71-
| **Build Docs** | Builds the documentation site (`apps/site`). |
86+
- `scripts/check-lint-coverage.mjs` runs first: every package must run ESLint or be declared a
87+
known gap. turbo skips scriptless packages silently, so without this guard a package reads as
88+
clean because nothing ever linted it.
89+
- Then `pnpm lint`.
7290

73-
Uses: Node 22, pnpm (via `corepack`), Turbo remote caching.
91+
**It gates errors, not warnings.** `--max-warnings` is deliberately unset: the repository carries
92+
thousands of warnings (overwhelmingly `no-explicit-any`, plus React Compiler rules the config
93+
downgrades on purpose), and failing on those would make the gate unusable. What must stay clean are
94+
the rules `eslint.config.js` sets to `error` — including the custom `object-ui/*` ratchets
95+
(ADR-0054 Phase 5, #2879, the `objectql.ts` ratchet, `no-dynamic-import-in-test-hook`). Until #2923
96+
this workflow was `workflow_dispatch`-only, so every one of those `error` ratchets was inert: each
97+
was written specifically to fail CI, and nothing ran them.
7498

7599
## Performance Budget (`performance-budget.yml`)
76100

@@ -182,6 +206,35 @@ Uses [git-cliff](https://git-cliff.org/) with `cliff.toml` configuration to auto
182206

183207
Automatically labels PRs based on file path patterns defined in `.github/labeler.yml`. Syncs labels on each push to the PR.
184208

209+
### Cross-repo Issue Closer (`cross-repo-issue-closer.yml`)
210+
211+
**Trigger:** `pull_request_target` with type `closed`; the job acts only when the PR was actually
212+
merged.
213+
214+
GitHub's closing keywords work **only within a repository**. A PR here whose body says
215+
`Fixes objectstack-ai/objectstack#4475` reads to a human exactly like a same-repo close, merges,
216+
and leaves that issue open forever — with no reference to the PR on the issue's page either. That
217+
is not hypothetical: during v17 verification it happened twice in one day, and both framework
218+
issues had to be closed by hand.
219+
220+
This workflow scans the merged PR body for **qualified** `owner/repo#N` closing keywords (the bare
221+
`#N` form is left to GitHub) and takes one of two visible paths:
222+
223+
| `CROSS_REPO_ISSUE_TOKEN` | Behaviour |
224+
|---|---|
225+
| Configured | Comments on each foreign issue with the PR link, then closes it as `completed`. |
226+
| Absent | Comments **on this PR**, listing every issue that still has to be closed by hand. |
227+
228+
The second path is the point. A workflow that quietly does nothing because a secret was never
229+
provisioned is the same "declared but never enforced" shape both repositories keep having to fix,
230+
so the missing credential announces itself — the run logs the token's presence before any early
231+
return, and the PR comment names the cost.
232+
233+
It uses `pull_request_target` rather than `pull_request` because the latter withholds repository
234+
secrets from fork-originated runs. The usual hazard of `pull_request_target` does not apply here:
235+
the job never checks out the head ref and never executes anything from the PR — it reads the body
236+
and calls the issues API.
237+
185238
### Stale Issues (`stale.yml`)
186239

187240
**Trigger:** Daily at 00:00 UTC (cron), or manual dispatch.
@@ -211,6 +264,12 @@ Exempt labels: `pinned`, `security`, `critical`, `in-progress`.
211264

212265
## Adding a New Workflow
213266

267+
> **Give it a section on this page in the same PR.** Not a convention — a test.
268+
> `scripts/__tests__/ci-cd-pipeline-doc.test.ts` reads `.github/workflows/` and fails when a
269+
> workflow has no heading here naming its file. Three workflows (`lint.yml`,
270+
> `cross-repo-issue-closer.yml`, `changeset-guard.yml`) went undocumented for months precisely
271+
> because nothing checked, and one of them is a PR gate.
272+
214273
1. Create a new `.yml` file in `.github/workflows/`.
215274
2. Follow the existing pattern for pnpm + Turbo setup:
216275

@@ -240,13 +299,18 @@ on:
240299
- 'pnpm-lock.yaml'
241300
```
242301
302+
5. Add a section for it under the right heading on this page, and a row to the
303+
[inventory table](#workflow-inventory). State the display name if it differs from the file name,
304+
and say plainly whether it can block a merge.
305+
243306
## Environment Variables and Secrets
244307
245308
| Secret / Variable | Used By | Purpose |
246309
|-------------------|---------|---------|
247310
| `GITHUB_TOKEN` | All workflows | GitHub API access (automatic) |
248311
| `NPM_TOKEN` | `changeset-release.yml` | npm package publishing |
249-
| `CODECOV_TOKEN` | `ci.yml` | Coverage upload to Codecov |
312+
| `CODECOV_TOKEN` | `ci.yml` (`test-coverage` job) | Coverage upload to Codecov |
313+
| `CROSS_REPO_ISSUE_TOKEN` | `cross-repo-issue-closer.yml` | Closing issues in sibling repositories. `GITHUB_TOKEN` cannot do this — it is scoped to the repository running the workflow. When absent the workflow reports instead of closing. |
250314
| `TURBO_TOKEN` | Build workflows | Turbo remote cache authentication |
251315
| `TURBO_TEAM` | Build workflows | Turbo remote cache team identifier |
252316

0 commit comments

Comments
 (0)