Skip to content

feat(issue-triage-label): reusable workflow to auto-apply needs-triage floor label - #191

Merged
kyle-sexton merged 1 commit into
mainfrom
feat/issue-triage-label
Jul 21, 2026
Merged

feat(issue-triage-label): reusable workflow to auto-apply needs-triage floor label#191
kyle-sexton merged 1 commit into
mainfrom
feat/issue-triage-label

Conversation

@kyle-sexton

@kyle-sexton kyle-sexton commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Closes #155

Summary

Adds .github/workflows/issue-triage-label.yml, a workflow_call reusable
workflow that applies a configured floor label (default priority: needs-triage) to an issue opened or reopened with no label matching a
configured prefix (default priority:). Non-gating — it never fails a PR or
blocks a merge; it only guarantees new issues don't silently drop out of the
triage queue for lack of a label.

  • Trigger, detection, permissions, loop-safety, and idempotency all follow
    the settled mechanism.
  • Fail-closed on a missing label (added per the implementation brief,
    and verified empirically against the live API in a disposable throwaway
    repo before writing this): the raw POST .../issues/{n}/labels endpoint
    does not 404 on an unknown label name — it silently auto-creates a
    bare label (default gray color, no description). Relying on that call to
    fail naturally would violate the "never define a label" contract, so this
    workflow calls GET /repos/{owner}/{repo}/labels/{name} first and hard-fails
    the job if that 404s, before ever calling addLabels.
  • Least-privilege: top-level permissions: {}, job grants only issues: write. Uses actions/github-script (matching this repo's existing
    pattern in pr-issue-linkage.yml/do-not-merge-gate.yml), SHA-pinned.
  • README documents the reusable alongside its sibling entries, with the
    same caller-example + rationale shape as pr-issue-linkage.yml and
    do-not-merge-gate.yml.

Locally verified clean: actionlint, zizmor --persona=pedantic (0
findings), markdownlint-cli2 (README), typos, editorconfig-checker,
and a node --check syntax pass on the embedded script.

Conventions followed

  • Studied pr-issue-linkage.yml/pr-issue-linkage-self.yml (PR feat(pr-issue-linkage): add exempt-authors opt-in for bot PRs #171's
    newest precedent), do-not-merge-gate.yml, pulumi-version-drift-check.yml,
    and link-check.yml for the reusable-workflow shape, inputs contract,
    pinned-SHA actions/github-script usage, and README documentation style.
  • runner input included per convention; no prerequisite-result input —
    that pattern is specific to gating required-checks chained after a
    prerequisite job, and this workflow is non-gating.

Ambiguities / decisions worth a second look

  • Filename. Issue issues.yml: reusable auto-triage-label workflow — apply 'priority: needs-triage' on issue open/reopen when no priority tier is present #155's title proposes issues.yml; every existing
    reusable workflow here is named after its concern (do-not-merge-gate,
    pr-issue-linkage, link-check), not its triggering event, so I named
    this issue-triage-label.yml instead. Flagging as veto-able rather than
    assuming.
  • No -self.yml dogfood caller in this PR. Every existing reusable here
    has a same-repo dogfood caller, and this repo already carries a real
    priority: needs-triage label — so a self-caller would be a genuine
    end-to-end test. But every existing -self.yml pins its reusable at an
    already-merged SHA (repo-wide pin-by-SHA policy), which is impossible
    for a workflow introduced in this same PR without either an unpinned
    reference (policy violation) or a relative-path call (inconsistent with
    every other caller here). Deferring issue-triage-label-self.yml to a
    follow-up PR once this one merges and a SHA exists to pin.
  • Per the task brief, the thin caller in claude-code-plugins is not
    included here — tracked separately, gated on this landing.

Related

Settled mechanism per melodic-software/claude-code-plugins#506's 2026-07-20
Class-B research-resolution comment. Mirrors the #510#154 cross-repo
filing precedent.

Test plan

  • actionlint .github/workflows/issue-triage-label.yml
  • zizmor --persona=pedantic .github/workflows/issue-triage-label.yml (0 findings)
  • markdownlint-cli2 README.md
  • typos / editorconfig-checker over changed files
  • node --check on the extracted embedded script
  • Empirical verification of POST .../issues/{n}/labels auto-create
    behavior and GET .../labels/{name} 404 behavior in a disposable
    private repo (not this workflow itself — no live trigger path exists
    pre-merge; tracked as the residual gap the follow-up -self.yml closes)

🤖 Generated with Claude Code

…e floor label

Applies the configured floor label (default `priority: needs-triage`) to an
issue opened or reopened with no label matching a configured prefix (default
`priority:`), so a forgotten priority label no longer silently drops an issue
out of the triage queue. Fail-closed on a missing label: the raw add-labels
endpoint auto-creates an unknown label instead of erroring (verified against
the live API), so this workflow calls GET .../labels/{name} first and fails
the job rather than let a bare, undefined label get created. Non-gating —
never blocks a merge.

Closes #155

## Related
Settled mechanism per melodic-software/claude-code-plugins#506's 2026-07-20
Class-B research-resolution comment.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@claude

claude Bot commented Jul 21, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 2m 46s —— View job


PR Review

  • Gather context and read changed files
  • Review .github/workflows/issue-triage-label.yml
  • Review README.md changes
  • Post comprehensive review feedback

Overall: LGTM. The implementation is solid — all security ground rules from CLAUDE.md are followed, the logic is correct, and the non-obvious design decisions are well-documented. Two minor observations below; neither is a blocker.


Security & CLAUDE.md compliance

Check Result
No pull_request_target / workflow_run-with-secrets trigger workflow_call only
SHA-pin both layers github-script@3a2844b7e9c... — matches repo-wide SHA across pr-issue-linkage.yml, do-not-merge-gate.yml, standards-sync.yml, select-runner.yml
Least privilege, top-level permissions: {}
Job grants only what's needed issues: write only
No untrusted checkout ahead of action ✅ No checkout step at all
No debug logging / no token echo
Inputs via env: not interpolated into script: body ✅ Avoids script injection for caller-controlled strings

Logic correctness

Fail-closed label existence checkline 116–127

The getLabel → 404 → setFailed pattern is the right call here. The POST .../issues/{n}/labels auto-create behavior is a real footgun and the comment documents exactly why the guard exists. Re-throwing non-404 errors (line 127) is correct — infrastructure errors should surface, not be swallowed as green skips.

Prefix guard reads from event payloadline 100–107

context.payload.issue?.labels on opened/reopened events carries the correct current label set. The optional-chain + ?? [] fallback is appropriate. The startsWith(labelPrefix) check means any existing priority tier (including the floor label itself) skips re-application — correct idempotency.

Step-level if: guardline 76–78

The >- folded scalar resolves to a single-line expression, which is valid. This is a good defense-in-depth layer for callers who later broaden their trigger matrix beyond opened/reopened — the job completes as a clean skip rather than misbehaving.

addLabels idempotency — GitHub's API returns the updated label list (not an error) if the label is already present, so a re-run after a prior success is a true no-op at the API level as well as at the guard level. ✓


Observations (non-blocking)

1. Caller example omits optional with: inputs

The README caller snippet (lines 183–193) shows only the minimal invocation. Callers who need non-default label, label-prefix, or runner values would need to consult the workflow file itself. This is consistent with the style of other caller examples in the README, so it's not out of place — just noting that a commented with: block could help discoverability for callers with non-default taxonomies.

2. No -self.yml dogfood caller

The PR body explains this clearly: pinning a newly-introduced workflow SHA at merge time requires a follow-up PR. The deferred issue-triage-label-self.yml is the right call — a policy-violating unpinned reference or a relative-path call would be worse. The gap is tracked. ✓


README

The documentation entry is thorough, consistently styled with sibling entries, and covers all the non-obvious contract points (fail-closed behavior, loop safety, idempotency, triager responsibility for tier assignment). The internal reference link to claude-code-plugins#506 is appropriate for traceability.

@claude

claude Bot commented Jul 21, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 1m 28s —— View job


Security Review

  • Gather context and read changed files
  • Audit workflow triggers and permission model
  • Check for script injection surfaces (github context in run: blocks)
  • Review supply-chain / SHA-pin hygiene
  • Check token/secret handling and least-privilege
  • Review logic: label-detection, fail-closed behavior, loop-safety
  • Post findings

Scope: .github/workflows/issue-triage-label.yml (new, +134 lines) and README.md (+38 lines, documentation only). No security-relevant findings in the README changes.

Findings

No security vulnerabilities found.

The implementation is clean across every attack surface I reviewed:

Trigger safetyon: workflow_call only; no pull_request_target or workflow_run. The defensive step guard (github.event_name == 'issues' && action in {opened, reopened}) is a belt-and-suspenders protection against callers broadening their trigger matrix. ✅

Script injection — The two inputs.* values are routed through env: variables (TARGET_LABEL, LABEL_PREFIX) and read inside the script via process.env.*, never interpolated directly into the JavaScript source. context.payload.issue.number and context.repo are consumed as typed objects through the Octokit SDK, not as shell strings. No ${{ github.event.* }} or ${{ inputs.* }} appears inside any run: or script: block. See line 81–106

Supply-chain / pin hygieneactions/github-script is pinned at a full 40-char SHA with a # v9.0.0 trailing comment. No other external actions are used. No checkout step is present (eliminating untrusted-code execution entirely). ✅

Least-privilege — Top-level permissions: {} zeroes out all defaults. The job re-grants only issues: write. No contents, pull-requests, id-token, or any other permission is opened. No secrets are referenced anywhere in the file. ✅

Fail-closed logic — The GET /repos/{owner}/{repo}/labels/{name} pre-check before addLabels correctly prevents silent label auto-creation. The getLabel 404 path calls core.setFailed and returns; any other error re-throws rather than swallowing it. ✅

Loop safety — Documented and technically correct: label addition emits issues.labeled, not opened/reopened, and GITHUB_TOKEN-authored events do not start new runs for those trigger types. ✅


@kyle-sexton

Copy link
Copy Markdown
Contributor Author

Tower merge (overnight delegation 2026-07-21, veto window open): merging under the operator's overnight decide-and-log delegation — non-gating reusable workflow, least-privilege (issues: write only, job-level), fail-closed label-existence check (load-bearing: addLabels silently auto-creates unknown labels, empirically verified), SHA-pinned, both advisory review lanes clean (code LGTM, security explicitly clear on injection/pin/permissions). Reversible (workflow deletable; no caller exists yet). Operator-vetoable items recorded on the PR: filename choice (follows repo concern-named convention), self-caller dogfood follow-up (being filed).

This was generated by AI (control tower, session 6).

@kyle-sexton
kyle-sexton merged commit c5e729c into main Jul 21, 2026
38 of 39 checks passed
@kyle-sexton
kyle-sexton deleted the feat/issue-triage-label branch July 21, 2026 10:06
kyle-sexton added a commit that referenced this pull request Jul 21, 2026
)

Closes #192

## Summary
Adds `.github/workflows/issue-triage-label-self.yml`, the dogfood caller
for
`issue-triage-label.yml` (#191), pinned to that reusable's merged squash
SHA
(`c5e729c`), not a PR head — mirroring `pr-issue-linkage-self.yml`'s pin
discipline exactly.

Why this exists: `issue-triage-label.yml` had no `issues:
opened`/`reopened`
trigger path anywhere in this repo before now, so its label-set guard
and
`getLabel` → 404 → `setFailed` fail-closed check were only ever
exercised by
local linting and an out-of-band API test in a disposable repo — not a
real
GitHub Actions event. This repo already carries a real `priority:
needs-triage` label (confirmed via `gh label list`), so this caller is a
genuine end-to-end exercise, not a stub.

Locally verified clean: `actionlint`, `zizmor --persona=pedantic` (0
findings), `editorconfig-checker`, `typos`.

## Related
#191 (adds `issue-triage-label.yml`, merged
at
`c5e729c0af0e55ffed4675ec85c1b57356fef79e`).

## Test plan
- [x] `actionlint .github/workflows/issue-triage-label-self.yml`
- [x] `zizmor --persona=pedantic
.github/workflows/issue-triage-label-self.yml` (0 findings)
- [x] `editorconfig-checker` / `typos` over the changed file
- [ ] Live verification: open or reopen a real issue in this repo
post-merge
      with no `priority:*` label and confirm `priority: needs-triage` is
      applied by this caller

🤖 Generated with [Claude Code](https://claude.com/claude-code)
kyle-sexton added a commit to melodic-software/standards that referenced this pull request Jul 21, 2026
…ontract (#236)

Closes #235

## Summary
Registers ci-workflows' `issue-triage-label` reusable workflow as a
reviewed `runner-input` contract in
`components/runner-policy/policy.json`,
pinned to its merged SHA `c5e729c0af0e55ffed4675ec85c1b57356fef79e`:

```json
"melodic-software/ci-workflows/.github/workflows/issue-triage-label.yml@c5e729c0af0e55ffed4675ec85c1b57356fef79e": {
  "routing": "runner-input",
  "runnerInput": "runner",
  "allowedInputs": ["runner", "label", "label-prefix"],
  "allowedSecrets": {},
  "allowedCallerPermissions": { "issues": "write" }
}
```

- `allowedInputs` covers the reusable's full `workflow_call.inputs`
surface
  (`runner`, `label`, `label-prefix`) — a caller may pass any subset.
- `allowedSecrets: {}` — the reusable takes no secrets.
- `allowedCallerPermissions: { issues: write }` — the reusable's job
needs
exactly `issues: write` to read and add labels; no `contents` (no
checkout).
- No `selectorResultInput` — the reusable has no chained-selector
  (`prerequisite-result`-style) input.

Locally verified: `npm run test:runner-policy` (227/227 passing),
`npm run lint:runner-policy` ("Runner policy passed."), `npm run
lint:md`,
`npm run lint:hooks`, plus `lefthook`'s pre-commit gate (typos,
gitleaks,
editorconfig, biome) on commit.

## Related
- melodic-software/ci-workflows#191 — the reusable workflow, merged.
- melodic-software/ci-workflows#195 — its dogfood self-caller, merged,
  pinned at this same merged SHA.
- melodic-software/ci-workflows#192 — end-to-end proof the self-caller
  actually applies the floor label live.
- melodic-software/claude-code-plugins#850 — the consumer PR blocked on
  this contract (its `Runner policy` check fails without it).
- melodic-software/claude-code-plugins#506 — the issue driving that
caller.

## Test plan
- [x] `npm run test:runner-policy` (227 passing)
- [x] `npm run lint:runner-policy` ("Runner policy passed.")
- [x] `npm run lint:md`
- [x] `npm run lint:hooks`
- [x] `lefthook` pre-commit gate (typos, gitleaks, editorconfig, biome)
kyle-sexton added a commit to melodic-software/claude-code-plugins that referenced this pull request Jul 21, 2026
…850)

Closes #506

## Summary
Adds `.github/workflows/issue-triage-label.yml`, a thin caller invoking
ci-workflows' `issue-triage-label` reusable workflow (`workflow_call`)
on
`issues: opened`/`reopened`. Applies the existing `priority:
needs-triage`
label when an issue carries no `priority:*` label; leaves issues filed
with
an explicit tier untouched. Non-gating.

- Pinned by merged SHA `c5e729c0af0e55ffed4675ec85c1b57356fef79e`
(ci-workflows
  PR #191), matching this repo's existing SHA-pin convention
  (`pr-issue-linkage.yml`, `do-not-merge.yml`, `link-check.yml`).
- Uses the reusable's defaults for `label` (`priority: needs-triage`)
and
`label-prefix` (`priority:`) — both already match this repo's taxonomy,
so
no overrides needed. `runner: ubuntu-24.04` passed explicitly per
sibling
  caller convention.
- Verified live: `priority: needs-triage` exists in this repo's label
set
(`gh label list`), so the reusable's fail-closed missing-label guard
will
  not trip.
- Locally verified clean: `actionlint`, `zizmor --persona=pedantic` (0
findings after adding an explanatory comment on the `issues: write`
grant),
  `typos`.

## Related
melodic-software/ci-workflows#191 (merged 2026-07-21T10:06:31Z)
implements the
reusable workflow this caller invokes, carrying the settled mechanism
from
#506's 2026-07-20 Class-B research-resolution comment and the tower
decision
comment confirming this thin-caller shape.

## Test plan
- [x] `actionlint .github/workflows/issue-triage-label.yml`
- [x] `zizmor --persona=pedantic
.github/workflows/issue-triage-label.yml` (0 findings)
- [x] `typos .github/workflows/issue-triage-label.yml`
- [x] `gh label list` confirms `priority: needs-triage` exists live in
this repo
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

issues.yml: reusable auto-triage-label workflow — apply 'priority: needs-triage' on issue open/reopen when no priority tier is present

1 participant