chrismdp / issue-to-pull-request
An issue becomes a pull request
workflow.yaml
workflow.yaml is AWML 0.4, as it was pushed. The registry draws, checks and fits it converted to AWML 0.8.
# An issue the maintainer labels build becomes a change on main. The
# repository is public, so everything here sits in one scope: anyone may read the issues, the
# threads and the branches. What matters is who may act. Only people with write access, and the
# repository's own app, can add a label, so the label is the decision. Claude drafts with a
# read-only token, and the repository's app opens the pull request once the checks pass. A code
# rule then decides whether the change is straightforward, by rules the maintainer wrote down. If it
# is, the app sets it to merge by itself. If not, the person who added the label reviews and merges
# it from their own account, which is their yes. Either way main's own rule holds the merge until CI
# has passed on the branch. Answering new issues with options is left out: it reads the
# maintainer's private notes, so it stays an interactive session.
#
# The app pushes, opens and merges, because GitHub starts other workflows from what an app does but
# not from what the Actions token does. So CI runs on every pull request and shows on it, and a
# merge starts CI on main and the rebase workflow, as a person's merge would.
#
# A build can also be started by this workflow when a draft fails its checks. It tries three times
# in all, each time telling Claude why the last try failed, then comments on the issue with the
# reason.
#
# A change that needs review is put in front of the person who added the build label, as a review
# request on the pull request. A retry carries that person's name with it. A build whose label the
# app added, for a failing main, has no such person, so it asks the repository's owner.
#
# The actions implementation runs it on GitHub Actions. GitHub decides whether a label
# starts anything, before any runner exists. Claude works in one job. The checks run in a second
# job that can only read, so Claude's code never runs where a token can push. A third job, which
# runs nothing from the change, pushes the branch, opens the pull request, applies the rule and
# sets the merge. None of these jobs shares another's access, so Claude can't write the verdict the
# merge relies on. Claude's session sits inside the first job's block, since it shares that job's
# access. CI is its own workflow, and GitHub holds main's rule.
awml: "0.4"
title: A decision on an issue becomes a change on main
scopes:
- { id: public, label: "public: anyone who reads the repository", processing: any }
parts:
- { id: comment, kind: trigger, label: "The build label is added to an issue", wait: PT1H }
- { id: dispatch, kind: trigger, label: "A failed try starts the build again" }
- { id: pushed, kind: trigger, label: "The app pushes the pull request's branch" }
- { id: token, kind: connector, label: "The repository, through its own Actions token", in: public }
- { id: app, kind: connector, label: "The repository, through its own GitHub App", in: public }
- { id: who, kind: code, op: sort, label: "Sort by rule: is the label build?", in: public }
- { id: bring, kind: code, op: bring-in, label: "Bring in the issue and the comments of the repository's own people", in: public }
- { id: repo, kind: context, label: "The spec, guide, code and AGENTS.md", in: public }
- { id: build, kind: ai, op: draft, label: "Write the change test first", in: public }
- { id: patch, kind: context, label: "The change, as a patch, with Claude's note on it", in: public }
- { id: gate, kind: code, op: check, label: "Run the tests, vet, the examples and wasm parity", in: public }
- { id: failed, kind: context, label: "The check's record of why it failed", in: public }
- { id: tries, kind: code, op: sort, label: "Sort by rule: has it tried three times?", in: public }
- { id: again, kind: code, op: do, label: "Start the build again, with why this try failed", in: public }
- { id: report, kind: code, op: do, label: "Comment on the issue with why the build failed", in: public }
- { id: issuenote, kind: outside, label: "Comment on the issue: Allow", in: public }
- { id: open, kind: code, op: do, label: "Push a branch and open a pull request that closes the issue", in: public }
- { id: pr, kind: outside, label: "Open the pull request: Allow", in: public }
- { id: ci, kind: code, op: check, label: "CI runs every check on the branch", in: public }
- { id: rules, kind: context, label: "The build rules: what needs a review", in: public }
- { id: judge, kind: code, op: sort, label: "Sort by rule: is the change straightforward?", in: public }
- { id: automerge, kind: code, op: do, label: "Set the pull request to merge once CI passes, saying in its message why it was straightforward", in: public }
- { id: ask, kind: code, op: do, label: "Ask the person who added the label to review the pull request", in: public }
- { id: askout, kind: outside, label: "Request a review: Allow", in: public }
- { id: merge, kind: person, op: do, label: "Review the pull request and merge it", in: public }
- { id: account, kind: connector, label: "The repository, through the reviewer's own account", in: public }
- { id: held, kind: code, op: sort, label: "Sort by main's rule: has CI passed on the branch?", in: public }
- { id: landauto, kind: code, op: do, label: "Merge the change the app set to merge", in: public }
- { id: land, kind: code, op: do, label: "Merge the reviewer's change", in: public }
- { id: mainauto, kind: outside, label: "Merge a straightforward change into main: Allow", in: public }
- { id: main, kind: outside, label: "Merge a change that needed review into main: Ask", in: public }
- { id: strangers, kind: comment, label: "Anyone can comment, but the bring-in step passes on only the comments of the owner, members and collaborators, so strangers' text never reaches the draft step. AWML can't mark this yet (awml #5)" }
- { id: label, kind: comment, label: "Only people with write access and the repository's app can add a label, and GitHub holds that, not a prompt, so a stranger's comment starts nothing. The draft step's token can only read, so it can't push (awml #26)" }
- { id: verdict, kind: comment, label: "The check runs where the draft step can't reach, and its verdict is in its own record (awml #13, #20)" }
- { id: terms, kind: comment, label: "Allow, because a merge is undone with git revert, everything in it is already public on its branch, and the maintainer would not be surprised by a change the rule finds straightforward. One the rule sends to review is where they would be, so that merge is Ask. Installs of the plugin pick up either only at a release, which stays the maintainer's yes, though a new install takes main" }
- { id: escalate, kind: comment, label: "Claude's note can send a change to review but can't skip one. The rule reads the patch itself: what it touches, its size and whether it deletes a test (awml #20, #26)" }
- { id: starts, kind: comment, label: "GitHub starts other workflows from the app's pushes, merges and labels, so CI runs on the pull request and a merge starts CI on main and the rebase workflow. The app's key is a secret in each repository it's installed on, and each run's token reaches only its own repository" }
- { id: noskip, kind: comment, label: "Main's rule holds every merge, the app's and the maintainer's, until CI passes. Only the maintainer may skip it, to push a release; the app may not" }
- { id: whoasked, kind: comment, label: "Each retry carries the name of the person who added the label. A build whose label the app added, with no such person, asks the repository's owner" }
- { id: same, kind: comment, label: "Only this workflow starts a retry, and only for an issue a label already started" }
lines:
- { from: comment, to: who, kind: start, label: "the label" }
- { from: who, to: bring, kind: start, label: "[build]" }
- { from: token, to: bring, kind: data, label: "the issue's thread" }
- { from: bring, to: build, kind: data, label: "the issue and its people's comments" }
- { from: repo, to: build, kind: data }
- { from: build, to: patch, kind: data }
- { from: patch, to: gate, kind: data }
- { from: dispatch, to: bring, kind: start, label: "the issue" }
- { from: gate, to: open, kind: start, label: "[all pass]" }
- { from: gate, to: tries, kind: start, label: "[a check fails]" }
- { from: tries, to: again, kind: start, label: "[fewer than three]" }
- { from: tries, to: report, kind: start, label: "[three]" }
- { from: failed, to: again, kind: data }
- { from: again, to: bring, kind: start, label: "the issue and why it failed" }
- { from: failed, to: build, kind: data, label: "why the last try failed" }
- { from: failed, to: report, kind: data }
- { from: report, to: issuenote, kind: data }
- { from: issuenote, to: token, kind: data }
- { from: patch, to: open, kind: data }
- { from: open, to: pr, kind: data }
- { from: pr, to: app, kind: data }
- { from: pushed, to: ci, kind: start, label: "the branch" }
- { from: open, to: judge, kind: start, label: "[opened]" }
- { from: patch, to: judge, kind: data }
- { from: rules, to: judge, kind: data }
- { from: judge, to: automerge, kind: start, label: "[straightforward]" }
- { from: judge, to: ask, kind: start, label: "[needs a review]" }
- { from: ask, to: askout, kind: data }
- { from: askout, to: token, kind: data }
- { from: ask, to: merge, kind: start, label: "the review request" }
- { from: account, to: merge, kind: data, label: "the pull request and CI's result" }
- { from: automerge, to: held, kind: start, label: "the merge, set" }
- { from: merge, to: held, kind: start, label: "the merge" }
- { from: ci, to: held, kind: start, label: "the result" }
- { from: held, to: landauto, kind: start, label: "[passed, set by the app]" }
- { from: held, to: land, kind: start, label: "[passed, by the reviewer]" }
- { from: landauto, to: mainauto, kind: data }
- { from: land, to: main, kind: data }
- { from: mainauto, to: app, kind: data }
- { from: main, to: account, kind: data }
- { from: strangers, to: bring, kind: tie }
- { from: label, to: comment, kind: tie }
- { from: verdict, to: gate, kind: tie }
- { from: terms, to: mainauto, kind: tie }
- { from: escalate, to: judge, kind: tie }
- { from: starts, to: app, kind: tie }
- { from: noskip, to: held, kind: tie }
- { from: same, to: dispatch, kind: tie }
- { from: whoasked, to: ask, kind: tie }
implementations:
actions:
label: GitHub Actions
blocks:
- { id: github, label: "GitHub: issues, branches, pull requests, and main's rule", parts: [comment, dispatch, pushed, token, app, who, repo, patch, failed, pr, issuenote, askout, rules, held, landauto, land, mainauto, account, main] }
- { id: draftjob, label: "Actions job: draft, with a read-only token", platform: actions-code, parts: [bring] }
- { id: agentjob, label: "Claude Code, claude-opus-5-5, on the maintainer's subscription", platform: actions-agent, in: draftjob, parts: [build] }
- { id: checkjob, label: "Actions job: checks, with a read-only token", platform: actions-code, parts: [gate] }
- { id: codejob, label: "Actions job: branch, pull request, merge or retry, running nothing from the change", platform: actions-code, parts: [open, judge, automerge, ask, tries, again, report] }
- { id: cijob, label: "Actions job: CI, with a read-only token", platform: actions-code, parts: [ci] }
- { id: browser, label: "The maintainer's browser", parts: [merge] }