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 change that alters what a drawing or a page shows is redrawn and pictured before it opens: the
# check job redraws the examples into the change, and takes before and after pictures of what
# changed, as images, or as a short animation for something that moves. The app pushes the pictures
# to a branch kept for them, and the pull request shows them. Claude writes the pull request's
# description in Markdown, and it opens with why the change needs review, if it does, then Claude's
# note as written.
#
# 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 and its description for the pull request", in: public }
- { id: gate, kind: code, op: check, label: "Run the tests, vet, the examples and wasm parity", in: public }
- { id: render, kind: code, op: do, label: "Redraw the examples the change alters, and picture what changed, before and after", in: public }
- { id: pictures, kind: context, label: "The before and after pictures", in: public }
- { id: media, kind: code, op: do, label: "Push the pictures to the branch kept for them", in: public }
- { id: mediaout, kind: outside, label: "Push to the pictures branch: Allow", 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: does the change touch a path that needs review, delete a test, change over 400 lines, or carry Claude's request for review?", 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: seen, kind: comment, label: "Only a change to what a drawing or a page shows is pictured. GitHub plays only video uploaded through its web page, so a moving change is pictured as an animated image, which it shows inline" }
- { id: layout, kind: comment, label: "The description opens with why the change needs review, as bullets, then Claude's note as written, then what changed, the decisions the issue didn't settle, the pictures, and the issue it closes" }
- { 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: render, kind: start, label: "[all pass]" }
- { from: patch, to: render, kind: data }
- { from: render, to: patch, kind: data, label: "the redrawn examples" }
- { from: render, to: pictures, kind: data }
- { from: render, to: media, kind: start }
- { from: pictures, to: media, kind: data }
- { from: media, to: mediaout, kind: data }
- { from: mediaout, to: app, kind: data }
- { from: media, to: open, kind: start, label: "the pictures' links" }
- { 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 }
- { from: seen, to: render, kind: tie }
- { from: layout, to: open, 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, pictures, mediaout, 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, render] }
- { id: codejob, label: "Actions job: branch, pull request, merge or retry, running nothing from the change", platform: actions-code, parts: [media, 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] }