Skip to content

fix(ci): run the test gate on stacked child PRs, and label PRs whose title carries no type - #979

Merged
lidge-jun merged 3 commits into
devfrom
codex/stacked-pr-ci
Aug 4, 2026
Merged

fix(ci): run the test gate on stacked child PRs, and label PRs whose title carries no type#979
lidge-jun merged 3 commits into
devfrom
codex/stacked-pr-ci

Conversation

@lidge-jun

@lidge-jun lidge-jun commented Aug 4, 2026

Copy link
Copy Markdown
Owner

Summary

Two defects that shared one symptom report on the #951-#955 stack, and only one of them was about stacked PRs.

  • ci.yml never ran for stacked children. The pull_request trigger filtered on branches: [main, dev], which GitHub matches against the base ref. A stacked child's base is another open PR's head branch, so the workflow was never queued: the stack merged with enforce-target, label, and react-doctor as its only check-runs, and no test job at all, for 24 changed files under src/.
  • The labeler skipped the whole stack. stack 1/5: fails the conventional-commit regex (the 1/5 sits between the word and the colon), reaches the sentence-case fallback, which extracts stack — a word with no PREFIX_TO_LABEL entry. The sync returns {skip: true}, a skip is not a failure, so the label check stayed green while all four PRs carried no type label.

This contradicted the repository's own design: AGENTS.md calls stacked children an intentional review workflow, and enforce-target already implements the exemption. The gate accepted them; the test gate never saw them.

What changed

CI trigger. The branches: filter is removed rather than extended. An allowlist cannot express "base is another PR's head": open PR head refs today are codex/ (14) but also fix/ (4), feat/ (3), agent/ (3), split/, ingw/, and any of them can become a stacked base. A codex/** glob — the first draft — would have fixed the maintainer's own stacks and left contributor stacks silently unverified, which is the worse half of the bug.

paths: is untouched and remains the real scope gate, so docs-only and devlog/-only PRs still queue nothing. The precedent is already in-repo: issue-quality-tests.yml runs pull_request with paths: and no branches:. Widening is safe here specifically because this workflow is pull_request (not pull_request_target), declares contents: read, and reads no secrets — the same change would not be safe on enforce-pr-target.yml. push: stays pinned to the integration lines.

Labeler. A title that carries no type falls back to the PR's commits, which stay conventional even when the title does not. Adding stack to PREFIX_TO_LABEL was rejected: a stack PR can carry fixes, features, or docs, so any fixed mapping would be a lie.

The unanimity rule this started with was falsified by running it on real data — #952 gives {bug: 1} and labels, but #955 gives {bug: 4, chore: 1} and would abstain, despite being four fix(codex): commits plus one test(codex):. So chore is treated as supporting, not competing: test:/ci:/chore:/style:/refactor:/build: all map to it and none says what a PR is for, so it drops out when a non-chore type is present. An all-chore PR still gets chore; a genuine fix:-plus-feat: mix stays unlabeled rather than guessed.

The title stays authoritative when it classifies, and the existing human-override gate still runs first. No permission change — pulls.listCommits is covered by the existing contents: read.

The test that was asserting the bug

"PR checks reach every branch the target gate accepts" pinned both workflows to ["dev", "main"] and asserted the trigger key set as ["branches", "paths"] — which is how this survived. Its intent is exactly this fix (commit 5229717b1: "an accepted PR is also a checked one"), so it is extended to the gate's stacked exemption rather than deleted. service-lifecycle.yml keeps its branch list; it gates the release service path, not review.

Test plan

  • bun run test — 8004 pass / 0 fail / 8 skip across 525 files.
  • bun test tests/ci-workflows.test.ts — 83 pass.
  • node --test .github/scripts/pr-labeler.test.cjs — 24 pass (8 new).
  • bun run typecheck, bun run privacy:scan — pass.
  • Parsed ci.yml at verification: pull_request keys ["paths"], branches undefined, push.branches unchanged [main, preview, dev], both path lists still 17 entries, permissions: {contents: read}.
  • Workflow script syntax checked async-wrapped, as github-script actually runs it.

Driven red, all three:

Note on live effect

enforce-target and label run on pull_request_target, which GitHub loads from the default branch. The labeler half of this does not change live behavior until promotion to main; the CI trigger half takes effect as soon as it is on the targeted base. The docs now say so — it was previously only a code comment.

No src/ changes. Planning notes in devlog/_plan/260804_stacked_pr_ci/.

Summary by CodeRabbit

  • New Features

    • Improved automatic PR type labeling by using commit messages when titles lack recognizable types.
    • Preserved human-applied labels and avoided labeling ambiguous or conflicting changes.
    • CI now runs for stacked pull requests regardless of their base branch.
  • Documentation

    • Updated contributor guidance for PR labeling, mixed commit types, and workflow behavior.
  • Tests

    • Added coverage for commit-based labeling, stacked PRs, overrides, and workflow triggers.

Two defects share one symptom report, and only one of them is about stacked
PRs.

`ci.yml` filters `pull_request` on `branches: [main, dev]`, which GitHub
matches against the BASE ref. A stacked child's base is another open PR's head,
so the workflow is never queued: the #951-#955 stack carried 24 changed files
under `src/` and 748 added lines with `enforce-target`, `label`, and
`react-doctor` as its only check-runs. AGENTS.md calls stacked children an
intentional review workflow and `enforce-target` already exempts them from the
wrong-base gate, so the test gate is the one place that never got the memo.

The labeler is a separate bug the stack happened to expose. `stack 1/5:` fails
the conventional-commit regex — the `1/5` sits between the word and the colon —
and the sentence-case fallback then extracts `stack`, which has no entry in
PREFIX_TO_LABEL. The sync skips, a skip is not a failure, and the `label` check
stays green while all four PRs carry no type label.

The audit killed the first design. It chose `branches: [main, dev, "codex/**"]`
on the theory that stacked bases live in the `codex/` namespace; listing real
open PR head refs falsified that (codex/ 14, but also fix/ 4, feat/ 3, agent/ 3,
split/, ingw/). Any of those can become a stacked base, and a contributor stack
is precisely the case that most needs CI — the allowlist would have fixed the
maintainer half and left the contributor half silently unverified. So the filter
goes and the untouched `paths:` stays the scope gate, which is already how
issue-quality-tests.yml is written.

The audit also caught 020 asserting that the conventional regex matches
`stack 1/5:`. It does not; both docs now name the sentence-case path.

Docs only — no workflow or script changes in this commit.
`ci.yml` filtered `pull_request` on `branches: [main, dev]`, and GitHub matches
that against the BASE ref. A stacked child's base is another open PR's head
branch, so the workflow was never queued: the #951-#955 stack merged with
`enforce-target`, `label`, and `react-doctor` as its only check-runs, and no
test job at all, for 24 changed files under `src/`.

That contradicts the repository's own design. AGENTS.md calls stacked children
an intentional review workflow, and `enforce-target` implements the exemption by
listing open PRs and matching `other.head.ref === pr.base.ref`. The gate accepts
them; the check workflow never saw them.

The filter is removed rather than extended. An allowlist cannot express "base is
another PR's head": open PR head refs today are `codex/` (14) but also `fix/`
(4), `feat/` (3), `agent/` (3), `split/`, and `ingw/`, and any of them can become
a stacked base. A `codex/**` glob — the first draft of this change — would have
fixed the maintainer's own stacks and left contributor stacks silently
unverified, which is the worse half of the bug.

`paths:` is untouched and is the real scope gate, so docs-only and devlog-only
PRs still queue nothing. The precedent is already in this repository:
`issue-quality-tests.yml` runs `pull_request` with `paths:` and no `branches:`.
Widening is safe here specifically because this workflow is `pull_request` (not
`pull_request_target`), declares `contents: read`, and reads no secrets — the
same change would not be safe on `enforce-pr-target.yml` or `pr-labeler.yml`,
which are untouched. `push:` stays pinned to the integration lines.

The existing "PR checks reach every branch the target gate accepts" test
asserted the opposite contract, which is how this survived: it pinned both
workflows to `["dev", "main"]` and asserted the trigger key set as
`["branches", "paths"]`. Its INTENT is exactly this fix — commit 5229717 wrote
it so "an accepted PR is also a checked one" — so it is extended to the gate's
stacked exemption rather than deleted: `ci.yml` is now asserted to carry no base
filter, `service-lifecycle.yml` keeps its list (it gates the release service
path, not review), and the gate itself is pinned as base-unfiltered.

The block's type annotation did not even model `branches` on the PR trigger, so
the second assertion adds it and pins its absence.

Driven red: restoring `branches: [main, dev]` fails exactly that one test.

Verified: bun test tests/ci-workflows.test.ts 83 pass; typecheck and
privacy:scan pass.
All four PRs of the #951-#955 stack carried no type label while the `label`
check reported success. `stack 1/5:` fails the conventional-commit regex — the
`1/5` sits between the word and the colon — and then reaches the sentence-case
fallback, which extracts `stack`. That has no entry in PREFIX_TO_LABEL, so
`planTypeLabelSync` returns `{skip: true, reason: "no-prefix"}`, and a skip is
not a failure. The check stays green and nothing is labeled.

This is not a stacked-PR bug; the labeler has no branch filter and ran fine on
all four. It is a title-vocabulary bug that the stack happened to expose: any
title with an unrecognised prefix word is silently unlabeled.

The commits underneath are conventional even when the title is not, so they
answer what the title cannot. Adding `stack` to PREFIX_TO_LABEL was rejected —
a stack PR can carry fixes, features, or docs, so any fixed mapping would be a
lie.

The unanimity rule this started with was falsified by running it on the real
data. #952 gives `{bug: 1}` and labels, but #955 gives `{bug: 4, chore: 1}` and
would abstain — four `fix(codex):` commits plus one `test(codex):`, which is a
bug fix by any honest reading. A rule that abstains there abstains on most real
PRs, since nearly every substantial change carries a test or chore commit.

So `chore` is supporting, not competing: `test:`, `ci:`, `chore:`, `style:`,
`refactor:`, and `build:` all map to it, and none of them says what a PR is FOR.
It drops out of the tally when a non-chore type is present. An all-chore PR
still gets `chore`, and a genuine `fix:`-plus-`feat:` mix is still left
unlabeled rather than guessed.

The title stays authoritative when it classifies, so a well-formed title is
never overridden by what happens to be committed under it, and the existing
human-override gate still runs first. No permission change: `pulls.listCommits`
is covered by the existing `contents: read`.

Both rules were driven red: removing the fallback fails the stack-PR test and
nothing else; removing the chore-demotion fails the #955-shape test and nothing
else.

The docs now also state the promotion model, which was only a code comment
before: `enforce-target` and `label` run on `pull_request_target` and are loaded
from the default branch, so merging either to `dev` does not change live
behavior until promotion to `main`.

Verified: node --test .github/scripts/pr-labeler.test.cjs 24 pass; bun test
tests/ci-workflows.test.ts 83 pass; typecheck and privacy:scan pass.
@github-actions github-actions Bot added the bug Something isn't working label Aug 4, 2026
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

CI pull-request triggers no longer restrict base branches to main and dev. PR type labeling now falls back to commit messages when titles are unrecognized. Human overrides and recognizable title types remain authoritative. Tests and documentation cover the new behavior.

Changes

Stacked PR CI and labeling

Layer / File(s) Summary
Enable CI for stacked pull requests
.github/workflows/ci.yml, tests/ci-workflows.test.ts, devlog/_plan/260804_stacked_pr_ci/000_scope.md, devlog/_plan/260804_stacked_pr_ci/010_ci_trigger.md
The CI workflow removes the pull-request base-branch filter and retains path filtering. Tests verify the trigger configuration.
Classify PR types from commits
.github/scripts/pr-labeler.cjs, .github/scripts/pr-labeler.test.cjs, devlog/_plan/260804_stacked_pr_ci/000_scope.md, devlog/_plan/260804_stacked_pr_ci/020_labeler_and_docs.md
detectTypeLabelFromCommits derives a type from commit subjects. Title results and human labels retain precedence. Tests cover supporting chore commits, conflicts, invalid input, and stack titles.
Wire commit retrieval and document behavior
.github/workflows/pr-labeler.yml, docs-site/src/content/docs/contributing/pr-quality.md, devlog/_plan/260804_stacked_pr_ci/020_labeler_and_docs.md
The labeler retrieves up to 100 PR commits and passes their messages to planTypeLabelSync. Documentation describes trigger scope, fallback labeling, mixed types, and workflow timing.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

Suggested labels: chore

Suggested reviewers: wibias, ingwannu

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes both primary changes: CI support for stacked child PRs and PR type labeling when titles lack a recognized type.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/stacked-pr-ci

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@devlog/_plan/260804_stacked_pr_ci/000_scope.md`:
- Around line 80-83: Add the text language identifier to each fenced
literal-output block: update the opening fences at
devlog/_plan/260804_stacked_pr_ci/000_scope.md lines 80-83,
devlog/_plan/260804_stacked_pr_ci/010_ci_trigger.md lines 31-33, and
devlog/_plan/260804_stacked_pr_ci/020_labeler_and_docs.md lines 15-18, 24-27,
and 56-59. No other content changes are needed.

In `@devlog/_plan/260804_stacked_pr_ci/020_labeler_and_docs.md`:
- Around line 61-64: Update the reference at the start of the paragraph in the
PR classification discussion to use “PR `#955`” instead of “#955”, preserving the
rest of the text unchanged so the Markdown heading rule is satisfied.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: a53ee9c9-3c4b-4324-b8a7-97ed55605225

📥 Commits

Reviewing files that changed from the base of the PR and between e3ba14f and 243197a.

📒 Files selected for processing (9)
  • .github/scripts/pr-labeler.cjs
  • .github/scripts/pr-labeler.test.cjs
  • .github/workflows/ci.yml
  • .github/workflows/pr-labeler.yml
  • devlog/_plan/260804_stacked_pr_ci/000_scope.md
  • devlog/_plan/260804_stacked_pr_ci/010_ci_trigger.md
  • devlog/_plan/260804_stacked_pr_ci/020_labeler_and_docs.md
  • docs-site/src/content/docs/contributing/pr-quality.md
  • tests/ci-workflows.test.ts

Comment on lines +80 to +83
```
planTypeLabelSync({title: "stack 1/5: triage the open issue surface..."})
-> { skip: true, reason: "no-prefix" }
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add languages to all fenced literal-output blocks.

markdownlint-cli2 reports MD040 at each site. Add text after each opening fence.

  • devlog/_plan/260804_stacked_pr_ci/000_scope.md#L80-L83: change the opening fence to ````text``.
  • devlog/_plan/260804_stacked_pr_ci/010_ci_trigger.md#L31-L33: change the opening fence to ````text``.
  • devlog/_plan/260804_stacked_pr_ci/020_labeler_and_docs.md#L15-L18: change the opening fence to ````text``.
  • devlog/_plan/260804_stacked_pr_ci/020_labeler_and_docs.md#L24-L27: change the opening fence to ````text``.
  • devlog/_plan/260804_stacked_pr_ci/020_labeler_and_docs.md#L56-L59: change the opening fence to ````text``.
🧰 Tools
🪛 markdownlint-cli2 (0.23.1)

[warning] 80-80: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

📍 Affects 3 files
  • devlog/_plan/260804_stacked_pr_ci/000_scope.md#L80-L83 (this comment)
  • devlog/_plan/260804_stacked_pr_ci/010_ci_trigger.md#L31-L33
  • devlog/_plan/260804_stacked_pr_ci/020_labeler_and_docs.md#L15-L18
  • devlog/_plan/260804_stacked_pr_ci/020_labeler_and_docs.md#L24-L27
  • devlog/_plan/260804_stacked_pr_ci/020_labeler_and_docs.md#L56-L59
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@devlog/_plan/260804_stacked_pr_ci/000_scope.md` around lines 80 - 83, Add the
text language identifier to each fenced literal-output block: update the opening
fences at devlog/_plan/260804_stacked_pr_ci/000_scope.md lines 80-83,
devlog/_plan/260804_stacked_pr_ci/010_ci_trigger.md lines 31-33, and
devlog/_plan/260804_stacked_pr_ci/020_labeler_and_docs.md lines 15-18, 24-27,
and 56-59. No other content changes are needed.

Source: Linters/SAST tools

Comment on lines +61 to +64
#955 is four `fix(codex):` commits plus one `test(codex):`. It is a bug-fix PR
by any honest reading, and a rule that abstains there is a rule that abstains on
most real PRs — almost every substantial change carries a test or chore commit
alongside its feature or fix.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Prevent #955 from becoming malformed heading syntax.

Line 61 starts with #955. markdownlint-cli2 reports MD018 for this form. Prefix the reference with PR .

🧰 Tools
🪛 markdownlint-cli2 (0.23.1)

[warning] 61-61: No space after hash on atx style heading

(MD018, no-missing-space-atx)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@devlog/_plan/260804_stacked_pr_ci/020_labeler_and_docs.md` around lines 61 -
64, Update the reference at the start of the paragraph in the PR classification
discussion to use “PR `#955`” instead of “#955”, preserving the rest of the text
unchanged so the Markdown heading rule is satisfied.

Source: Linters/SAST tools

@lidge-jun
lidge-jun merged commit 9557097 into dev Aug 4, 2026
22 checks passed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 243197ae6a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread .github/workflows/ci.yml
Comment on lines +5 to +8
# No base-branch filter on purpose. GitHub matches `branches:` against the
# BASE ref, so `[main, dev]` silently excluded stacked child PRs — whose
# base is another open PR's head branch, an intentional review workflow per
# AGENTS.md that `enforce-target` already exempts from the wrong-base gate.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Update the workflow map for the broadened trigger

Removing the base-branch filter makes this workflow run for qualifying PRs against any base, but structure/06_docs-and-release.md:43 still says Cross-platform CI runs only for pull requests to main or dev. This leaves the repository's maintainer workflow map contradicting the actual trigger and the newly updated public guide; update that entry to describe the unfiltered pull_request trigger and its path filtering.

Useful? React with 👍 / 👎.

for (const [path, expectedKeys] of [
// No `branches`: the stacked-base exemption has no enumerable branch list.
[".github/workflows/ci.yml", ["paths"]],
[".github/workflows/service-lifecycle.yml", ["branches", "paths"]],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Run service lifecycle checks for stacked children

For a stacked child submitted from a fork, retaining branches: [main, dev] on service-lifecycle.yml means changes to src/service.ts, src/cli/index.ts, or the other service paths receive none of the Linux, macOS, and Windows lifecycle smoke tests that the same change would receive when targeting dev; unlike an in-repository stack, the fork's push cannot produce an upstream push-triggered run. Since enforce-target accepts this stacked shape, remove the service workflow's base filter or otherwise ensure its three-platform PR coverage reaches every accepted stacked base.

AGENTS.md reference: .github/AGENTS.md:L18-L18

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant