Reconcile the project board on a schedule, under a GitHub App - #157
Merged
Merged
Conversation
#104 added a `board` step to the Phase 2 executor and deliberately left it as a maintainer-run command, because the only way to automate it then was a personal access token with organization-wide project write sitting in a repository secret. That commit named a GitHub App as the right long-term answer and separate work. This is that work. ## Why now The board drifted for seven days. Every issue and pull request from #105 onward, 40 items, was never added: the external conformance reports (#109, #111, #113), the second independent implementation (#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, so nobody noticed until someone went looking for an issue they had just filed. The cause was narrower than it appeared. Project 9 has "Item added to project" enabled, which is why all 58 items carried a Status and none were blank. What it did not have was any auto-add for this repository. The board set Status correctly and never added anything. A dry run of the reconciler put the split at 40 adds against 1 Status change. ## What keeps it current Two things, and the real-time half needs no credential. The project's own "Auto-add to project" workflow adds each issue and pull request when it opens, and "Item added to project" gives it a Status. This workflow is the safety net under that. Nightly, it adds anything auto-add missed and corrects the Status of anything whose labels moved. The reconciler only adds an item or changes its Status field and never archives or removes one, which is the property that makes an unattended run acceptable. ## The grant organization_projects: write, plus read on this repository's issues and pull requests. A PAT with `project` scope would carry write on every project in the organization for as long as it lived; the App reaches boards and nothing else, and the job holds a one-hour installation token rather than the key that mints it. The key sits in the `board-automation` environment, not in repository secrets, with its deployment branch rule limited to the default branch, so a pull request branch cannot read it. Landing a malicious workflow on the default branch remains a path and is the one CODEOWNERS already guards: `/.github/` carries a narrower owner list than the default and the ruleset on main requires a code owner review. Signed-off-by: Rock Lambros <rock@rockcyber.com>
rocklambros
requested review from
GangGreenTemperTatum,
afogel,
bar-capsule,
fewdisc,
mamicidal and
sclintonowasp
as code owners
September 17, 2026 21:50
rocklambros
added a commit
that referenced
this pull request
Sep 17, 2026
… one pass (#158) Follow-up to #157, fixing a defect I found by testing the merged workflow rather than one a reviewer would have caught from the diff. ## The defect A reconcile run that adds items cannot also set their Status. Adding an item triggers project 9's own `Item added to project` workflow, which stamps a default Status asynchronously. `apply_governance.py` builds its entire plan before executing any of it, so on a run that adds anything it has already decided the Status actions before those items exist on the board. The default lands after the run finishes and nothing corrects it until the next run. ## Measured, not theorised On the 2026-09-17 backfill the first pass made **40 adds and 1 Status change**. A second pass run immediately afterwards found **5 more Status corrections waiting**: | Item | Why the default was wrong | | --- | --- | | #135 | `status:accepted` | | #131 | `status:accepted` | | #126 | open pull request, belongs in In progress | | #113 | open pull request | | #112 | open pull request | None of those five has the default as its correct Status, which is precisely the population a single pass strands. A third pass reported zero actions, so the tool converges, it just cannot converge in one run when it is also adding. Nightly, that is up to 24 hours of a board reading Needs triage for work that is accepted or in flight. That is the failure the board exists to prevent, so shipping #157 without this would have left the automation quietly wrong in the one case it most needed to be right. ## The fix Run the board step twice under `--apply`. The dry-run path still runs once and returns, since printing the same plan twice helps nobody. The reconciler is idempotent, so the second pass costs one API listing on a run that added nothing. Once `Auto-add to project` is enabled on the project, that is every run. ## Note on review I did not admin-merge this one. #157 was merged under the administrative authorisation given for it specifically, and extending that to a second change on my own judgement is the kind of thing that should be asked for rather than assumed. The workflow is inert until the App and environment exist, so there is no urgency in merging this ahead of a normal review. Signed-off-by: Rock Lambros <rock@rockcyber.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 projectis 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 isAuto-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 projectworkflow, which needs no credential and cannot be configured from here, since the GraphQL API exposes onlydeleteProjectV2Workflowand 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_dispatchthat 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:
and named the alternative:
This is that work, and it narrows the objection twice rather than waving it off.
The grant is smaller.
organization_projects: writeplus read on this repository's issues and pull requests. A classic PAT withprojectscope 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-automationenvironment 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.
organization_projects: writeand read on issues and pull requests. Install it on this repository only.board-automationenvironment in repository settings. AddBOARD_APP_PRIVATE_KEYas a secret andBOARD_APP_IDas a variable. Set the deployment branch rule to the default branch.Auto-add to projecton 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
run:block. Every value reaches the shell throughenv:, so the workflow-injection class is closed by construction rather than by review.permissions: {}withcontents: readon the job.