Skip to content

ci: dual-trigger pr-title + label workflows for fork-PR coverage#69

Merged
EricAndrechek merged 3 commits into
mainfrom
ci/dual-trigger-pr-title-label
Apr 23, 2026
Merged

ci: dual-trigger pr-title + label workflows for fork-PR coverage#69
EricAndrechek merged 3 commits into
mainfrom
ci/dual-trigger-pr-title-label

Conversation

@EricAndrechek

Copy link
Copy Markdown
Member

Summary

Adds pull_request_target alongside pull_request on pr-title.yml and label.yml. Extends fork-PR coverage (sticky comment on title failures, auto-labeling) without hitting the chicken-and-egg that blocked a straight trigger swap in #64.

How it works:

  • pull_request runs from the PR head branch — works on internal PRs with full permissions.
  • pull_request_target runs from the default branch with base-repo permissions — catches fork PRs where GITHUB_TOKEN is otherwise read-only.
  • Both fire on internal PRs. The duplicate run is harmless: the pr-title sticky comment is marker-based (idempotent update) and actions/labeler with sync-labels: true is idempotent (same label set applied twice is effectively one write).

A future follow-up PR can drop the pull_request trigger once we've confirmed pull_request_target is firing correctly in production.

First live test of project-orchestrator

Worth flagging: this is the first post-merge PR that should exercise project-orchestrator.yml end-to-end. Expected behavior:

If anything in that sequence doesn't fire, it's useful signal for orchestrator fixes.

Test plan

  • Validate check passes (both pull_request and pull_request_target runs)
  • Apply labels runs and applies area/infra, github_actions
  • project-orchestrator fires on pull_request_target, assigns Taite, adds PR to board, sets Status=Ready
  • No conflicting sticky comment behavior (just one pr-title-lint-comment marker managed)
  • Admin approval check fails until Taite approves, then succeeds
  • On merge, issue ci: switch pr-title.yml + label.yml back to pull_request_target #56 auto-closes

Related Issues

Closes #56

Adds `pull_request_target` alongside the existing `pull_request`
trigger on both `pr-title.yml` and `label.yml`. The dual-trigger
pattern sidesteps the chicken-and-egg we hit in #64 (switching
a trigger in a PR breaks that PR's own validation cycle) while
extending coverage to fork PRs:

- `pull_request` runs from the PR head branch — works on internal
  PRs with full permissions.
- `pull_request_target` runs from the default branch with base-repo
  permissions — gets fork PRs the sticky comment on title failures
  and the auto-labeling that `pull_request` can't do (GITHUB_TOKEN
  is read-only on fork PRs under `pull_request`).

Both triggers fire on internal PRs, producing a duplicate run.
That's intentional and harmless: the pr-title sticky comment is
marker-based (marker is the same, content is the same, so the
second run is a no-op update), and `actions/labeler` with
`sync-labels: true` is idempotent — same label set applied twice
is effectively one write.

A future follow-up PR can drop the `pull_request` trigger once
we've confirmed `pull_request_target` is firing correctly in
production. That simplification is tracked as a side note in the
workflow headers.

Closes #56.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 22, 2026 21:21
@gemini-code-assist

Copy link
Copy Markdown

Note

Gemini is unable to generate a summary for this pull request due to the file types involved not being currently supported.

@github-actions github-actions Bot added github_actions Pull requests that update GitHub Actions code area/infra CI, build, deploy, Docker, release labels Apr 22, 2026
@claude

claude Bot commented Apr 22, 2026

Copy link
Copy Markdown

Claude finished @EricAndrechek's task in 5m 56s —— View job


PR Review — ci: dual-trigger pr-title + label workflows for fork-PR coverage

  • Gather context (AGENTS.md, changed files, diff)
  • Review workflow correctness and security
  • Check doc-sync requirements
  • Post final review

Summary

The mechanics are correct and the prior Copilot concerns are all addressed in the current commits. Two minor issues worth noting before merge, neither a blocker.


Correctness

Concurrency group fix (label.yml:32, pr-title.yml:44): Including ${{ github.event_name }} in the group is the right fix. The original single-key group (label-${{ github.event.pull_request.number }}) would have let the pull_request run cancel the pull_request_target run (or vice versa) on the same PR event. On fork PRs, the pull_request_target run is the only one with write access, so cancelling it would have silently skipped labeling and the sticky comment — exactly the failure mode this PR exists to prevent.

issues: write permission (pr-title.yml:34): Correct and necessary. PR comments go through POST /repos/{owner}/{repo}/issues/{issue_number}/comments — PRs are issues at the REST layer. pull-requests: write does not cover that endpoint. This was Copilot's correct catch.

Orchestrator cleanup (project-orchestrator.yml): Removing pull_request_review_thread from the triggers was necessary — the Actions parser rejects it, which would have suppressed all triggers silently. The cleanup of the dead pull_request branch from the case statement and the elif chains is correct; the workflow never fires on pull_request events.

actions/labeler with pull_request_target (label.yml:40): Safe pattern. The action reads changed file paths from the GitHub API (not by checking out code), uses the labeler config from the base branch (correct), and is pinned to a full SHA (634933edcd8...) per the repo's supply-chain hardening requirement. No security concerns here.


Security — NONE

Both pr-title.yml and label.yml follow the safe pull_request_target pattern: no actions/checkout, no execution of fork-contributed code. The GITHUB_TOKEN scopes (issues: write, pull-requests: write, contents: read) are minimal. The PR title is reflected into the sticky comment body via a heredoc $PR_TITLE expansion, which is safe — bash does not recursively evaluate command substitutions in the results of parameter expansion, so a crafted title like $(id) stays as a literal string in the comment body.


Minor Issues

1. Duplicate sticky comment race (LOW, cosmetic)

When both pull_request and pull_request_target fire simultaneously on an internal PR (which happens for every push after this change), both runs hit the sticky-comment step in parallel. If the title fails and both runs query for an existing marker comment at the same moment (before either has created one), both will find EXISTING empty and both will POST a new comment — leaving two comments with the same `` marker. The next event only updates the first one found by head -n1, leaving an orphan. The PR description says the duplicate run is "harmless," but this specific race isn't addressed there.

Not a blocker — it's cosmetic and self-correcting on the next push. But worth tracking: if the orphaned-comment problem shows up in practice, the fix is to serialize the comment-step between the two runs (e.g., add a short sleep + retry-on-conflict, or check for duplicates and delete extras on the cleanup pass).

2. CHANGELOG.md entry inaccuracy (LOW, pre-existing)

CHANGELOG.md line 45 (present on main, not added by this PR's diff) says "Moved from pull_request to pull_request_target so fork PRs also get the helpful comment." The actual implementation is a dual-trigger (both kept), with pull_request dropped in a follow-up PR. The entry describes the end state, not the current intermediate state. Pre-existing, not introduced here, and will become accurate once the follow-up lands — but someone reading the changelog today will think the transition is complete when it isn't.


Copilot's Open Second-Review Comments — All Addressed

Copilot comment Status
pr-title.yml: issues: write required for sticky comment endpoint Fixed: line 34
Orchestrator: thread-resolution gap with pull_request_review_thread removed Documented in workflow comment + AGENTS.md. Copilot's suggested workaround (pull_request_review_comment: [edited]) wouldn't help — thread resolution doesn't emit that event. Current "wait for next push/check_suite" is the right pragmatic choice.
AGENTS.md: stale trigger list Fixed: AGENTS.md updated to remove pull_request_review_thread and document the gap.
Orchestrator: dead pull_request case in case statement and elif chain Fixed: both removed.

Ship it — the two issues flagged above don't warrant holding this. The CHANGELOG entry will self-correct after the follow-up PR, and the duplicate-comment race is cosmetic. Copilot's second-review concerns are fully addressed.

Copilot AI 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.

Pull request overview

Extends CI coverage to fork PRs by adding pull_request_target alongside pull_request for PR title linting and auto-labeling workflows, while aiming to preserve internal PR behavior.

Changes:

  • Add dual triggers (pull_request + pull_request_target) to PR title lint workflow to enable sticky comments on fork PRs.
  • Add dual triggers (pull_request + pull_request_target) to labeler workflow to enable auto-labeling on fork PRs.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
.github/workflows/pr-title.yml Adds pull_request_target trigger to allow base-repo permissions for fork PR sticky comments.
.github/workflows/label.yml Adds pull_request_target trigger to allow base-repo permissions for labeling fork PRs.

Comment thread .github/workflows/label.yml
Comment thread .github/workflows/pr-title.yml
…gger concurrency groups

Two real bugs uncovered while validating this PR:

1. project-orchestrator.yml was marked "Invalid workflow file" by
   GitHub Actions because of `pull_request_review_thread: [resolved,
   unresolved]`. Despite being listed in GitHub's events-that-trigger
   docs, the Actions parser rejects it ("Unexpected value"), and an
   invalid workflow suppresses ALL other triggers — which is why
   orchestrator hadn't fired on a single `pull_request_target` event
   since #65 merged (15+ runs in history, all push-event failures).
   Removed the trigger; thread resolution is detected via the next
   `synchronize` or `check_suite: completed` event that follows,
   which is fine in practice.

2. pr-title.yml and label.yml's dual-trigger setup shared a concurrency
   group keyed only on PR number (with `cancel-in-progress: true`),
   which meant the first-to-start run got cancelled by the second
   whenever both `pull_request` and `pull_request_target` fired.
   On fork PRs this could cancel the ONLY run with write permissions
   (the `pull_request_target` one), killing the sticky comment and
   auto-label paths we added the trigger for. Copilot caught this on
   #69 review. Fix: include `github.event_name` in the concurrency
   key so same-PR runs of different event types don't collide.

After this merges, orchestrator will actually fire on
pull_request_target events across all PRs (including this one via
the post-merge run) and the pr-title / label workflows get
deterministic dual-trigger coverage without cross-event cancellation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Copilot AI 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.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

Comment thread .github/workflows/pr-title.yml
Comment thread .github/workflows/project-orchestrator.yml Outdated
Comment thread .github/workflows/project-orchestrator.yml Outdated
Comment thread .github/workflows/project-orchestrator.yml Outdated
Four threads from Copilot's post-concurrency-fix review:

- pr-title.yml: added `issues: write` to the permissions block. The
  sticky-comment endpoint (`/repos/.../issues/{n}/comments`) requires
  that scope even for PR comments — `pull-requests: write` alone
  doesn't cover it. Without the fix, fork PRs would still fail to
  post the sticky under pull_request_target, defeating the dual-
  trigger rationale.
- project-orchestrator.yml: removed the dead `pull_request` branch
  from the PR-number resolver and action-selector. The workflow's
  `on:` block only has pull_request_target / pull_request_review /
  check_suite — the `pull_request` case is unreachable and misleads
  future maintainers into thinking pull_request is supported.
- AGENTS.md: fixed stale reference in the reviewer-tooling table —
  was still citing `pull_request_review_thread` as an orchestrator
  trigger despite the workflow having dropped it (parser rejection).
  Updated the row to list only the actual supported triggers with
  a footnote about the parser limitation.
- project-orchestrator.yml header comment: acknowledged Copilot's
  concern about the thread-resolution gap (no trigger fires when a
  reviewer only resolves threads without pushing). Noted that in
  practice the coder's next push / CI's next completion always
  follows thread resolution closely, with schedule/workflow_dispatch
  as escape hatches if it ever matters.

Claude's review verdict: "Ship it" with the same sticky-comment
TOCTOU race caveat acknowledged in-thread as a low-severity self-
healing cosmetic issue (documented in the PR description).

CI lint failure this round was golangci-lint couldn't fetch its
online JSON schema (context deadline — external service timeout);
logged as a data point on #70 (CI efficiency / flakiness tracker).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added the area/docs Documentation, site/, README label Apr 22, 2026
@EricAndrechek EricAndrechek requested a review from Copilot April 22, 2026 22:11

Copilot AI 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.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

@taitelee taitelee left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Before this PR, we'd receive notifications on every PR opening, sending reviewers and managers tons of notifications. The issue with this, besides the fact that there are hundreds of emails being sent, is that the PR might be fully ready yet. Github instead now waits for PRs to fully pass checks via the Project Orchestrator. It watches the PR. Once the bots (Lint, Build, Test) are all green and all conversation threads are resolved, then changes can be reviewed by developers.

Because we removed the standard CODEOWNERS protection, we needed a new way to make sure no one merges "bad" code without an admin's eyes on it.

  • A new Required Check called "Admin Approval" has been created.
  • Even if every other test passes, the "Merge" button stays locked until a reviewer or admin submits an official "Approve" review.

Technical Highlights:

  • Fork Coverage: By adding pull_request_target, we ensure that bots have the necessary "Write" permissions to post sticky comments and apply labels on external contributions which are actions that were previously blocked by GitHub's pull_request security sandbox.
  • Internal Stability: Keeping the standard pull_request trigger alongside the new one prevents a "chicken-and-egg" failure during the transition.
  • Idempotency: The duplicate runs on internal PRs are harmless as the labeler and comment-marker logic are idempotent (one write replaces or confirms the other without duplication).

@taitelee taitelee moved this from In progress to In review in WaveHouse Task Board Apr 23, 2026
@EricAndrechek EricAndrechek enabled auto-merge (squash) April 23, 2026 04:39
@EricAndrechek EricAndrechek merged commit b6590b5 into main Apr 23, 2026
26 of 32 checks passed
@EricAndrechek EricAndrechek deleted the ci/dual-trigger-pr-title-label branch April 23, 2026 04:40
@github-project-automation github-project-automation Bot moved this from In review to Done in WaveHouse Task Board Apr 23, 2026
taitelee added a commit that referenced this pull request Apr 23, 2026
Bundles all the CI-hygiene work from #70 into one PR so we can re-add
`Lint` / `Test` / `Integration Tests` to the ruleset's required status
checks once this lands.

Closes #70.

## Summary of changes

| # | Scope | Change | Files |
|---|---|---|---|
| 1 | DLQ flake root-cause | Wait for ClickHouse `/ping` + explicit
`chConn.Ping()` retry loop | `tests/integration_test.go` |
| 2 | golangci-lint flake | `verify: false` to skip `golangci-lint.run`
schema fetch | `.github/workflows/ci.yml` |
| 3 | Workflow cleanup | Drop `pull_request` trigger; revert
`event_name` concurrency suffix | `.github/workflows/pr-title.yml`,
`.github/workflows/label.yml` |
| 4 | Token efficiency | Claude review waits for required CI; skips on
red | `.github/workflows/claude-review.yml` |
| 5 | Test coverage | Add `sdk-test` (every PR) and `e2e` (non-draft)
jobs | `.github/workflows/ci.yml` |

## Scope 1 — `TestDLQIntegration` flake

Two observed failure modes
([generic](https://github.com/Wave-RF/WaveHouse/actions/runs/24803345618/job/72591993314),
[`connection reset by
peer`](https://github.com/Wave-RF/WaveHouse/actions/runs/24805317816/job/72598466908))
both pointed at the same root cause: ClickHouse opens 9000/tcp before
it's ready to accept native-protocol queries, so
`wait.ForListeningPort(\"9000/tcp\")` returned too early. The next
`chConn.Exec` could meet a half-ready server.

Fix is belt-and-suspenders:
- `wait.ForAll(wait.ForListeningPort, wait.ForHTTP(\"/ping\"))` — the
HTTP `/ping` endpoint only returns 200 once the server has finished
initializing.
- Explicit `chConn.Ping(ctx)` retry loop after `clickhouse.Open` —
`Open` is lazy and doesn't dial until the first query, so without this
the first real `Exec` would still be the test of readiness.

## Scope 2 — golangci-lint flake

[Reference
run](https://github.com/Wave-RF/WaveHouse/actions/runs/24817100832) on
main:
\`\`\`
[.golangci.yml] validate: compile schema: failing loading
\"https://golangci-lint.run/jsonschema/golangci.v2.11.jsonschema.json\":
context deadline exceeded
\`\`\`

`verify: false` skips the schema-validate pre-flight fetch. The actual
linter run is unaffected.

## Scope 3 — drop `pull_request` from dual-trigger workflows

#69 added both `pull_request` AND `pull_request_target` as a transition
pattern (the new trigger landed in the same PR, so the new event
wouldn't fire on that PR itself). Now that `pull_request_target` is on
`main` and observed firing on subsequent PRs, the `pull_request` half is
dead weight: it doubles the CI minutes on internal PRs, races with the
sticky-comment write, and only `pull_request_target` has the right
permissions on fork PRs anyway.

Concurrency-group `${{ github.event_name }}` suffix reverted with the
trigger removal — only one event fires now, so cross-event cancellation
is no longer a concern.

## Scope 4 — gate Claude review on CI

Before: Claude review ran on every `pull_request: opened` /
`synchronize` regardless of CI state. PRs with red `Lint` / `Test` would
burn OAuth tokens for a review that the human will bounce back as \"come
back when CI is green.\"

After: a first-step polls `gh pr checks --watch` until the PR's required
checks (`Check`, `Build`, `Validate`) reach a terminal state, then
short-circuits the rest of the job if any failed/cancelled/timed-out.
Subsequent re-pushes that go green re-trigger the review normally.

Out of scope: Gemini (managed App, not configurable) and Copilot
(per-seat).

## Scope 5 — SDK + E2E jobs

Gap surfaced by #63: `make test-sdk` and `make test-e2e` exist in the
Makefile but weren't wired into `ci.yml`. SDK-only PRs ran zero
TypeScript tests automatically.

- **`sdk-test`** — `npm ci && npm test` in `clients/ts`. Runs on every
PR (no path filter — a deps bump or a workflow tweak should still
exercise the suite). Fast (~30s) once node_modules is cached.
- **`e2e`** — `make test-e2e` invokes vitest in `tests/sdk`, whose
`setup.ts` globalSetup spins up the full ClickHouse + WaveHouse compose
stack. Gated on `pull_request.draft == false` so WIP pushes don't pay
the Docker-build cost. Depends on the `build` job to fail-fast if the
binary doesn't compile.

Both use SHA-pinned `actions/setup-node@v6.4.0`.

## After this merges

Re-add to `main branch protection` ruleset 15353356:

\`\`\`
required_status_checks: + \"Lint\" + \"Test\" + \"Integration Tests\"
\`\`\`

(Was temporarily removed pre-#66 when main's CI was failing. Reinstating
after this PR's `verify: false` + DLQ flake fix have at least 3 clean
runs on main.)

## Test plan

- [ ] CI run on this PR — verify all jobs green (incl. new `SDK Tests`
and `E2E Tests`)
- [ ] Open a follow-up draft PR after merge to verify
`claude-review.yml` skips when CI is intentionally red, then runs after
a fix push
- [ ] Confirm `pr-title.yml` + `label.yml` still fire (only one run per
PR now, not two)
- [ ] Re-add Lint/Test/Integration Tests to required ruleset checks via
`gh api PUT`

---

*— Posted by Claude Code on behalf of @EricAndrechek*
---
### Added late

Scope 6: **Fix `project-orchestrator.yml` `gh api --jq` bug.** Surfaced
when this very PR went `ready_for_review` — the orchestrator assigned
Taite (preceding step passed) but then crashed on the board add/edit
with `accepts 1 arg(s), received 4`. `gh api --jq` doesn't forward
`--arg` to the embedded jq. Fix: pipe JSON to a standalone `jq` at all
six callsites. Existing bug on main that nothing exercised because the
`reeval` path only runs when a bot-clean non-Dependabot PR goes
ready-for-review — none had since #65 shipped.

*— Updated by Claude Code on behalf of @EricAndrechek*

---------

Co-authored-by: Taite Lee <113070390+taitelee@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/docs Documentation, site/, README area/infra CI, build, deploy, Docker, release github_actions Pull requests that update GitHub Actions code

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

ci: switch pr-title.yml + label.yml back to pull_request_target

3 participants