Skip to content

Automate issue status through a project board - #62

Merged
milanmalhotra merged 8 commits into
devfrom
issue-60-issue-status-automation
Aug 18, 2026
Merged

Automate issue status through a project board#62
milanmalhotra merged 8 commits into
devfrom
issue-60-issue-status-automation

Conversation

@milanmalhotra

Copy link
Copy Markdown
Collaborator

Closes #60.

Problem

Issues here carry no state between open and closed, and nothing closes on its
own. The cause of the second half is specific and was verified rather than
assumed: GitHub honors Closes #NN only when the pull request merges into the
repository's default branch. The default is master, every feature pull request
targets dev, so the keyword never fires. Promoting dev to master does not
fire it either, because the promotion's merge commit carries no keyword.

closedByPullRequestsReferences is empty for #36, #48, #49 and #53, every one of
which has a merged pull request whose body says Closes #NN. GitHub records no
closing link at all for a pull request based on a non-default branch, so this
automation owns the link and parses pull request bodies itself.

What this adds

An organization project with a Status field, and the automation that drives it.

Trigger Transition
First push of an issue-<number>-<slug> branch In progress, only from Todo or unset
Pull request opened, reopened, ready for review, edited In review, only from Todo, unset or In progress
Pull request merged into dev Ready for release, from any state except Done
A release ships from master closed, and the board's own workflow moves it to Done

Closing by containment

An issue closes when the commit that fixed it is contained in the commit being
released, rather than by clearing the Ready for release column. An immediate
fix branched off master ships a release while dev still holds merged work,
and clearing the column would close issues that are not in the shipped jar.

Case Contained? Result
Merged to dev, dev promoted to master yes closes
Merged to dev, an immediate fix released instead no stays in Ready for release
Immediate fix merged straight to master yes closes, though it never entered Ready for release

The third row is why containment is the rule rather than a patch on top of column
scanning. An immediate fix never passes through dev, so the Ready for release
automation never touches it, and containment still closes it.

PREV_TAG is captured before Commit and tag creates the new tag. Taken
afterwards the range is empty and nothing closes.

Files

  • scripts/issue-status.sh - closing_refs and pr_numbers_from_log, pure text
    handling, no network.
  • scripts/test-issue-status.sh - their contract, wired into tests.yml.
  • scripts/set-issue-status.sh - the Projects v2 writes.
  • scripts/close-released-issues.sh - called from a new final step in
    release.yml.
  • .github/workflows/issue-status.yml - the push and pull request triggers.
  • release.yml gains issues: write, pull-requests: read, the tag capture,
    the closing step, and a dry-run preview.

Notes for review

Several decisions are worth knowing about rather than rediscovering.

  • No standalone jq anywhere. It is not installed on the development
    machine, which is why test-publish-bukkitdev.sh skips locally. Everything
    uses gh's built-in --jq, so all of it runs locally as well as in CI.
  • Failures exit non-zero even though the release already succeeded. The
    closing step runs after publishing. If every pull request lookup fails, or any
    issue fails to close, the step goes red. A green step that closed nothing would
    never be noticed, and there is no rerun because the release is spent.
  • edited is guarded. Editing the title of an already merged pull request
    fires edited. Without a guard that wrote In review and dragged the issue
    back out of Ready for release.
  • The merge path excludes only Done. A down-merge pull request repeating a
    Closes #NN would otherwise move an already shipped, already closed issue back
    to Ready for release permanently, since the release step skips issues that
    are not open.
  • An empty PREV_TAG aborts when the repository has tags, rather than
    scanning all history and closing every issue ever referenced. A genuine first
    release, with no tags at all, still scans full history.
  • Fork pull requests are skipped, because GitHub withholds secrets from them
    and the job would redden every external contribution.
  • Pull request bodies reach the shell through environment variables only. No
    ${{ }} appears inside any run: block.
  • GH_TOKEN uses the existing RELEASE_TOKEN, which needs the project scope.
    GITHUB_TOKEN cannot reach an organization project.

Two conventions are now load bearing

Recorded in CLAUDE.md. Branch names must be issue-<number>-<slug>, and a pull
request body must name every issue it resolves with a closing keyword. Rebase
merging must also stay disabled, because a rebase merge leaves no pull request
number in any commit subject and the release would close nothing, silently.

Tests

  • mvn -B test: 385 tests, Failures 0, Errors 0, Skipped 0.
  • test-issue-status.sh: 35 passed, 0 failed. New file, 20 test functions.
  • test-require-changeset.sh: 35 passed, 0 failed. test-release.sh: 49 passed,
    0 failed. Both unchanged.

Every guard in the new suite was verified by breaking it and confirming the
matching test fails, then restoring, per this repository's convention. The
parsers were also checked against real history: pr_numbers_from_log returns
31 over v1.2.2..origin/master, and closing_refs over #58's body returns
exactly 53, not the 41, 46 and 56 it mentions in passing.

No changeset: everything here is under .github/ or scripts/, both exempt.

Not yet verified

The end to end path has never run on GitHub. Opening this pull request is the
first live exercise of the trigger. Still unproven: the Release workflow's
closing step against a real promotion, and whether RELEASE_TOKEN carries the
project scope, which will show as a loud failure on the first board write if it
does not.

Infra-only work never closes at all, since it ships nothing to server owners. This
issue will sit at Ready for release until an unrelated release runs, and the
same is true of every docs and CI issue. No transition covers that case.

Editing an already merged pull request's title or body could pull an
issue backwards out of Ready for release, since the pull request job
had no allowed-current guard on its non-closed path. Add one.

Also skip the job entirely for fork-originated pull requests, since
GitHub withholds secrets from those runs and the first API call would
fail for a reason the contributor cannot fix. Scope GITHUB_TOKEN
permissions down to none, since this workflow only ever uses the PAT
in GH_TOKEN. And track failures across the issue loop instead of
letting only the last invocation decide the step's exit code.
Down-merges from master to dev can repeat a Closes reference for an
issue that already shipped; the merged-into-dev path now allows every
status except Done, so a merge still beats an earlier state without
dragging a shipped issue backwards.

An empty PREV_TAG with tags already present in the repository now
fails fast instead of silently scanning all of history and closing
every issue any pull request ever referenced; a genuine first release
with no tags at all is unaffected.

A branch deletion after its pull request merges can fire a push event
with no branch left to check out; the push job now skips deleted refs.
@milanmalhotra
milanmalhotra merged commit 4a3c41d into dev Aug 18, 2026
9 of 11 checks passed
@milanmalhotra
milanmalhotra deleted the issue-60-issue-status-automation branch August 18, 2026 01:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant