Skip to content

feat(scripts): gate triage-triggered code/fix PRs behind write+ approval - #523

Draft
waynesun09 wants to merge 1 commit into
mainfrom
fix/5687-triage-code-fix-merge-gate
Draft

feat(scripts): gate triage-triggered code/fix PRs behind write+ approval#523
waynesun09 wants to merge 1 commit into
mainfrom
fix/5687-triage-code-fix-merge-gate

Conversation

@waynesun09

@waynesun09 waynesun09 commented Jul 29, 2026

Copy link
Copy Markdown
Member

Summary

fullsend-ai/fullsend#5687 proposes letting the GitHub triage role trigger /fs-code and /fs-fix (currently write+ only), on the condition that the resulting PR is explicitly gated behind write+ approval before merge — independent of whatever branch protection a given repo happens to have configured. That fullsend-side authorization change is a companion PR; this PR implements the gate.

Changes

  • scripts/lib/write-approval-gate.lib.sh: apply_write_approval_gate_if_needed(target_pr) applies a needs-write-approval label when TRIGGER_ROLE normalizes (case-insensitive, trimmed) to "triage". Warns (without gating) on any other unrecognized non-empty value. Wired into post-code.src.sh (before ready-for-review, so downstream automation never races ahead of it) and post-fix.src.sh.
  • skills/merge-queue/scripts/lib/write-approval-check.lib.sh: the enforcement, used by both enqueue-pr.sh (the documented primary enqueue entry point) and await-and-enqueue.sh (its polling wrapper) — see "Design history" below for why both needed it.
  • Docs: needs-write-approval and TRIGGER_ROLE documented in docs/code.md/docs/fix.md; skills/merge-queue/SKILL.md documents the new refusal behavior.
  • Tests: gate-predicate tests (including normalization) in post-code-test.sh/post-fix-test.sh; a new write-approval-check-test.sh exercising the actual jq filters used for timeline-based detection, commit-pinning, and bot-exclusion.

Design history — this took two review rounds to get right

Round 1 shipped a label-only design. Review (3 agents: Claude ×2, Grok) found it didn't work: GitHub's triage role includes repo-wide label management (removable by the constrained user), and nothing read the label at all — post-review.sh never submits a real gh pr review (confirmed by grep), so reviewDecision=APPROVED can only come from an actual review by some collaborator, and GitHub doesn't distinguish reviewer permission level. Since the PR is bot-authored (not authored by the triage trigger), that user could approve their own triggered PR themselves.

Round 2 fixed enforcement but gated it on the label's current presence — reintroducing the exact removability problem one layer down — and only wired it into await-and-enqueue.sh, missing that enqueue-pr.sh is the documented primary path and bypasses it entirely. Also missing: nothing pinned an approval to the PR's current head commit, so a stale approval survives a later /fs-fix push.

What's actually shipped now:

  • Whether a PR ever required write-approval is derived from the immutable issue-events timeline (a labeled event), not current label state.
  • The approving reviewer's permission is re-checked live against the collaborator-permission API on every check, not trusted from a point-in-time value.
  • Approval must be on the PR's current head commit — a later push invalidates a prior approval for this gate's purposes, independent of whether the repo's branch protection has "dismiss stale reviews" enabled.
  • Bot/App reviewers never count as write+ approvers.
  • Both enqueue-pr.sh and await-and-enqueue.sh enforce this independently (the latter also delegates to the former at the end, so there's no path that skips it).

Known residual limitation (documented in docs/code.md/docs/fix.md): a write+ collaborator merging directly via GitHub's native UI or gh pr merge bypasses this entirely — it's unaware of the label. This PR only gates the merge-queue skill's own scripts, not GitHub itself.

Test plan

  • make check-bundle — bundled scripts match make script-build output
  • bash scripts/post-code-test.sh / --bundled, bash scripts/post-fix-test.sh / --bundled — all pass
  • bash skills/merge-queue/scripts/write-approval-check-test.sh — all pass (timeline-immutability, commit-pinning, bot-exclusion, latest-review-wins dedup)
  • pre-commit run — shellcheck clean across all changed/new scripts

Related

@waynesun09
waynesun09 requested a review from a team as a code owner July 29, 2026 12:35
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Gate triage-triggered code/fix PRs behind needs-write-approval label

✨ Enhancement 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Add a write-approval gate that labels triage-triggered PRs as needs-write-approval.
• Wire the gate into post-code and post-fix for both new and existing PR flows.
• Extend script tests to assert gate bundling and role-predicate behavior.
Diagram

graph TD
trigger["Dispatch (TRIGGER_ROLE)"] --> postcode(["post-code"]) --> gate["write-approval gate"] --> gh["gh CLI"] --> pr["PR label: needs-write-approval"]
trigger --> postfix(["post-fix"]) --> gate
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Enforce a required status check for triage-triggered PRs
  • ➕ Hard block: cannot merge until the check is cleared by write+ automation
  • ➕ More reliable than human/tooling convention around labels
  • ➖ Requires additional CI/check plumbing and a clearing mechanism
  • ➖ May be harder to keep independent of varying branch protection setups
2. Auto-request review from a write+ team when TRIGGER_ROLE=triage
  • ➕ Directly drives the desired approval behavior in GitHub UX
  • ➕ Still works even if label-based tooling is not used
  • ➖ Does not guarantee merge is blocked without branch protection
  • ➖ Requires managing team/ownership mapping across repos
3. Put a prominent gate notice in the PR body/title instead of a label
  • ➕ No dependency on labels existing or gh label permissions
  • ➕ Visible in the PR content even if labels are missed
  • ➖ Easier to ignore/overwrite; harder for merge tooling to query reliably
  • ➖ Less standardized than a dedicated label for automation

Recommendation: Keep the label-based gate as implemented. It is lightweight, repo-agnostic, and integrates cleanly with existing manual merge workflows while remaining best-effort (no script failure if labeling fails). If enforcement needs to be stronger later, consider adding a required status check or auto-requesting write+ reviews, but those add operational complexity and/or reintroduce reliance on repo-specific branch protection configuration.

Files changed (7) +269 / -0

Enhancement (5) +185 / -0
write-approval-gate.lib.shAdd shared triage-trigger merge-gate helper +51/-0

Add shared triage-trigger merge-gate helper

• Introduces apply_write_approval_gate_if_needed(target_pr), which labels PRs with needs-write-approval only when TRIGGER_ROLE is exactly "triage". Uses best-effort gh calls (label create + pr edit) and emits warnings without failing callers.

scripts/lib/write-approval-gate.lib.sh

post-code.src.shSource and invoke write-approval gate in post-code source +8/-0

Source and invoke write-approval gate in post-code source

• Documents TRIGGER_ROLE semantics and sources the new write-approval gate library. Calls the gate for both the existing-PR early-exit path and the newly-created PR path.

scripts/post-code.src.sh

post-code.shRegenerate bundled post-code with gate + call sites +59/-0

Regenerate bundled post-code with gate + call sites

• Bundles write-approval-gate.lib.sh into the generated script and documents TRIGGER_ROLE in the header. Ensures apply_write_approval_gate_if_needed runs for existing PR reuse and after new PR creation.

scripts/post-code.sh

post-fix.src.shSource and invoke write-approval gate in post-fix source +8/-0

Source and invoke write-approval gate in post-fix source

• Documents TRIGGER_ROLE behavior, sources the gate library, and applies the gate after the iteration-cap/needs-human labeling step so the PR is marked when the trigger role is triage.

scripts/post-fix.src.sh

post-fix.shRegenerate bundled post-fix with gate + call site +59/-0

Regenerate bundled post-fix with gate + call site

• Bundles the new gate library into the generated script and documents TRIGGER_ROLE in the header. Invokes apply_write_approval_gate_if_needed on the target PR number near the end of the workflow.

scripts/post-fix.sh

Tests (2) +84 / -0
post-code-test.shAdd bundling and predicate tests for write-approval gate +42/-0

Add bundling and predicate tests for write-approval gate

• Adds a static assertion that the bundled post-code script contains apply_write_approval_gate_if_needed. Adds decision-logic tests validating the gate applies only for TRIGGER_ROLE="triage" and is skipped otherwise.

scripts/post-code-test.sh

post-fix-test.shAdd bundling and predicate tests for write-approval gate +42/-0

Add bundling and predicate tests for write-approval gate

• Adds a static assertion that the bundled post-fix script contains apply_write_approval_gate_if_needed. Adds decision-logic tests covering triage/write/unset/garbage TRIGGER_ROLE values to match existing test style (no gh mocking).

scripts/post-fix-test.sh

@fullsend-ai-review

fullsend-ai-review Bot commented Jul 29, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 12:37 PM UTC · Completed 12:54 PM UTC
Commit: 0aed7e1 · View workflow run →

@qodo-code-review

qodo-code-review Bot commented Jul 29, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📜 Skill insights (3)

Context used
✅ Compliance rules (platform): 55 rules
✅ Skills: 4 invoked
  code-review
  code-implementation
  pr-review
  docs-review

Grey Divider


Action required

1. TRIGGER_ROLE malformed skips gate 📜 Skill insight ⛨ Security
Description
apply_write_approval_gate_if_needed treats any TRIGGER_ROLE value other than exactly triage as
a no-op, so a malformed/unexpected value bypasses the gate instead of failing closed. This can
result in triage-triggered PRs missing the required merge gate if configuration is absent or
malformed.
Code

scripts/lib/write-approval-gate.lib.sh[R40-42]

+  if [[ "${TRIGGER_ROLE:-}" != "triage" ]]; then
+    return 0
+  fi
Relevance

●●● Strong

Team previously accepted hardening malformed config to avoid fail-open gates (PR #10).

PR-#10

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The checklist prohibits fail-open behavior when a gate’s controlling config is absent, empty, or
malformed. The new code explicitly no-ops for any TRIGGER_ROLE not equal to triage, which
includes malformed values.

scripts/lib/write-approval-gate.lib.sh[40-42]
Skill: pr-review

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The gate predicate `[[ "${TRIGGER_ROLE:-}" != "triage" ]] && return 0` fails open for malformed/unexpected `TRIGGER_ROLE` values.

## Issue Context
For auth/validation gates, absence/malformed config must not broaden access. Here, an unexpected value (e.g. typo) should not silently disable the gate.

## Fix Focus Areas
- scripts/lib/write-approval-gate.lib.sh[37-43]
- scripts/post-code-test.sh[1187-1214]
- scripts/post-fix-test.sh[403-430]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Gate label application fail-open 📜 Skill insight ☼ Reliability
Description
When TRIGGER_ROLE=triage, apply_write_approval_gate_if_needed treats gh pr edit failures as
warnings and returns success, so post-code/post-fix can continue and a triage-triggered PR may
proceed without the required needs-write-approval merge gate label. This creates a fail-open
authorization path where labeling errors (permissions/auth/network/API) can silently bypass the
intended enforcement signal.
Code

scripts/lib/write-approval-gate.lib.sh[R44-50]

+  echo "Trigger role is 'triage' — applying needs-write-approval gate to PR #${target_pr}"
+  gh label create "needs-write-approval" --repo "${REPO_FULL_NAME}" \
+    --description "Triggered by a triage-role user; needs write+ approval before merge" \
+    --color "B60205" --force 2>/dev/null || true
+  gh pr edit "${target_pr}" --repo "${REPO_FULL_NAME}" \
+    --add-label "needs-write-approval" 2>/dev/null || \
+    _write_approval_gate_warn "Failed to apply needs-write-approval label to PR #${target_pr}"
Relevance

●● Moderate

Repo often tolerates best-effort gh pr edit failures (PR #284), but has fail-closed precedent for
gates (PR #415).

PR-#284
PR-#415

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The checklist calls for guard mechanisms to explicitly handle failure paths rather than proceeding
as if a gate succeeded, yet the new gate implementation explicitly suppresses/ignores gh errors
(e.g., warning-only behavior and stderr swallowing) and returns success even when gh pr edit fails
to add needs-write-approval. Because both post-code and post-fix invoke this helper and then
continue/exit successfully regardless of labeling success, a failure to apply the label leaves the
PR unlabeled with no enforced marker, despite the gate being described as mandatory/load-bearing for
triage-triggered PRs.

scripts/lib/write-approval-gate.lib.sh[44-50]
scripts/lib/write-approval-gate.lib.sh[8-13]
scripts/lib/write-approval-gate.lib.sh[29-50]
scripts/post-code.src.sh[521-533]
scripts/post-fix.src.sh[436-456]
Skill: pr-review

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`apply_write_approval_gate_if_needed` is intended to enforce an explicit merge-authorization gate for triage-triggered PRs by applying the `needs-write-approval` label, but it currently fails open: when `gh pr edit` cannot apply the label (permissions/auth/network/API errors, invalid PR input, missing label), the helper suppresses the error and returns success, allowing post-code/post-fix to complete and leaving a triage PR potentially mergeable without the required gate marker.

## Issue Context
The gate is described as “informational-but-load-bearing” and as a mandatory explicit merge gate for triage-triggered PRs; therefore, failure to apply the label must be treated as a hard failure (or otherwise enforced) when `TRIGGER_ROLE=triage`. Today the behavior is effectively best-effort (warning-only, `2>/dev/null`, `|| true` style suppression), and both post-code and post-fix call the helper and then continue/exit 0, meaning labeling failure is not surfaced or enforced.

## Fix Focus Areas
- scripts/lib/write-approval-gate.lib.sh[29-51]
- scripts/lib/write-approval-gate.lib.sh[37-51]
- scripts/post-code.src.sh[521-533]
- scripts/post-code.src.sh[522-532]
- scripts/post-fix.src.sh[436-456]
- scripts/post-fix.src.sh[446-456]
- scripts/post-code-test.sh[1187-1214]
- scripts/post-fix-test.sh[403-430]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. Protected scripts/ files modified 📜 Skill insight § Compliance
Description
This PR modifies multiple files under the protected scripts/ path, which requires explicit human
review and must not be auto-approved. Ensure governance/infrastructure review expectations are
followed for these changes.
Code

scripts/post-code.src.sh[R50-54]

source "${SCRIPT_DIR_POST}/lib/gitleaks-install.lib.sh"
# shellcheck source=lib/pr-assignee.lib.sh
source "${SCRIPT_DIR_POST}/lib/pr-assignee.lib.sh"
+# shellcheck source=lib/write-approval-gate.lib.sh
+source "${SCRIPT_DIR_POST}/lib/write-approval-gate.lib.sh"
Relevance

●●● Strong

Protected-path findings are enforced historically; schema blocks approval when protected paths
modified (PR #303).

PR-#303

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The checklist flags any modifications under protected governance/infrastructure paths (including
scripts/) as requiring a compliance finding and heightened review. The diff includes changes under
scripts/, such as sourcing and applying the new gate library.

scripts/post-code.src.sh[50-54]
Skill: pr-review


Grey Divider

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment on lines +44 to +50
echo "Trigger role is 'triage' — applying needs-write-approval gate to PR #${target_pr}"
gh label create "needs-write-approval" --repo "${REPO_FULL_NAME}" \
--description "Triggered by a triage-role user; needs write+ approval before merge" \
--color "B60205" --force 2>/dev/null || true
gh pr edit "${target_pr}" --repo "${REPO_FULL_NAME}" \
--add-label "needs-write-approval" 2>/dev/null || \
_write_approval_gate_warn "Failed to apply needs-write-approval label to PR #${target_pr}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. Gate label application fail-open 📜 Skill insight ☼ Reliability

When TRIGGER_ROLE=triage, apply_write_approval_gate_if_needed treats gh pr edit failures as
warnings and returns success, so post-code/post-fix can continue and a triage-triggered PR may
proceed without the required needs-write-approval merge gate label. This creates a fail-open
authorization path where labeling errors (permissions/auth/network/API) can silently bypass the
intended enforcement signal.
Agent Prompt
## Issue description
`apply_write_approval_gate_if_needed` is intended to enforce an explicit merge-authorization gate for triage-triggered PRs by applying the `needs-write-approval` label, but it currently fails open: when `gh pr edit` cannot apply the label (permissions/auth/network/API errors, invalid PR input, missing label), the helper suppresses the error and returns success, allowing post-code/post-fix to complete and leaving a triage PR potentially mergeable without the required gate marker.

## Issue Context
The gate is described as “informational-but-load-bearing” and as a mandatory explicit merge gate for triage-triggered PRs; therefore, failure to apply the label must be treated as a hard failure (or otherwise enforced) when `TRIGGER_ROLE=triage`. Today the behavior is effectively best-effort (warning-only, `2>/dev/null`, `|| true` style suppression), and both post-code and post-fix call the helper and then continue/exit 0, meaning labeling failure is not surfaced or enforced.

## Fix Focus Areas
- scripts/lib/write-approval-gate.lib.sh[29-51]
- scripts/lib/write-approval-gate.lib.sh[37-51]
- scripts/post-code.src.sh[521-533]
- scripts/post-code.src.sh[522-532]
- scripts/post-fix.src.sh[436-456]
- scripts/post-fix.src.sh[446-456]
- scripts/post-code-test.sh[1187-1214]
- scripts/post-fix-test.sh[403-430]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread scripts/lib/write-approval-gate.lib.sh Outdated
Comment on lines +40 to +42
if [[ "${TRIGGER_ROLE:-}" != "triage" ]]; then
return 0
fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

2. trigger_role malformed skips gate 📜 Skill insight ⛨ Security

apply_write_approval_gate_if_needed treats any TRIGGER_ROLE value other than exactly triage as
a no-op, so a malformed/unexpected value bypasses the gate instead of failing closed. This can
result in triage-triggered PRs missing the required merge gate if configuration is absent or
malformed.
Agent Prompt
## Issue description
The gate predicate `[[ "${TRIGGER_ROLE:-}" != "triage" ]] && return 0` fails open for malformed/unexpected `TRIGGER_ROLE` values.

## Issue Context
For auth/validation gates, absence/malformed config must not broaden access. Here, an unexpected value (e.g. typo) should not silently disable the gate.

## Fix Focus Areas
- scripts/lib/write-approval-gate.lib.sh[37-43]
- scripts/post-code-test.sh[1187-1214]
- scripts/post-fix-test.sh[403-430]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread scripts/post-code.src.sh
Comment on lines 50 to +54
source "${SCRIPT_DIR_POST}/lib/gitleaks-install.lib.sh"
# shellcheck source=lib/pr-assignee.lib.sh
source "${SCRIPT_DIR_POST}/lib/pr-assignee.lib.sh"
# shellcheck source=lib/write-approval-gate.lib.sh
source "${SCRIPT_DIR_POST}/lib/write-approval-gate.lib.sh"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

3. Protected scripts/ files modified 📜 Skill insight § Compliance

This PR modifies multiple files under the protected scripts/ path, which requires explicit human
review and must not be auto-approved. Ensure governance/infrastructure review expectations are
followed for these changes.

@fullsend-ai-review

fullsend-ai-review Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review

Findings

Medium

  • [fail-open] scripts/lib/write-approval-gate.lib.sh:44 — The merge gate for triage-role PRs is best-effort only: if gh pr edit --add-label fails (network error, token permission issue, API rate limit), the PR proceeds without the needs-write-approval label. Since write_approval_ever_required in the enforcement side checks the issue-events timeline for a labeled event, a failed label application means enforce_write_approval_gate sees ever_required == false and returns 0 — the triage-triggered PR becomes indistinguishable from a write-triggered PR. The code comments explicitly document this as intentional ("Best-effort: never fails the calling script"), and the enforcement side IS fail-closed (API errors default to gate-required), but a transient gh failure at label-application time silently drops the entire authorization requirement for that PR.
    Remediation: Consider making the label application a hard failure (exit non-zero on failure), or use a second enforcement signal (e.g., persist TRIGGER_ROLE as a check-run annotation) so the enforcement side does not depend solely on the label.

  • [protected-path] scripts/, skills/ — 12 of 15 changed files are under protected paths (scripts/ and skills/). The PR links to dispatch: allow Triage role to trigger code/fix, gated on write+ approval before merge fullsend#5687 and the description thoroughly explains the rationale for the change, including design history across two review rounds. Human approval is always required for protected-path changes, regardless of context.

Low

  • [authorization-bypass] scripts/lib/write-approval-gate.lib.sh:26 — Unrecognized TRIGGER_ROLE values (anything other than triage or write) are treated as write — no gate is applied. A warning is logged, but execution continues with return 0. If a new role is introduced in the calling workflow and this script is not updated, PRs triggered by that role will bypass the write-approval gate. The practical risk is limited since TRIGGER_ROLE is set by the workflow dispatch system, not user input.
    Remediation: Consider inverting the logic: only allow known safe roles (write, admin, maintain) to skip the gate, applying the gate for all other roles (fail-closed for unknown roles).

  • [test-inadequate] skills/merge-queue/scripts/write-approval-check-test.sh:62 — No test for the case where a reviewer's most recent action is a COMMENT review (not APPROVED or CHANGES_REQUESTED) on the head commit, with a prior APPROVED on the same commit. The jq filter uses max_by(.submitted_at) which would pick the COMMENT review, and .state == "APPROVED" would then fail — blocking enqueue when a valid approval exists. This is fail-closed behavior (conservative), not fail-open, but may cause friction if reviewers leave follow-up comments after approving.

  • [code-organization] scripts/post-code-test.sh — The gate_applies_for_role / run_gate_test helper and its 7 test cases are duplicated verbatim between post-code-test.sh and post-fix-test.sh. Every other shared library in this repo has a single dedicated *-test.sh file (e.g., pr-assignee-test.sh, gitleaks-install-test.sh). The new write-approval-check.lib.sh correctly follows this pattern, but the gate-label predicate tests were copy-pasted into two consuming-script test files.
    Remediation: Extract the gate predicate tests into a single write-approval-gate-test.sh following the existing convention.

Previous run

Review

Findings

Medium

  • [fail-open] scripts/lib/write-approval-gate.lib.sh:48 — The merge gate for triage-role PRs is best-effort only: if gh pr edit --add-label fails (network error, token permission issue, API rate limit), the PR proceeds without the needs-write-approval label and has no merge gate. The 2>/dev/null || _write_approval_gate_warn pattern suppresses the error and continues. Since the label is the sole enforcement mechanism in this code, a transient gh failure means a triage-triggered PR becomes indistinguishable from a write-triggered PR — a silent fail-open on the authorization gate. The existing ready-for-review and needs-human labels use the same best-effort pattern, but those are operational signals, not security gates.
    Remediation: Consider making the label application non-best-effort for the triage path (exit with error or retry on failure), or implement complementary server-side enforcement so the label is defense-in-depth rather than the sole gate.

  • [protected-path] scripts/ — All 7 changed files are under scripts/, a protected path. The PR links to dispatch: allow Triage role to trigger code/fix, gated on write+ approval before merge fullsend#5687 and explains the rationale for the change. Human approval is always required for protected-path changes, regardless of context.

Low

  • [stale-doc] docs/code.md:23 — Permission statement says "Requires write-level repository permission" — will become stale once the companion PR (dispatch: allow Triage role to trigger code/fix, gated on write+ approval before merge fullsend#5687) ships and triage-role users can trigger /fs-code. Currently accurate since this PR only implements the gate, not the authorization change.
    Remediation: Update when the companion PR ships.

  • [missing-doc] docs/code.md:30 — Control labels table does not include the new needs-write-approval label. The label is inert until the companion PR ships.
    Remediation: Add a row for needs-write-approval when the companion PR ships.

  • [stale-doc] docs/fix.md:27 — Same stale permission statement as docs/code.md — will need updating when the companion PR ships.
    Remediation: Update when the companion PR ships.

  • [missing-doc] docs/fix.md:102 — Control labels table does not include needs-write-approval. Same timing as above.
    Remediation: Add a row for needs-write-approval when the companion PR ships.


Labels: PR modifies post-code and post-fix script infrastructure to add triage-role authorization gate

fullsend-ai-review[bot]

This comment was marked as outdated.

@waynesun09
waynesun09 force-pushed the fix/5687-triage-code-fix-merge-gate branch from 0aed7e1 to cc91a3c Compare July 29, 2026 14:20
@fullsend-ai-review

fullsend-ai-review Bot commented Jul 29, 2026

Copy link
Copy Markdown

🤖 Review · ⚠️ Cancelled · Started 2:22 PM UTC · Ended 2:38 PM UTC
Commit: cc91a3c · View workflow run →

fullsend-ai/fullsend#5687 will allow the GitHub triage role to trigger
/fs-code and /fs-fix. post-code.sh/post-fix.sh apply a needs-write-approval
label to the resulting PR when TRIGGER_ROLE is triage (normalized
case-insensitively; unrecognized values warn and default to no-gate).

Enforcement lives in skills/merge-queue/scripts/lib/write-approval-check.lib.sh,
used by both enqueue-pr.sh (the documented primary enqueue entry point) and
await-and-enqueue.sh (its polling wrapper), so there is no bypass via either
documented path:

- Whether a PR ever required write-approval is derived from the immutable
  issue-events timeline (a labeled event), not the label's current presence
  — GitHub's triage role includes repo-wide label management, so a mutable
  "is it currently labeled" check is removable by the very user it
  constrains.
- The approving reviewer's permission is re-checked live against the
  collaborator-permission API, not trusted from GitHub's reviewDecision
  alone — reviewDecision does not distinguish reviewer permission level, and
  since the PR is bot-authored (not authored by the triage trigger), that
  user is free to review and approve their own triage-triggered PR.
- The approval must be on the PR's current head commit, so a later /fs-fix
  push cannot ride on a stale approval from before those commits existed.
- Bot/App reviewers are excluded from counting as write+ approvers.

This does not prevent a write+ collaborator from merging directly via
GitHub's native UI or `gh pr merge`, which are unaware of this label —
documented as a known residual limitation in docs/code.md and docs/fix.md.

Assisted-by: Claude (fix, review), Grok (review)
Signed-off-by: Wayne Sun <gsun@redhat.com>
@waynesun09
waynesun09 force-pushed the fix/5687-triage-code-fix-merge-gate branch from cc91a3c to 0638a35 Compare July 29, 2026 14:37
@fullsend-ai-review

fullsend-ai-review Bot commented Jul 29, 2026

Copy link
Copy Markdown

🤖 Review · ❌ Terminated · Started 2:39 PM UTC · Ended 2:58 PM UTC
Commit: 0638a35 · View workflow run →

@waynesun09
waynesun09 marked this pull request as draft July 29, 2026 14:54
@waynesun09

Copy link
Copy Markdown
Member Author

Status: draft, paused pending a design revisit

This went through three independent review rounds (Claude ×2 + Grok per round, 9 agent-reviews total), each of which fixed the previous round's specific gap and surfaced a new one. Converting to draft rather than continuing to patch — the pattern below suggests the underlying approach needs a design rethink, not another quick fix.

Round 1 — label-only gate

Shipped: needs-write-approval label applied when TRIGGER_ROLE=triage, treated as sufficient on its own.
Broken by: Nothing anywhere read the label (post-review.sh never submits a real gh pr review; await-and-enqueue.sh/enqueue-pr.sh only checked native reviewDecision). GitHub's triage role has repo-wide label management, so the label is removable by the very user it constrains. Since the PR is bot-authored (not authored by the triage trigger), that user could also just approve their own triage-triggered PR themselves — reviewDecision doesn't distinguish reviewer permission level.

Round 2 — live-permission check, gated on current label state

Shipped: has_write_plus_approval() requiring a live admin/maintain/write approver, wired into await-and-enqueue.sh, gated on the label's current presence.
Broken by: Gating on current label presence reintroduced the exact removability problem from round 1, one layer down. Also only wired into await-and-enqueue.shenqueue-pr.sh (the documented primary entry point per SKILL.md) bypassed the check entirely. Separately: nothing pinned an approval to the PR's current head commit, so a stale approval survived a later /fs-fix push.

Round 3 — immutable-timeline check, commit-pinned, both entry points

Shipped: "Was write-approval ever required" derived from the issue-events timeline (a labeled event, immutable to later removal) instead of current label state; approval pinned to current head commit via commit_id; bot/App reviewers excluded; enforcement in both enqueue-pr.sh and await-and-enqueue.sh via a shared lib.
Broken by (confirmed independently by all 3 review agents with live reproduction, and corroborated by this repo's own qodo-code-review[bot] and fullsend-ai-review[bot]):

  1. write_approval_ever_required fails open, not closed, on gh api errors. Under set -o pipefail, when gh api fails with empty stdout, jq -s still succeeds on empty input (evaluates to false, exit 0). The pipeline's exit status is still non-zero (from gh api), so the || echo "true" fallback also fires — the captured value becomes the literal two-line string "false\ntrue", which fails both == "true" and != "true" comparisons cleanly, silently disabling the gate on any transient API failure. Reproduced independently three times with actual shell commands.
  2. Label application is still best-effort/unverified (scripts/lib/write-approval-gate.lib.sh, unchanged since round 1, also flagged independently by fullsend-ai-review[bot] and qodo-code-review[bot]). If gh pr edit --add-label fails — including via a triage-role user racing a label-delete against it, since they have that permission — no labeled event is ever recorded, and the round-3 design's "immutable timeline" foundation never comes into existence for that PR. write_approval_ever_required then legitimately (no bug this time) returns false forever.

Recommendation for whoever picks this back up

The recurring failure mode is relying on signals GitHub's own triage role has legitimate access to interfere with (labels, its own reviews, timing races against bot API calls). A more robust design likely needs a signal the triage role has zero access to — e.g., an immutable marker in the bot's own commit trailer (git history requires force-push/write access to alter, which triage lacks), checked via git log on the PR's commits rather than the GitHub API's label/timeline surface. That's a real design change, not a patch, and should get a proper look before more code is written here.

Related: fullsend-ai/fullsend#5687 (the authorization-side change this PR was meant to support).

@fullsend-ai-review fullsend-ai-review 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.

See the review comment for full details.

# in the caller rather than a legitimate write+ trigger.
# Requires REPO_FULL_NAME. Best-effort: never fails the calling script.
# Note: parameter is target_pr (not pr_number) to avoid SC2153 against
# PR_NUMBER from post-failure-report.lib.sh once both libs are bundled into

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[medium] fail-open

The merge gate for triage-role PRs is best-effort only: if gh pr edit --add-label fails, the PR proceeds without the needs-write-approval label. The enforcement side (write_approval_ever_required) checks the issue-events timeline for a labeled event — a failed label application means the enforcement gate sees ever_required == false and returns 0, making the triage-triggered PR indistinguishable from a write-triggered PR.

Suggested fix: Make the label application a hard failure (exit non-zero), or use a second enforcement signal so enforcement does not depend solely on the label.

if declare -F gha_echo >/dev/null 2>&1; then
gha_echo warning "$*"
else
echo "warning: $*" >&2

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] authorization-bypass

Unrecognized TRIGGER_ROLE values are treated as write (no gate applied). If a new role is introduced and this script is not updated, PRs triggered by that role will bypass the write-approval gate.

Suggested fix: Invert the logic: only allow known safe roles to skip the gate.

run_ever_required_test "currently-labeled" \
'[{"event": "labeled", "label": {"name": "needs-write-approval"}}]' \
"true"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] test-inadequate

No test for the case where a reviewer's most recent action is a COMMENT review on the head commit with a prior APPROVED. The jq filter's max_by(submitted_at) would pick the COMMENT and fail the APPROVED check — blocking enqueue when a valid approval exists. Fail-closed, not fail-open.

@fullsend-ai-review

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 2:39 PM UTC · Completed 2:58 PM UTC
Commit: 0638a35 · View workflow run →

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant