Skip to content

feat(workflows): add Initiatives project auto-add workflow#388

Merged
don-petry merged 13 commits into
mainfrom
feat/add-to-project-workflow
Jun 7, 2026
Merged

feat(workflows): add Initiatives project auto-add workflow#388
don-petry merged 13 commits into
mainfrom
feat/add-to-project-workflow

Conversation

@don-petry

@don-petry don-petry commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

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:

  • Issues / PRs — adds the item to the project only when it has the dev-lead label and none of compliance-audit / health-check / fleet-tracker / daily-report. That noise gate is the difference between the project being a useful strategic plane and a duplicate of gh issue list.
  • Discussions — adds Ideas-category discussions as draft items (the Projects v2 GraphQL schema does not include Discussion in the ProjectV2ItemContent union; content-linking is not available).

No third-party actions used — all logic is shell + gh api graphql calls — so there's nothing to pin and nothing for the compliance audit to flag.

Prerequisites before merge

  • Create org-level secret PROJECTS_TOKEN with Projects: Read and write scope on petry-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 first gh api graphql call.

Test plan

  • Merge with secret in place
  • Smoke-test 1 — open a throwaway issue in any repo, apply dev-lead. Confirm it auto-appears in the project.
  • Smoke-test 2 — open a throwaway issue, apply dev-lead + compliance-audit. Confirm it does not appear (noise gate).
  • Smoke-test 3 — open a throwaway discussion in Ideas category. Confirm a draft item appears in the project with the discussion URL in the body.
  • Smoke-test 4 — open a discussion in a non-Ideas category. Confirm nothing is added.

Related

Summary by CodeRabbit

  • New Features
    • Added automated GitHub Actions workflow to streamline project management. Automatically adds qualifying issues, pull requests, and discussions to the "Initiatives" Projects board based on configured labels and discussion categories.

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
@don-petry don-petry requested a review from a team as a code owner June 4, 2026 11:50
@don-petry don-petry added the dev-lead For dev-lead agent pickup label Jun 4, 2026
Copilot AI review requested due to automatic review settings June 4, 2026 11:50
@don-petry don-petry added the dev-lead For dev-lead agent pickup label Jun 4, 2026
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Note

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

@coderabbitai

coderabbitai Bot commented Jun 4, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@don-petry, we couldn't start this review because you've reached your PR review rate limit.

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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: aa0dcd41-49a8-4eba-9038-1706966fbdf1

📥 Commits

Reviewing files that changed from the base of the PR and between dee5846 and 9390135.

📒 Files selected for processing (9)
  • .github/scripts/add-to-project/add-issue-or-pr.sh
  • .github/scripts/add-to-project/reconcile-discussion.sh
  • .github/workflows/add-to-project-tests.yml
  • .github/workflows/add-to-project.yml
  • standards/workflows/dev-lead.yml
  • test/workflows/add-to-project/add-issue-or-pr.bats
  • test/workflows/add-to-project/helpers/setup.bash
  • test/workflows/add-to-project/reconcile-discussion.bats
  • test/workflows/add-to-project/stubs/gh
📝 Walkthrough

Walkthrough

A new GitHub Actions workflow automatically routes qualifying content to the org "Initiatives" Projects v2 board. Issues and PRs require the dev-lead label and exclude specific labels; discussions must be in the Ideas category. Matching items are added to the project via GraphQL API calls.

Changes

Auto-add to Projects workflow

Layer / File(s) Summary
Workflow configuration and environment setup
.github/workflows/add-to-project.yml
Event triggers for issues, pull_request_target, and discussions; minimal permissions and concurrency group; environment variables for Projects v2 project ID and URL.
Issue and PR auto-addition with label validation
.github/workflows/add-to-project.yml
add-issue-or-pr job validates that the event has the required dev-lead label and lacks excluded labels, then calls the Projects v2 GraphQL API to add the issue/PR node as a project item.
Discussion auto-addition with category filtering
.github/workflows/add-to-project.yml
add-discussion job filters discussions by Ideas category, constructs a draft item with title and body from discussion metadata, and calls the GraphQL API to create a draft project item.

Sequence Diagram

sequenceDiagram
  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
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related issues

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: adding a GitHub Actions workflow that auto-adds items to the Initiatives project board.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/add-to-project-workflow

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.

❤️ Share

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.yml to add issues/PRs labeled dev-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).

Comment thread .github/workflows/add-to-project.yml
Comment thread .github/workflows/add-to-project.yml
Comment thread .github/workflows/add-to-project.yml
Comment thread .github/workflows/add-to-project.yml Outdated
Comment thread .github/workflows/add-to-project.yml Outdated

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 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".

Comment thread .github/workflows/add-to-project.yml Outdated
Comment thread .github/workflows/add-to-project.yml Outdated
Comment thread .github/workflows/add-to-project.yml Outdated
Comment thread .github/workflows/add-to-project.yml

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 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

📥 Commits

Reviewing files that changed from the base of the PR and between 55b2e7d and dee5846.

📒 Files selected for processing (1)
  • .github/workflows/add-to-project.yml

Comment thread .github/workflows/add-to-project.yml Outdated

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 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".

Comment thread .github/workflows/add-to-project.yml Outdated
Comment thread .github/workflows/add-to-project.yml Outdated
Comment thread .github/workflows/add-to-project.yml Outdated
@don-petry

Copy link
Copy Markdown
Contributor Author

@dev-lead Please action the unresolved Codex review comments on this PR. (Manual re-trigger — #445 now trusts chatgpt-codex-connector[bot].)

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
@don-petry

Copy link
Copy Markdown
Contributor Author

Pushed ff69ff0 addressing the actionable review findings and the Lint failure. Summary by reviewer:

Addressed in this push

Lint (CI)

  • shellcheck SC2016 on lines 45/88 was correctly flagging single-quoted strings that look like shell expansions but are actually GraphQL variables ($projectId, $contentId, $title, $body). Added # shellcheck disable=SC2016 on the two run: blocks — single-quoting the GraphQL query is the right thing.

CodeRabbit — "Remove discussion:labeled or dedupe drafts" (CHANGES_REQUESTED)

  • Dropped labeled from discussion.types. Keeps created and category_changed. Ideas-category discussions added once; renaming/relabeling won't duplicate.

Copilot — pull_request_target exposes PROJECTS_TOKEN to forks

  • Gated add-issue-or-pr on author_association ∈ {OWNER, MEMBER, COLLABORATOR} for the pull_request_target case. Issues stay ungated (issue creators can't apply privileged labels themselves).

Copilot — opaque failure when PROJECTS_TOKEN missing

Codex P2 — Reset top-level token permissions

  • Removed top-level permissions: contents: read. Both jobs declare permissions: {} (the jobs use PROJECTS_TOKEN, not GITHUB_TOKEN, so they need nothing).

Copilot / Codex P1 — Multi-repo deployment gap

  • This is real. on: only fires for events in petry-projects/.github. Documented as a known scope limit in the file header rather than fixing in this PR — the right fix is a reusable workflow callable from each repo (or an org webhook handler), which is a meaningful design decision that should be its own PR. The pilot still adds value: it auto-adds the meta-work (this PR, future Adopt GitHub Projects: org-level Initiatives pilot #387-style tracking issues, and any Ideas discussion since those land in .github).

Deferred to follow-on work

Finding Reviewer Why deferred
Multi-repo rollout via reusable workflow Codex P1, Copilot Architectural choice (reusable workflow vs. org webhook handler) — own PR after 30-day pilot review
Remove item from project when later labeled excluded Codex P2 Stale-item cleanup is a separate "cleanup" workflow scope; doesn't block correctness on adds
Handle label removals (unlabeled trigger) Codex P2 Same as above
Generate App installation tokens per run Codex P2 Only relevant if PROJECTS_TOKEN ends up sourced from a GitHub App. Decision point on token provisioning is in #387; if we go App, this comment should be re-opened
Grant Contents: Read to PROJECTS_TOKEN on private repos Codex P2 Depends on the previous decision — the token-setup docs in #387 will spell out the minimum scope set

@don-petry — these last three deferred items hinge on how PROJECTS_TOKEN gets provisioned. Calling it out here so the discussion in #387 captures the choice once you decide between fine-grained PAT vs. GitHub App.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 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".

Comment thread .github/workflows/add-to-project.yml
Comment thread .github/workflows/add-to-project.yml
don-petry added 3 commits June 6, 2026 22:53
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
@don-petry

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

The duplicate-discussion concern (line 20-21) is addressed in 4af76ce: dropped labeled from discussion.types. Kept created and category_changed only, so re-labeling an Ideas discussion no longer creates a new draft item.

Workflow has since been further updated to use a dedicated GitHub App (petry-projects-planner) for token minting — see the rest of the PR thread for context.

@coderabbitai

coderabbitai Bot commented Jun 7, 2026

Copy link
Copy Markdown

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,commits

Length of output: 11336



@don-petry — reviewed the current file state. Here's the full picture:


✅ All prior blocking concerns resolved

Finding Fix Status
Duplicate drafts via discussion: labeled Dropped labeled from discussion.types; kept created + category_changed ✅ Resolved
Static long-lived PROJECTS_TOKEN Replaced with actions/create-github-app-token minting a fresh installation token per run (petry-projects-planner, App ID 3985527) ✅ Resolved
Fork PR secret exposure (pull_request_target) Gated on author_association ∈ {OWNER,MEMBER,COLLABORATOR} ✅ Resolved
Over-broad permissions Both jobs now declare permissions: {} ✅ Resolved
shellcheck SC2016 Per-command # shellcheck disable=SC2016 inside the run: block (matches org pattern from ci-failure-analyst-reusable.yml) ✅ Resolved
Opaque missing-token failure Dropped the explicit check — actions/create-github-app-token fails fast with its own actionable error if secrets are absent ✅ Resolved

🔍 Observations (no blockers)

  1. category_changed duplicate-draft edge case — if a discussion bounces Ideas → Other → Ideas, a second draft item would be created. This is an acceptable pilot-scope limitation and is covered by the deferred cleanup work. No action needed here.

  2. addProjectV2ItemById idempotency for issues/PRs — the labeled trigger on issues and pull_request_target can fire multiple times on the same item (e.g., multiple label applications). Projects v2 addProjectV2ItemById is idempotent: re-adding an already-present item returns the existing item's ID without creating a duplicate. ✅ No action needed.

  3. Action pinactions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 matches the SHA already pinned org-wide in dependabot-rebase-reusable.yml. Dependabot will keep it current. ✅

Verdict

The workflow is correct, secure, and ready to merge once the INITIATIVES_APP_ID / INITIATIVES_APP_PRIVATE_KEY org secrets are provisioned for petry-projects-planner. The deferred items (multi-repo rollout, stale-item cleanup, label-removal trigger) are appropriately documented in the file header and tracked in #387.

[approve]


🧠 Learnings used
Learnt from: don-petry
Repo: petry-projects/.github PR: 0
File: :0-0
Timestamp: 2026-04-08T03:11:34.650Z
Learning: In `petry-projects/.github` `.github/workflows/dependabot-rebase-reusable.yml`, ALL write operations (update-branch via `gh api PUT /repos/.../pulls/{number}/update-branch` and merge via `gh api PUT /repos/.../pulls/{number}/merge`) are performed using a GitHub App token generated by `actions/create-github-app-token` from `APP_ID`/`APP_PRIVATE_KEY` secrets — NOT via `GITHUB_TOKEN`. The caller's `permissions:` block governs only `GITHUB_TOKEN` scopes, so read-only permissions (`contents: read`, `pull-requests: read`) are correct and sufficient. Granting `contents: write` would be incorrect because: (1) no write call uses `GITHUB_TOKEN`, and (2) the GitHub App token is intentionally used so that the resulting branch push triggers `on: push` in this same workflow (self-sustaining merge chain) — `GITHUB_TOKEN`-authored pushes cannot trigger workflows and would break this chain. Do not suggest adding `contents: write` to the caller or reusable for this workflow.

Learnt from: CR
Repo: petry-projects/.github PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-05-11T20:21:19.083Z
Learning: Dependabot auto-merge workflow automatically resolves AI reviewer threads on patch/minor dependency bumps

Learnt from: CR
Repo: petry-projects/.github PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-05-11T20:21:19.083Z
Learning: All review comment threads MUST be resolved before merge via the `pr-quality` ruleset

Learnt from: CR
Repo: petry-projects/.github PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-05-21T12:16:44.942Z
Learning: All PRs must pass CI quality gates before merge: CodeQL (SAST), SonarCloud (quality), project linter at zero errors, type-checker, full test suite at coverage threshold, CodeRabbit and Copilot review comments resolved. Never bypass or weaken thresholds.

Learnt from: CR
Repo: petry-projects/.github PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-05-11T20:21:19.083Z
Learning: Address CodeRabbit and Copilot review comments the same way you address human reviewer comments

Learnt from: don-petry
Repo: petry-projects/.github PR: 24
File: standards/ci-standards.md:165-165
Timestamp: 2026-04-05T20:25:07.664Z
Learning: In `petry-projects` org workflows, the `anthropics/claude-code-action` is pinned to the SHA corresponding to the specific release version (e.g., `anthropics/claude-code-action6e2bd52842c65e914eba5c8badd17560bd26b5de # v1.0.89`). The version comment reflects the specific release tag (e.g., `v1.0.89`), not the floating `v1` tag. All org repos standardize on the same pinned SHA, and Dependabot (github-actions ecosystem) keeps the SHA up to date. Do not flag the specific version comment as incorrect — `# v1.0.89` is intentional and the current org-wide standard.

Learnt from: CR
Repo: petry-projects/.github PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-05-11T20:21:19.083Z
Learning: Mark each resolved PR review comment thread as Resolved on GitHub after the fix is pushed

Learnt from: don-petry
Repo: petry-projects/.github PR: 15
File: .github/workflows/dependency-audit.yml:173-173
Timestamp: 2026-04-05T18:54:48.169Z
Learning: In the petry-projects org, `.github/workflows/dependency-audit.yml` (and likely other standard CI workflows) are copies of an upstream template. Any bug fixes or improvements must be applied to the template source, not to the individual repository copies. Do not suggest inline changes to per-repo workflow copies; instead, note that the fix belongs in the template.

Learnt from: don-petry
Repo: petry-projects/.github PR: 169
File: scripts/org_status.sh:0-0
Timestamp: 2026-05-02T01:58:39.456Z
Learning: In `scripts/org_status.sh` (petry-projects/.github repo), the GraphQL fetch fallback `|| result='{"data":{"repository":{"pullRequests":{"pageInfo":{"hasNextPage":false},"nodes":[]}}}}` in the `collect_classify_prs` function is intentional. Repos without PR/issue features (e.g. archived or wiki-only repos) return a GraphQL error, which is treated as "no PRs" so the script continues processing remaining repos. Genuine failures are caught by `set -euo pipefail` at the top of the script, and the workflow's empty-report guard (`[ -s /tmp/report.md ] || exit 1`) provides an additional safety net.

Learnt from: CR
Repo: petry-projects/.github PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-05-11T20:21:19.083Z
Learning: When multiple autonomous agents work from the same issue queue, they MUST coordinate via GitHub labels and PR checks to prevent duplicate work

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
@don-petry

Copy link
Copy Markdown
Contributor Author

Status as of 5bfe035

CI lineup (5 required: Lint, ShellCheck, Agent Security Scan, SonarCloud, CodeQL):

  • 4af76ce fixed the SC2016 disable position (was at YAML indent, treated as next-command-only)
  • e212c57 moved disables to per-command (matching ci-failure-analyst-reusable.yml pattern) — Lint started passing
  • 5bfe035 adds top-level permissions: {} (Codex P2 on e212c57 was correct — compliance-audit.sh check_workflow_permissions flags multi-job workflows missing it) and documents the fork-PR gap as a known limit deferred to multi-repo follow-on

Findings status

Addressed:

  • Lint SC2016 — e212c57
  • CodeRabbit duplicate discussion — 4af76ce (CodeRabbit's own thread now isResolved: true)
  • Copilot pull_request_target exposure — ff69ff0 (author_association gate)
  • Copilot opaque auth-failure messaging — superseded by 84faef0 (App token mints fast-fail on its own)
  • Codex multi-repo scope — ff69ff0 documented as known limit
  • Codex reset top-level permissions — 5bfe035
  • Codex App installation token per run — 84faef0 (App-token step minted each run via actions/create-github-app-token@bcd2ba49)
  • Codex repo read scopes — handled by App permission setup (petry-projects-planner granted Issues + PRs Read)

Deferred (documented as known limits for the multi-repo follow-on):

  • Codex P2: remove item when later excluded label is applied
  • Codex P2: handle label removals (unlabeled trigger + reconcile)
  • Codex P2 on e212c57: maintainer-labeled fork PRs not added (author_association evaluates PR author, not labeler)
  • Multi-repo deployment (Codex P1) — needs separate design decision (reusable workflow vs. webhook handler)

What's left to merge

  • Required checks green (CodeQL still pending, all others pass)
  • Code owner approval from @petry-projects/org-leads (review request open)
  • Review threads resolved (~14 outdated bot threads — many are now isOutdated: true)

Ready for code-owner review.

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jun 7, 2026

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 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".

Comment thread .github/workflows/add-to-project.yml Outdated
donpetry-bot
donpetry-bot previously approved these changes Jun 7, 2026

@donpetry-bot donpetry-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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_target is gated on contains(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-job permissions: {} are both set; the workflow uses the App-minted token via GH_TOKEN rather than GITHUB_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 pipefail on both run blocks; SC2016 disables are correctly scoped per-command.

Correctness:

  • CodeRabbit's prior actionable finding (duplicate draft items from discussion:labeled) was resolved in 4af76cediscussion.types is now [created, category_changed]. CodeRabbit subsequently re-approved at this SHA.
  • Concurrency group keys on event_name + item number, with cancel-in-progress: false — appropriate for additive mutations.
  • Noise gate logic (dev-lead required, 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_changed twice 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_TOKEN secret approach; the workflow has since moved to the petry-projects-planner App (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
@don-petry don-petry dismissed stale reviews from donpetry-bot and coderabbitai[bot] via 20e3459 June 7, 2026 04:29
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jun 7, 2026

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 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".

Comment thread .github/workflows/add-to-project.yml Outdated
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
@don-petry

Copy link
Copy Markdown
Contributor Author

Review pass + fixes — b9d9be8

Ran a 9-angle code-review (extra-high recall mode) over the scripts/tests/workflow added in 334f2fb. After dedup and sweep, 15 findings. Of those, 11 actionable correctness/observability bugs are fixed in b9d9be8 and 4 test-fragility findings are addressed in the same commit. Two altitude/architectural findings (hard-coded labels/category, generalized reconcile across content types) are deferred to the multi-repo follow-on — they're real but out of scope for the pilot.

Correctness fixes applied

# 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 use run --separate-stderr so they assert against $stderr directly — they don't depend on bats' default stream merge.
  • New tests for: data.node:null → exit 75; multi-match → warn+pick-first; Issue.title matched; delete idempotency on Could not resolve; delete propagates real errors; null/non-array LABELS_JSON treated as empty.

Bats locally: 33/33 pass. Shellcheck -S warning: clean.

Deferred (architectural, out-of-pilot)

  • Hard-coded dev-lead + 4 excluded labels in evaluate_noise_gate — should be env-driven when a second Project lands.
  • Hard-coded Ideas category in reconcile_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.
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jun 7, 2026

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 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".

Comment thread .github/workflows/add-to-project.yml
don-petry added 2 commits June 7, 2026 00:58
…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
@sonarqubecloud

sonarqubecloud Bot commented Jun 7, 2026

Copy link
Copy Markdown

@don-petry don-petry requested a review from donpetry-bot June 7, 2026 13:03
@donpetry-bot

Copy link
Copy Markdown
Contributor

@don-petry assigned me as reviewer — starting a fresh review now. Results will appear in a few minutes.

@donpetry-bot donpetry-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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_target is gated on contains(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 by dependabot-rebase-reusable.yml.
  • Top-level permissions: {} (compliance audit) and per-job permissions: { contents: read } (minimum for checkout). The App-minted installation token (1h TTL) is used for the GraphQL API; GITHUB_TOKEN is unused, so the empty-ish job permissions are correct.
  • All GraphQL mutations use parameterized variables ($projectId, $contentId, $title, $body) bound via gh 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 pipefail on every script; per-command # shellcheck disable=SC2016 is correctly scoped (matches the org pattern from ci-failure-analyst-reusable.yml).
  • Discussion concurrency group keys on disc-<N> for discussion events (vs. <event>-<N> for issues/PRs), so created and category_changed for the same discussion serialize — closes the state-machine race window flagged earlier.

Correctness:

  • evaluate_noise_gate collapses to a single jq invocation and returns distinct codes (0/1/64/65); process_issue_or_pr propagates programmer/payload bugs rather than swallowing them as a silent skip.
  • find_existing_draft_id paginates with cursor-passing (matches anywhere in the project, not just the first 100 items), fails loudly with exit 75 on data.node:null, and warns on multi-match rather than silently picking one.
  • delete_project_item catches "Could not resolve to a node" as idempotent success — correct for redelivered webhooks / racing runs.
  • The DISC_CATEGORY:- default in the runner correctly pairs with reconcile_discussion's non-Ideas branch to clean up drafts on deleted and transferred payloads (where the category can be empty/null).
  • addProjectV2ItemById is idempotent on the issues/PRs side, so re-firing on multiple labeled events 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.yml gate runs shellcheck + bats on every PR touching the surface.

Prior reviewer feedback — all resolved at head:

  • CodeRabbit's discussion: labeled duplicate-drafts finding → fixed in 4af76ce (dropped labeled from discussion.types); CodeRabbit re-approved at 9390135bc.
  • Copilot's pull_request_target fork-PR exposure → fixed via the author_association gate.
  • Codex P2 "generate App installation tokens per run" → fixed via the petry-projects-planner App migration in 84faef0.
  • 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_TOKEN secret; the workflow has since moved to INITIATIVES_APP_ID / INITIATIVES_APP_PRIVATE_KEY via 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.yml change is a trailing-newline fix (yamllint new-line-at-end-of-file) introduced by the main-merge in e51a17b — 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.

@don-petry don-petry merged commit a30b59b into main Jun 7, 2026
26 checks passed
@don-petry don-petry deleted the feat/add-to-project-workflow branch June 7, 2026 13:09

@donpetry-bot donpetry-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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_target job gated on author_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/checkout is invoked without ref: under pull_request_target, which checks out the trusted base — executed scripts are not from the PR.
  • Top-level permissions: {}; per-job contents: read only.
  • 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-planner App, not a long-lived PAT.
  • Scripts use set -euo pipefail; GraphQL queries use -F/-f variables (no shell interpolation into query bodies); LABELS_JSON is passed via env, not interpolated into a script.

Implementation quality

  • evaluate_noise_gate runs the policy in a single jq invocation; distinguishes clean skip (exit 1) from programmer/payload bug (exit 64/65) so silent drops are impossible.
  • find_existing_draft_id paginates correctly, hard-fails on data.node:null (exit 75) instead of silently allowing duplicates, and warns on multi-match without crashing under pipefail.
  • delete_project_item is idempotent on "Could not resolve to a node" (handles redelivered webhooks / racing runs) while still propagating real errors.
  • reconcile_discussion correctly treats empty DISC_CATEGORY (deleted/transferred payloads) as non-Ideas, so a tracked draft is cleaned up.
  • Concurrency group keyed on content number across event subtypes, preventing created vs. category_changed races 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:null hard-fail, DraftIssue→Issue conversion still findable, idempotent delete, real-error propagation, and noise-gate variants.
  • Gh stub mechanism mirrors the established feature-ideation pattern.
  • add-to-project-tests.yml runs shellcheck + bats on every PR touching these paths.

Trivia

  • standards/workflows/dev-lead.yml change 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.

don-petry added a commit that referenced this pull request Jun 11, 2026
* 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dev-lead For dev-lead agent pickup

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants