Skip to content

Reconcile the project board from labels - #104

Merged
rocklambros merged 2 commits into
integrationfrom
feat/board-reconciler
Sep 10, 2026
Merged

rocklambros merged 2 commits into
integrationfrom
feat/board-reconciler

Conversation

@rocklambros

Copy link
Copy Markdown
Contributor

Adds a board step to the Phase 2 executor so the Kanban reflects labels instead of drifting away from them.

Why not a GitHub Action

The obvious answer is a workflow, and it was rejected on evidence. The project is organization-owned and a workflow's GITHUB_TOKEN is repository-scoped, so it cannot write to the board at all. Making it work means storing a personal access token with organization-wide project write as a repository secret, in a repository that opened to a hundred contributors this morning, where anyone who can land a workflow file can reach it. This runs under a maintainer's own credentials instead, idempotent and dry-run by default, exactly like every other Phase 2 step. A GitHub App is the right long-term answer and is separate work.

What it does

Precedence, highest first. State beats every label, and a blocked item says so before it says anything else.

  1. closed or merged, to Done
  2. status:blocked, to Blocked
  3. an open pull request, to In progress
  4. status:accepted, to Accepted
  5. scope:deferred, to Deferred
  6. otherwise, to Needs triage

Rung 3 was missing from the first draft and would have caused real damage. The In progress column holds nine open pull requests, none of them labelled, so the original mapping resolved every one to Needs triage and the first run would have swept the column. An open pull request is work in flight by definition and needs no label to prove it.

In progress is sticky for issues. GitHub's built-in "Pull request linked to issue" workflow parks an issue there, and nothing in the label set records that, so a reconciler that recomputed it would move the issue back on every run and the two automations would fight forever. If an open issue already sits at In progress, this leaves it alone.

Safety

The tool remains structurally incapable of merging, closing, or deleting anything, and the existing assertion now covers the board step's full desired state. It sets project field values and adds project items. Nothing else.

Status option ids are fetched at runtime, never hardcoded. They regenerate whenever the field's options are edited, which has already happened once here, and a test greps the source for the current literals to prove none was baked in.

Verified against the live board, not a fixture

A dry run reported nothing to do, which is also what a broken reconciler reports, so it was proven by perturbation instead. Moving #92 from Accepted to Needs triage produced exactly one proposed action naming the right item and the right target. Applying it converged, and a second run reported nothing to do.

276 tests pass.

Reconciles the org-level project board (GenAI-Security-Project/agent-control-standard
projects/9) from the same label taxonomy the rest of this tool already applies.
GitHub's built-in project workflows cover add, close, and merge, but not the move a
maintainer's label triggers, so merged pull requests were sitting in In progress and
27 items were missing from the board entirely.

desired_board_status maps an issue or pull request's labels, state, and issue-vs-pull-
request kind to one of six columns, state beating every label and a blocked item
beating an accepted one. plan_board_actions diffs live board items against every issue
and pull request in the repo, adding what is open and missing, and reconciling the
status of anything already tracked, open or closed. An open issue already sitting in
In progress is left alone, since GitHub's own workflow puts it there when a pull
request links to it and nothing in the label set records that link.

The project id, Status field id, and status option ids are all read at runtime via gh
project view/field-list rather than hardcoded, since the option ids have already
regenerated once in this project's life. The safety assertion that this tool never
merges, closes, or edits a pull request, or deletes a label, now scans the board
step's full desired state alongside every other step.

Signed-off-by: rocklambros <rock@rockcyber.com>
…ilures readably

Signed-off-by: rocklambros <rock@rockcyber.com>
@rocklambros
rocklambros merged commit 7d2dd3c into integration Sep 10, 2026
3 checks passed
@rocklambros
rocklambros deleted the feat/board-reconciler branch September 10, 2026 14:48
@github-project-automation github-project-automation Bot moved this from In progress to Done in ACS Project Tracker Sep 10, 2026
rocklambros added a commit that referenced this pull request Sep 17, 2026
Closes the automation half of #97, and picks up the follow-on #104 named for itself.

## What happened

The board drifted for seven days without anyone noticing. Everything from #105 onward, 40 issues and pull requests, was never added. That included the external conformance reports (#109, #111, #113), the second independent implementation and its profile claim (#106), the whole accepted 0.1.3 set (#132 through #136), and every AGT reference implementation bug an outside contributor filed. A stale board looks exactly like a current one, which is why it held for a week.

## The cause was narrower than it looked

Project 9 has six built-in workflows. `Item added to project` is enabled, which is why all 58 items carried a Status and not one was blank. What the project does not have is any auto-add for this repository. The only add workflow present is `Auto-add sub-issues to project`, which fires on sub-issues.

So the board sets Status correctly and never adds anything. A dry run of the reconciler put the split at **40 adds against 1 Status change**.

The reconciler has since been run by hand. The board is at 98 items, every one with a Status.

## What this adds

The real-time path is the project's own `Auto-add to project` workflow, which needs no credential and cannot be configured from here, since the GraphQL API exposes only `deleteProjectV2Workflow` and no way to create or enable one. **That toggle is a manual step and this pull request does not do it.** See below.

This workflow is the safety net under it. Nightly at 04:40 UTC, plus `workflow_dispatch` that defaults to printing the plan rather than applying it. The reconciler only adds an item or changes a Status field, never archives or removes one, which is the property that makes an unattended run acceptable at all.

## On the credential, since #104 rejected exactly this

#104 turned down a workflow for a good reason, quoted from its own message:

> Making it work means storing a personal access token with organization-wide project write as a repository secret, in a repository that opened to a hundred contributors this morning, where anyone who can land a workflow file can reach it.

and named the alternative:

> A GitHub App is the right long-term answer and is separate work.

This is that work, and it narrows the objection twice rather than waving it off.

**The grant is smaller.** `organization_projects: write` plus read on this repository's issues and pull requests. A classic PAT with `project` scope carries write on every project the granting user can see, organization-wide, for as long as the token lives. The App reaches project boards and nothing else. What the job holds is a one-hour installation token, not the key that mints it.

**The key is harder to spend.** It lives in a `board-automation` environment rather than in repository secrets, with the deployment branch rule limited to the default branch. A workflow on a pull request branch, fork included, cannot read it even if it names the environment.

**The residual risk, stated rather than assumed away.** Landing a malicious workflow on the default branch still reaches the key. That is the path CODEOWNERS already guards: `/.github/` carries a narrower owner list than the repository default, and the ruleset on main requires a code owner review. Two controls, not one.

## Manual steps before this does anything

This workflow is inert until all three are done, and it will fail loudly rather than silently if they are not.

1. **Create the GitHub App** on the organization with `organization_projects: write` and read on issues and pull requests. Install it on this repository only.
2. **Create the `board-automation` environment** in repository settings. Add `BOARD_APP_PRIVATE_KEY` as a secret and `BOARD_APP_ID` as a variable. Set the deployment branch rule to the default branch.
3. **Enable `Auto-add to project`** on project 9, filtered to this repository. This is the half that actually prevents the drift in real time. The nightly job only catches what it misses.

Step 3 is worth doing even if 1 and 2 are declined, since it costs nothing and would have prevented 40 of the 41 drift events on its own.

## Verification

- YAML parses, triggers and permissions read as intended.
- Both actions are SHA-pinned with a version comment, matching the convention in the other eleven workflows.
- No GitHub expression is interpolated into a `run:` block. Every value reaches the shell through `env:`, so the workflow-injection class is closed by construction rather than by review.
- Top-level `permissions: {}` with `contents: read` on the job.



Signed-off-by: Rock Lambros <rock@rockcyber.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant