feat(workflows): add Initiatives project auto-add workflow#388
Conversation
Adds qualifying issues, PRs, and Ideas-category discussions to https://github.com/orgs/petry-projects/projects/1 via the noise gate defined in #387. - Issues/PRs: require dev-lead label and exclude compliance-audit / health-check / fleet-tracker / daily-report - Discussions: only Ideas category, added as draft items (API limitation prevents content-linking discussions to Projects v2) Requires org secret PROJECTS_TOKEN with Projects: Read+write scope. Refs #387, discussion #386
|
Note Gemini is unable to generate a review for this pull request due to the file types involved not being currently supported. |
|
Warning Review limit reached
More reviews will be available in 14 minutes and 40 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (9)
📝 WalkthroughWalkthroughA new GitHub Actions workflow automatically routes qualifying content to the org "Initiatives" Projects v2 board. Issues and PRs require the ChangesAuto-add to Projects workflow
Sequence DiagramsequenceDiagram
participant GitHub as GitHub Event
participant Workflow as add-to-project Workflow
participant Validation as Label Validation
participant GraphQL as Projects v2 GraphQL API
GitHub->>Workflow: Issue/PR event (issue/PR opened/labeled)
Workflow->>Validation: Check for dev-lead label
alt Has dev-lead and no excluded labels
Validation->>GraphQL: Add item to project
GraphQL->>Workflow: Success
Workflow->>GitHub: Item added to Initiatives board
else Missing dev-lead or has excluded label
Validation->>Workflow: Gate failed
Workflow->>GitHub: No action
end
GitHub->>Workflow: Discussion event (discussion created)
alt Category is Ideas
Workflow->>GraphQL: Create draft project item
GraphQL->>Workflow: Draft created
Workflow->>GitHub: Draft item added to Initiatives board
else Category is not Ideas
Workflow->>GitHub: No action
end
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Possibly related issues
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
Adds a new GitHub Actions workflow intended to auto-add qualifying work items to the org-level Initiatives GitHub Project.
Changes:
- Introduces
.github/workflows/add-to-project.ymlto add issues/PRs labeleddev-lead(with specific excluded labels) to the Initiatives project via GraphQL. - Adds Ideas-category discussions to the project as draft items (due to Projects v2 Discussion content limitations).
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: dee58461ff
ℹ️ 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".
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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 @.github/workflows/add-to-project.yml:
- Around line 20-21: The workflow is triggering on discussion:labeled which
causes addProjectV2DraftIssue to run and create duplicate draft items; either
remove discussion:labeled from the discussion.types list in
.github/workflows/add-to-project.yml or add deduplication before creating a
draft by checking for an existing draft in the Initiatives project (the call
site that invokes addProjectV2DraftIssue should query existing project items and
skip creation if a matching draft exists). Update the discussion trigger lines
(and the same block around lines referenced 99-108) or add the existence check
inside the code path that calls addProjectV2DraftIssue so duplicate drafts are
prevented.
🪄 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: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 03308524-3e62-45db-b87d-1b88bed8c300
📒 Files selected for processing (1)
.github/workflows/add-to-project.yml
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 531e586374
ℹ️ 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".
Lint:
- Add shellcheck disable=SC2016 on run blocks (the single-quoted GraphQL
query strings contain GraphQL variables, not shell variables — intentional)
Review feedback addressed:
- Drop discussion.types: labeled — was producing duplicate draft items
on each label change to the same Ideas discussion (CodeRabbit, Codex,
Copilot all flagged this)
- Add explicit PROJECTS_TOKEN presence check at the top of both run
blocks so missing-secret failures are actionable (Copilot)
- Gate pull_request_target on trusted author_association so fork PRs
cannot trigger a secret-bearing runner (Copilot)
- Drop top-level permissions; declare permissions: {} per job (Codex)
- Document the multi-repo scope limit in the file header (Copilot, Codex P1)
Deferred for follow-on (not in scope for the pilot):
- Multi-repo rollout via reusable workflow (Codex P1) — needs a separate
design decision; documented as known limit in file header
- Project item cleanup when label set later becomes excluded (Codex P2)
- App installation token rotation if PROJECTS_TOKEN comes from an App (Codex P2)
- Token scope for repo read on private repos (Codex P2)
Refs #387, discussion #386
|
Pushed Addressed in this pushLint (CI)
CodeRabbit — "Remove
Copilot —
Copilot — opaque failure when
Codex P2 — Reset top-level token permissions
Copilot / Codex P1 — Multi-repo deployment gap
Deferred to follow-on work
@don-petry — these last three deferred items hinge on how |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ff69ff07ff
ℹ️ 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".
Replace PROJECTS_TOKEN with a fresh installation token minted per run via actions/create-github-app-token@bcd2ba49 (v3.2.0, same SHA already pinned by dependabot-rebase-reusable.yml and dependabot-automerge-reusable.yml). App: petry-projects-planner (App ID 3985527) Org secrets: INITIATIVES_APP_ID, INITIATIVES_APP_PRIVATE_KEY (scoped to .github) App permissions: Org Projects R+W, Repo Issues + PRs R-only Addresses Codex P2: 'Generate App installation tokens per run' — installation tokens have 1-hour lifetime, so a static long-lived PAT was the wrong shape. Drops the PROJECTS_TOKEN empty-check since the App-token step fails fast with its own error if secrets are missing. Refs #387, discussion #386
The shellcheck disable comment was at YAML indentation level (treated as a YAML comment, not visible to shellcheck). Moving it inside the run: | block as a bash comment so SC2016 actually gets suppressed. Refs #387
Match the org's existing pattern (ci-failure-analyst-reusable.yml:121-126) of placing the disable directive immediately before the offending command, not at script top. Top-of-script disable was being treated as next-command-only. Refs #387
|
@coderabbitai review The duplicate-discussion concern (line 20-21) is addressed in Workflow has since been further updated to use a dedicated GitHub App ( |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Fetch the current workflow file from the PR branch
cat .github/workflows/add-to-project.yml
echo "---"
# Also get the PR diff to see what changed
gh pr view 388 --repo petry-projects/.github --json files,headRefName,commitsLength of output: 11336
✅ All prior blocking concerns resolved
🔍 Observations (no blockers)
VerdictThe workflow is correct, secure, and ready to merge once the [approve] 🧠 Learnings used |
Addresses two new findings from Codex on commit e212c57: 1. Add top-level permissions: {} so compliance-audit.sh check_workflow_permissions doesn't flag this multi-job workflow. Job-level permissions: {} stays for least-privilege; the App-minted token handles all API work, GITHUB_TOKEN is unused. 2. Document the fork-PR gap in the file header: author_association gate evaluates the PR author, not the labeler, so fork PRs from FIRST_TIMER contributors won't auto-add even after a maintainer labels dev-lead. Workaround: manual add via UI. Defer the fix to the multi-repo follow-on PR (will need a labeler-association check). Refs #387
Status as of
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5bfe0355fa
ℹ️ 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".
donpetry-bot
left a comment
There was a problem hiding this comment.
Automated review — APPROVED ✓
Risk: LOW
Reviewed commit: 5bfe0355fa2bc9807d8a9f927040e151088bea82
Review mode: triage-approved (single reviewer)
Summary
Adds .github/workflows/add-to-project.yml, a 155-line workflow that auto-adds qualifying issues/PRs and Ideas-category discussions to the org-level Initiatives project via gh api graphql. The triage tier cleared this as low-risk; this is the confirmation pass. Verified the security-sensitive surfaces (pull_request_target, App token, permissions, action pinning) and confirmed all prior automated-reviewer feedback has been resolved.
Linked issue analysis
No closes reference; PR body links tracking issue #387 and discussion #386 as related (not closed by this PR). That's correct — #387 is the umbrella initiative and this PR is the scoped deliverable, so the body's framing accurately reflects scope.
Findings
Security posture — verified clean:
pull_request_targetis gated oncontains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.pull_request.author_association), blocking fork-PR runs from minting the App token. The known fork-PR gap (maintainer-labeled fork PRs from CONTRIBUTOR/FIRST_TIMER authors won't be auto-added) is documented in the workflow header.- Only third-party action is
actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0— pinned by full SHA, satisfying the compliance audit. - Top-level
permissions: {}and per-jobpermissions: {}are both set; the workflow uses the App-minted token viaGH_TOKENrather thanGITHUB_TOKEN, so the empty job permissions are correct. - GraphQL mutations use parameterized variables (
$projectId,$contentId,$title,$body) bound via-F/-F/-f; all string inputs flow through env vars, so there's no shell-injection surface from issue/PR/discussion titles. set -euo pipefailon both run blocks; SC2016 disables are correctly scoped per-command.
Correctness:
- CodeRabbit's prior actionable finding (duplicate draft items from
discussion:labeled) was resolved in4af76ce—discussion.typesis now[created, category_changed]. CodeRabbit subsequently re-approved at this SHA. - Concurrency group keys on
event_name+ item number, withcancel-in-progress: false— appropriate for additive mutations. - Noise gate logic (
dev-leadrequired, four excluded labels) matches the PR description and the noise-control intent in #387.
Minor observations (not blocking):
- A user toggling an Ideas discussion's category away and back will fire
category_changedtwice and create a second draft item. Low-probability edge case; consistent with the explicit decision to keep the workflow stateless. - The PR description's "Prerequisites before merge" still references the old
PROJECTS_TOKENsecret approach; the workflow has since moved to thepetry-projects-plannerApp (INITIATIVES_APP_ID/INITIATIVES_APP_PRIVATE_KEY). Worth a body update before merge so the operator sets up the right secrets, but not a code issue.
CI status
All required checks green at 5bfe035: Lint, ShellCheck, Agent Security Scan, Secret scan (gitleaks), SonarCloud, CodeQL (actions analyzer), AgentShield, Dependency audit, CodeRabbit. No failing or pending required check.
Reviewed automatically by the PR-review agent (claude-opus-4-7-single). Reply if you need a human review.
Replace the add-only `add-discussion` job with `reconcile-discussion` that handles all four corners of the discussion state machine: - Ideas + no existing draft → add (original behavior) - Ideas + existing draft → skip (idempotent; also dedupes any future re-entry into Ideas) - non-Ideas + existing draft → delete (cleanup; addresses the Codex P2 finding on commit 5bfe035) - non-Ideas + no existing draft → no-op Lookup matches by title prefix "[Discussion #N] " against the project's draft items (paginated at 100; ~15 today, years of headroom). Uses the same app-minted token for both query and mutate paths. Updates file header to document the new state machine. Removes the implicit OUT-of-Ideas gap from the known-limits section. Refs #387, addresses Codex P2 on PR #388 review 4444371481
20e3459
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 20e3459752
ℹ️ 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".
Addresses Codex P2 on PR review 4444371481 (paginate project item lookup)
and the user request to add test coverage.
Layout (mirrors test/workflows/feature-ideation/):
.github/scripts/add-to-project/
add-issue-or-pr.sh sourceable functions: evaluate_noise_gate,
add_content_to_project, process_issue_or_pr
reconcile-discussion.sh sourceable functions: find_existing_draft_id
(paginated), add_discussion_draft,
delete_project_item, reconcile_discussion
test/workflows/add-to-project/
helpers/setup.bash tmpdir, gh stub install (copy + chmod)
stubs/gh fake gh with single- and multi-call modes
add-issue-or-pr.bats 12 tests covering noise gate, all 4 skip
reasons, and the happy path
reconcile-discussion.bats 10 tests covering all 4 state machine
corners, title-prefix anchoring (#42 vs
#420), and pagination (match on page 2,
no match across all pages)
.github/workflows/add-to-project-tests.yml
PR/push gate: shellcheck + bats
.github/workflows/add-to-project.yml
thin shell: checkout, mint App token, call
the script with env
Pagination fix: find_existing_draft_id now loops with after:cursor until
hasNextPage is false, so projects beyond 100 items still match correctly.
At ~16 items today, this is preventative.
Local validation (donpetry@host): bats 1.13.0, jq 1.7, shellcheck 0.10.0:
- shellcheck -S warning: all scripts + harness clean
- bats test/workflows/add-to-project/: 22 of 22 pass
Refs #387
…tests
Correctness fixes (review findings 1-11):
1. evaluate_noise_gate: coerce non-array LABELS_JSON to [] so events
with null labels don't abort the script via jq error.
2. evaluate_noise_gate: collapse 5 jq calls to 1, return distinct codes
(0=ok, 1=skip, 64=arg-bug, 65=unexpected shape).
3. process_issue_or_pr: distinguish gate-said-skip (log+continue) from
gate-errored (propagate to mark workflow failed); previously every
non-zero became a silent 'Skip'.
4. find_existing_draft_id: parse match+pageInfo in ONE jq call using
first(...); avoids SIGPIPE on jq | head -n 1 under pipefail when
multiple candidates match on a page.
5. find_existing_draft_id: fail loudly (exit 75) when data.node is null
(wrong PROJECT_ID, token scope drift) instead of silently returning
no-match and letting the caller add duplicates.
6. find_existing_draft_id: warn when multiple drafts share a prefix
rather than silently picking one — surfaces the inconsistency.
7. find_existing_draft_id: also resolve Issue.title and PullRequest.title
so a 'Convert to issue' on an existing draft doesn't orphan the
reconciliation (item becomes invisible to the lookup).
8. add_*/delete_*: redirect gh's GraphQL JSON to /dev/null so the
functions don't leak responses to stdout when called by .
9. delete_project_item: catch 'Could not resolve to a node' as
idempotent success (handles webhook redelivery).
10. Both scripts: explicit empty-GH_TOKEN check with ::error::
annotation pointing at #387 (regression from PR #388's earlier
cleanup pass).
11. Discussion triggers: add 'deleted' and 'transferred' so orphan
drafts get cleaned up when an Ideas discussion vanishes.
12. Workflow concurrency: group discussion events by discussion number
alone (not event_name) so 'created' and 'category_changed' for the
same discussion serialize — closes a real race window.
Test additions (review findings 12-15):
- assert_invocation_count on every happy path: a regression that adds
the same item twice now fails the test.
- Pagination test asserts the EXACT cursor value (MY_DISTINCTIVE_CURSOR)
was passed on call 2, not just that the substring 'cursor' appears.
- Ideas-add test asserts the body contains 'Source:' + URL + the
'Auto-added from Ideas-category' marker.
- PROJECT_ID/GH_TOKEN required tests use 'run --separate-stderr' so
they assert against $stderr directly, no longer relying on bats's
default stdout+stderr merge.
- New tests: data.node:null → exit 75; multiple matches → warn+pick
first; Issue.title match; delete idempotency on 'Could not resolve';
delete propagates real errors; null/non-array LABELS_JSON treated
as empty.
Local validation:
bats 1.13.0: 33 of 33 pass
shellcheck -S warning: clean
Refs #387
Review pass + fixes —
|
| # | Bug | Fix |
|---|---|---|
| 1 | LABELS_JSON: "null" (or non-array) made jq abort the script under set -e |
evaluate_noise_gate coerces non-array to [] |
| 2 | process_issue_or_pr swallowed every non-zero (arg-count, jq parse error, etc.) as a clean Skip |
New exit-code contract: 0=ok, 1=skip, 64=arg bug, 65=unexpected shape; only 1 logs as Skip |
| 3 | find_existing_draft_id's jq | head -n 1 SIGPIPE-killed jq on multi-match |
One jq call with first(...) returns at most one id, no pipefail trap |
| 4 | GraphQL JSON responses from add_*/delete_* polluted stdout |
All mutations redirect to /dev/null |
| 5 | delete_project_item failed loudly on already-deleted item (webhook redelivery) |
Catches Could not resolve to a node and returns success — idempotent |
| 6 | data.node:null made find_existing_draft_id silently return no-match (wrong PROJECT_ID, scope drift → duplicate adds) |
Explicit check, exits 75 with diagnostic |
| 7 | Title-prefix multi-match picked arbitrary draft with no signal | Logs a WARNING: N drafts match prefix so the operator sees the bad state |
| 8 | Concurrency group keyed on event_name, so created and category_changed for the same discussion raced |
Group resolves to disc-<number> for all discussion events; per-content serialization |
| 9 | discussion:deleted / discussion:transferred not subscribed → orphan drafts forever |
Triggers added; the non-Ideas branch of reconcile_discussion handles cleanup |
| 10 | Lost the explicit empty-GH_TOKEN annotation that pointed at #387 |
_atp_require_env re-introduces ::error:: annotation in both scripts |
| 11 | find_existing_draft_id only saw DraftIssue.title → 'Convert to issue' on a draft orphaned the tracking |
Resolves Issue.title and PullRequest.title too |
Test improvements
- Every happy-path test now asserts
assert_invocation_count— silent double-add regressions can't sneak past. - Pagination test asserts the EXACT cursor value (
MY_DISTINCTIVE_CURSOR) was passed on call 2, not just that "cursor" appears. - Ideas-add test asserts the draft body contains the source URL + the auto-added marker, not just the title.
PROJECT_ID/GH_TOKEN-required tests userun --separate-stderrso they assert against$stderrdirectly — they don't depend on bats' default stream merge.- New tests for:
data.node:null→ exit 75; multi-match → warn+pick-first;Issue.titlematched; delete idempotency onCould not resolve; delete propagates real errors;null/non-arrayLABELS_JSONtreated as empty.
Bats locally: 33/33 pass. Shellcheck -S warning: clean.
Deferred (architectural, out-of-pilot)
- Hard-coded
dev-lead+ 4 excluded labels inevaluate_noise_gate— should be env-driven when a second Project lands. - Hard-coded
Ideascategory inreconcile_discussion— same reasoning. - Reconcile pattern only for discussions, not issues/PRs — the issue/PR cleanup-on-label-removal is a sibling design decision; both belong in the multi-repo follow-on so the underlying mechanism (generic
reconcile_content_with_project) can be designed once.
Refs #387.
…nt line-length (200)
The new conditional grouping (disc-N for discussion events vs
event_name-N for issues/PRs) is too long to fit on one line. Use
a folded scalar (>-) so newlines fold to spaces inside the
${{ ... }} expression — GitHub Actions tokenizes whitespace
identically.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f1641cad03
ℹ️ 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".
…ed + fix standards/dev-lead.yml newline Codex flagged (PR #388 review on f1641ca): the deleted and transferred discussion payloads can deliver an empty discussion.category, so ${DISC_CATEGORY:?required} would hard-fail the script before reconcile_discussion ever ran. Switch the main runner's default from :?required to :- (empty allowed). reconcile_discussion's non-Ideas branch already treats any non-'Ideas' value (including '') as 'if a draft exists, clean it up', which is the desired behavior for deleted/transferred. New bats coverage: - 'empty category (deleted/transferred payload) + existing draft → delete' - 'empty category + no existing draft → no-op' Also: append the missing trailing newline to standards/workflows/dev-lead.yml that was introduced by the main-merge in e51a17b — yamllint's new-line-at-end-of-file rule was blocking the Lint gate on this PR. Local: bats 35/35 pass, shellcheck clean. Refs #387
|
|
@don-petry assigned me as reviewer — starting a fresh review now. Results will appear in a few minutes. |
donpetry-bot
left a comment
There was a problem hiding this comment.
Automated review — APPROVED ✓
Risk: LOW
Reviewed commit: 9390135bc18edc1cc8ef57ea07e6fe49591b20ff
Review mode: triage-approved (single reviewer)
Summary
Confirmation pass on the triage tier's low-risk assessment. This PR introduces an Initiatives-project auto-add workflow plus its shell scripts (add-issue-or-pr.sh, reconcile-discussion.sh), a dedicated test workflow, and a 33+ test bats suite. The surface is conventionally high-risk (pull_request_target, GitHub App secrets, GraphQL mutations) but the security controls verified clean, all prior automated-reviewer feedback is resolved, CodeRabbit re-approved at the head SHA, and every required CI check is green.
Linked issue analysis
No closes reference; the body links #387 (umbrella initiative) and discussion #386 as related context. That's accurate scope framing — this PR is the scoped pilot deliverable, not a close-out of either reference.
Findings
Security posture — verified clean on the sensitive surfaces:
pull_request_targetis gated oncontains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.pull_request.author_association), so a fork PR cannot trigger a secret-bearing runner. The known fork-PR gap (maintainer-labeled CONTRIBUTOR/FIRST_TIMER PRs won't be auto-added) is documented in the workflow header and deferred to the multi-repo follow-on.- Only third-party action is
actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0— full-SHA pinned, matching the org-wide SHA already used bydependabot-rebase-reusable.yml. - Top-level
permissions: {}(compliance audit) and per-jobpermissions: { contents: read }(minimum for checkout). The App-minted installation token (1h TTL) is used for the GraphQL API;GITHUB_TOKENis unused, so the empty-ish job permissions are correct. - All GraphQL mutations use parameterized variables (
$projectId,$contentId,$title,$body) bound viagh api graphql -F/-f. Issue/PR/discussion titles and bodies flow through env vars, never through shell expansion into the query string — no injection path. set -euo pipefailon every script; per-command# shellcheck disable=SC2016is correctly scoped (matches the org pattern fromci-failure-analyst-reusable.yml).- Discussion concurrency group keys on
disc-<N>for discussion events (vs.<event>-<N>for issues/PRs), socreatedandcategory_changedfor the same discussion serialize — closes the state-machine race window flagged earlier.
Correctness:
evaluate_noise_gatecollapses to a single jq invocation and returns distinct codes (0/1/64/65);process_issue_or_prpropagates programmer/payload bugs rather than swallowing them as a silent skip.find_existing_draft_idpaginates with cursor-passing (matches anywhere in the project, not just the first 100 items), fails loudly with exit 75 ondata.node:null, and warns on multi-match rather than silently picking one.delete_project_itemcatches "Could not resolve to a node" as idempotent success — correct for redelivered webhooks / racing runs.- The
DISC_CATEGORY:-default in the runner correctly pairs withreconcile_discussion's non-Ideas branch to clean up drafts ondeletedandtransferredpayloads (where the category can be empty/null). addProjectV2ItemByIdis idempotent on the issues/PRs side, so re-firing on multiplelabeledevents is safe.
Test coverage:
- 33+ bats tests covering: every noise-gate label condition, every state-machine corner, exact-cursor pagination assertion, idempotency on already-deleted items, malformed-payload defense (
null/ non-array labels), and the data.node:null hard-fail path. Tests assert exact gh invocation counts on happy paths so duplicate-add regressions would fail loudly. - Dedicated
.github/workflows/add-to-project-tests.ymlgate runs shellcheck + bats on every PR touching the surface.
Prior reviewer feedback — all resolved at head:
- CodeRabbit's
discussion: labeledduplicate-drafts finding → fixed in4af76ce(droppedlabeledfromdiscussion.types); CodeRabbit re-approved at9390135bc. - Copilot's
pull_request_targetfork-PR exposure → fixed via theauthor_associationgate. - Codex P2 "generate App installation tokens per run" → fixed via the
petry-projects-plannerApp migration in84faef0. - Codex P2 "empty DISC_CATEGORY hard-fails on deleted/transferred" → fixed at head SHA by switching the runner's default from
:?to:-.
Minor observations (not blocking):
- The PR body's "Prerequisites before merge" still references the original
PROJECTS_TOKENsecret; the workflow has since moved toINITIATIVES_APP_ID/INITIATIVES_APP_PRIVATE_KEYvia the App. Worth a body update so the operator provisions the right secrets, but it's a doc nit, not a code issue. - The
standards/workflows/dev-lead.ymlchange is a trailing-newline fix (yamllintnew-line-at-end-of-file) introduced by the main-merge ine51a17b— incidental cleanup, not in scope.
CI status
All required checks green at 9390135bc: Lint, ShellCheck, Agent Security Scan, Secret scan (gitleaks), SonarCloud, CodeQL (actions), AgentShield, Dependency audit, Add to Project Tests (shellcheck + bats), and CodeRabbit. No failing or pending required check.
Reviewed automatically by the PR-review agent (single-reviewer mode: opus 4.7). Reply if you need a human review.
donpetry-bot
left a comment
There was a problem hiding this comment.
Automated review — APPROVED ✓
Risk: MEDIUM
Reviewed commit: 9390135bc18edc1cc8ef57ea07e6fe49591b20ff
Review mode: triage-approved (single reviewer)
Summary
Adds the org-level Initiatives project auto-add workflow (add-to-project.yml) plus two sourceable shell scripts (add-issue-or-pr.sh, reconcile-discussion.sh) and a comprehensive bats test suite. Triage tier cleared this as low-risk; this review confirms the security-sensitive surface (pull_request_target + App-minted token) is correctly mitigated and the implementation is sound. Approving.
Linked issue analysis
The PR body references tracking issue #387 and discussion #386 (not declared via closes, intentionally — this is the scoped deliverable, not a full close). The workflow's scope matches what's described: noise-gated issues/PRs and an Ideas-category discussion state machine, scoped to petry-projects/.github only with a documented multi-repo follow-on.
Findings
Security surface — correctly hardened
pull_request_targetjob gated onauthor_association ∈ {OWNER, MEMBER, COLLABORATOR}, so fork PRs from untrusted authors do not trigger the secret-bearing runner. The known FIRST_TIMER/CONTRIBUTOR gap is explicitly documented.actions/checkoutis invoked withoutref:underpull_request_target, which checks out the trusted base — executed scripts are not from the PR.- Top-level
permissions: {}; per-jobcontents: readonly. - All third-party actions SHA-pinned (
actions/checkout@de0fac2e…,actions/create-github-app-token@bcd2ba49…). - Auth is a short-lived App installation token minted per run via
petry-projects-plannerApp, not a long-lived PAT. - Scripts use
set -euo pipefail; GraphQL queries use-F/-fvariables (no shell interpolation into query bodies);LABELS_JSONis passed via env, not interpolated into a script.
Implementation quality
evaluate_noise_gateruns the policy in a singlejqinvocation; distinguishes clean skip (exit 1) from programmer/payload bug (exit 64/65) so silent drops are impossible.find_existing_draft_idpaginates correctly, hard-fails ondata.node:null(exit 75) instead of silently allowing duplicates, and warns on multi-match without crashing under pipefail.delete_project_itemis idempotent on "Could not resolve to a node" (handles redelivered webhooks / racing runs) while still propagating real errors.reconcile_discussioncorrectly treats emptyDISC_CATEGORY(deleted/transferred payloads) as non-Ideas, so a tracked draft is cleaned up.- Concurrency group keyed on content number across event subtypes, preventing
createdvs.category_changedraces for the same discussion.
Tests
- bats suite covers: all 4 corners of the discussion state machine, prefix-anchoring (
#42≠#420), pagination cursor value (not just substring),data.node:nullhard-fail, DraftIssue→Issue conversion still findable, idempotent delete, real-error propagation, and noise-gate variants. - Gh stub mechanism mirrors the established
feature-ideationpattern. add-to-project-tests.ymlruns shellcheck + bats on every PR touching these paths.
Trivia
standards/workflows/dev-lead.ymlchange is just adding a trailing newline — harmless.
No blocking issues. Note that the workflow won't function until org secrets INITIATIVES_APP_ID / INITIATIVES_APP_PRIVATE_KEY are configured (the PR body's prerequisite checklist captures this and the script will fail loudly with an ::error:: annotation if they're missing).
CI status
All required checks green: Shellcheck and bats, agent-shield, Lint, CodeQL, SonarCloud (×3 + Code Analysis), Secret scan (gitleaks), Agent Security Scan, ShellCheck, CodeRabbit, pr-auto-review, pr-review-mention. Language-specific audits (npm, pnpm, govulncheck, cargo, pip) correctly skipped — no manifest changes. CodeRabbit moved from CHANGES_REQUESTED → APPROVED after iteration.
Reviewed automatically by the PR-review agent (single-reviewer mode: opus 4.7). Reply if you need a human review.
* feat(workflows): add Initiatives project auto-add workflow Adds qualifying issues, PRs, and Ideas-category discussions to https://github.com/orgs/petry-projects/projects/1 via the noise gate defined in #387. - Issues/PRs: require dev-lead label and exclude compliance-audit / health-check / fleet-tracker / daily-report - Discussions: only Ideas category, added as draft items (API limitation prevents content-linking discussions to Projects v2) Requires org secret PROJECTS_TOKEN with Projects: Read+write scope. Refs #387, discussion #386 * fix(workflows): address PR #388 review and shellcheck SC2016 Lint: - Add shellcheck disable=SC2016 on run blocks (the single-quoted GraphQL query strings contain GraphQL variables, not shell variables — intentional) Review feedback addressed: - Drop discussion.types: labeled — was producing duplicate draft items on each label change to the same Ideas discussion (CodeRabbit, Codex, Copilot all flagged this) - Add explicit PROJECTS_TOKEN presence check at the top of both run blocks so missing-secret failures are actionable (Copilot) - Gate pull_request_target on trusted author_association so fork PRs cannot trigger a secret-bearing runner (Copilot) - Drop top-level permissions; declare permissions: {} per job (Codex) - Document the multi-repo scope limit in the file header (Copilot, Codex P1) Deferred for follow-on (not in scope for the pilot): - Multi-repo rollout via reusable workflow (Codex P1) — needs a separate design decision; documented as known limit in file header - Project item cleanup when label set later becomes excluded (Codex P2) - App installation token rotation if PROJECTS_TOKEN comes from an App (Codex P2) - Token scope for repo read on private repos (Codex P2) Refs #387, discussion #386 * feat(workflows): use petry-projects-planner App for token (PR #388) Replace PROJECTS_TOKEN with a fresh installation token minted per run via actions/create-github-app-token@bcd2ba49 (v3.2.0, same SHA already pinned by dependabot-rebase-reusable.yml and dependabot-automerge-reusable.yml). App: petry-projects-planner (App ID 3985527) Org secrets: INITIATIVES_APP_ID, INITIATIVES_APP_PRIVATE_KEY (scoped to .github) App permissions: Org Projects R+W, Repo Issues + PRs R-only Addresses Codex P2: 'Generate App installation tokens per run' — installation tokens have 1-hour lifetime, so a static long-lived PAT was the wrong shape. Drops the PROJECTS_TOKEN empty-check since the App-token step fails fast with its own error if secrets are missing. Refs #387, discussion #386 * fix(workflows): move shellcheck disable inside run blocks The shellcheck disable comment was at YAML indentation level (treated as a YAML comment, not visible to shellcheck). Moving it inside the run: | block as a bash comment so SC2016 actually gets suppressed. Refs #387 * fix(workflows): per-command shellcheck disable for SC2016 Match the org's existing pattern (ci-failure-analyst-reusable.yml:121-126) of placing the disable directive immediately before the offending command, not at script top. Top-of-script disable was being treated as next-command-only. Refs #387 * fix(workflows): add top-level permissions: {} and document fork-PR gap Addresses two new findings from Codex on commit e212c57: 1. Add top-level permissions: {} so compliance-audit.sh check_workflow_permissions doesn't flag this multi-job workflow. Job-level permissions: {} stays for least-privilege; the App-minted token handles all API work, GITHUB_TOKEN is unused. 2. Document the fork-PR gap in the file header: author_association gate evaluates the PR author, not the labeler, so fork PRs from FIRST_TIMER contributors won't auto-add even after a maintainer labels dev-lead. Workaround: manual add via UI. Defer the fix to the multi-repo follow-on PR (will need a labeler-association check). Refs #387 * feat(workflows): reconcile discussion drafts on category change Replace the add-only `add-discussion` job with `reconcile-discussion` that handles all four corners of the discussion state machine: - Ideas + no existing draft → add (original behavior) - Ideas + existing draft → skip (idempotent; also dedupes any future re-entry into Ideas) - non-Ideas + existing draft → delete (cleanup; addresses the Codex P2 finding on commit 5bfe035) - non-Ideas + no existing draft → no-op Lookup matches by title prefix "[Discussion #N] " against the project's draft items (paginated at 100; ~15 today, years of headroom). Uses the same app-minted token for both query and mutate paths. Updates file header to document the new state machine. Removes the implicit OUT-of-Ideas gap from the known-limits section. Refs #387, addresses Codex P2 on PR #388 review 4444371481 * test(add-to-project): extract logic into scripts and add bats suite Addresses Codex P2 on PR review 4444371481 (paginate project item lookup) and the user request to add test coverage. Layout (mirrors test/workflows/feature-ideation/): .github/scripts/add-to-project/ add-issue-or-pr.sh sourceable functions: evaluate_noise_gate, add_content_to_project, process_issue_or_pr reconcile-discussion.sh sourceable functions: find_existing_draft_id (paginated), add_discussion_draft, delete_project_item, reconcile_discussion test/workflows/add-to-project/ helpers/setup.bash tmpdir, gh stub install (copy + chmod) stubs/gh fake gh with single- and multi-call modes add-issue-or-pr.bats 12 tests covering noise gate, all 4 skip reasons, and the happy path reconcile-discussion.bats 10 tests covering all 4 state machine corners, title-prefix anchoring (#42 vs #420), and pagination (match on page 2, no match across all pages) .github/workflows/add-to-project-tests.yml PR/push gate: shellcheck + bats .github/workflows/add-to-project.yml thin shell: checkout, mint App token, call the script with env Pagination fix: find_existing_draft_id now loops with after:cursor until hasNextPage is false, so projects beyond 100 items still match correctly. At ~16 items today, this is preventative. Local validation (donpetry@host): bats 1.13.0, jq 1.7, shellcheck 0.10.0: - shellcheck -S warning: all scripts + harness clean - bats test/workflows/add-to-project/: 22 of 22 pass Refs #387 * fix(add-to-project): apply /code-review findings; harden scripts and tests Correctness fixes (review findings 1-11): 1. evaluate_noise_gate: coerce non-array LABELS_JSON to [] so events with null labels don't abort the script via jq error. 2. evaluate_noise_gate: collapse 5 jq calls to 1, return distinct codes (0=ok, 1=skip, 64=arg-bug, 65=unexpected shape). 3. process_issue_or_pr: distinguish gate-said-skip (log+continue) from gate-errored (propagate to mark workflow failed); previously every non-zero became a silent 'Skip'. 4. find_existing_draft_id: parse match+pageInfo in ONE jq call using first(...); avoids SIGPIPE on jq | head -n 1 under pipefail when multiple candidates match on a page. 5. find_existing_draft_id: fail loudly (exit 75) when data.node is null (wrong PROJECT_ID, token scope drift) instead of silently returning no-match and letting the caller add duplicates. 6. find_existing_draft_id: warn when multiple drafts share a prefix rather than silently picking one — surfaces the inconsistency. 7. find_existing_draft_id: also resolve Issue.title and PullRequest.title so a 'Convert to issue' on an existing draft doesn't orphan the reconciliation (item becomes invisible to the lookup). 8. add_*/delete_*: redirect gh's GraphQL JSON to /dev/null so the functions don't leak responses to stdout when called by . 9. delete_project_item: catch 'Could not resolve to a node' as idempotent success (handles webhook redelivery). 10. Both scripts: explicit empty-GH_TOKEN check with ::error:: annotation pointing at #387 (regression from PR #388's earlier cleanup pass). 11. Discussion triggers: add 'deleted' and 'transferred' so orphan drafts get cleaned up when an Ideas discussion vanishes. 12. Workflow concurrency: group discussion events by discussion number alone (not event_name) so 'created' and 'category_changed' for the same discussion serialize — closes a real race window. Test additions (review findings 12-15): - assert_invocation_count on every happy path: a regression that adds the same item twice now fails the test. - Pagination test asserts the EXACT cursor value (MY_DISTINCTIVE_CURSOR) was passed on call 2, not just that the substring 'cursor' appears. - Ideas-add test asserts the body contains 'Source:' + URL + the 'Auto-added from Ideas-category' marker. - PROJECT_ID/GH_TOKEN required tests use 'run --separate-stderr' so they assert against $stderr directly, no longer relying on bats's default stdout+stderr merge. - New tests: data.node:null → exit 75; multiple matches → warn+pick first; Issue.title match; delete idempotency on 'Could not resolve'; delete propagates real errors; null/non-array LABELS_JSON treated as empty. Local validation: bats 1.13.0: 33 of 33 pass shellcheck -S warning: clean Refs #387 * style(workflows): wrap concurrency-group expression to satisfy yamllint line-length (200) The new conditional grouping (disc-N for discussion events vs event_name-N for issues/PRs) is too long to fit on one line. Use a folded scalar (>-) so newlines fold to spaces inside the ${{ ... }} expression — GitHub Actions tokenizes whitespace identically. * fix(add-to-project): handle empty DISC_CATEGORY for deleted/transferred + fix standards/dev-lead.yml newline Codex flagged (PR #388 review on f1641ca): the deleted and transferred discussion payloads can deliver an empty discussion.category, so ${DISC_CATEGORY:?required} would hard-fail the script before reconcile_discussion ever ran. Switch the main runner's default from :?required to :- (empty allowed). reconcile_discussion's non-Ideas branch already treats any non-'Ideas' value (including '') as 'if a draft exists, clean it up', which is the desired behavior for deleted/transferred. New bats coverage: - 'empty category (deleted/transferred payload) + existing draft → delete' - 'empty category + no existing draft → no-op' Also: append the missing trailing newline to standards/workflows/dev-lead.yml that was introduced by the main-merge in e51a17b — yamllint's new-line-at-end-of-file rule was blocking the Lint gate on this PR. Local: bats 35/35 pass, shellcheck clean. Refs #387



Summary
Adds
add-to-project.yml, the auto-add workflow for the org-level Initiatives project (https://github.com/orgs/petry-projects/projects/1). This is the scoped deliverable from #387, which tracks the broader GitHub Projects pilot proposed in discussion #386.The workflow:
dev-leadlabel and none ofcompliance-audit/health-check/fleet-tracker/daily-report. That noise gate is the difference between the project being a useful strategic plane and a duplicate ofgh issue list.Discussionin theProjectV2ItemContentunion; content-linking is not available).No third-party actions used — all logic is shell +
gh api graphqlcalls — so there's nothing to pin and nothing for the compliance audit to flag.Prerequisites before merge
PROJECTS_TOKENwithProjects: Read and writescope onpetry-projects. A fine-grained PAT or a GitHub App installation token both work. The workflow will not function until this secret exists; until then, runs will fail at the firstgh api graphqlcall.Test plan
dev-lead. Confirm it auto-appears in the project.dev-lead+compliance-audit. Confirm it does not appear (noise gate).Ideascategory. Confirm a draft item appears in the project with the discussion URL in the body.Related
Summary by CodeRabbit