Automation on GitHub Actions¶
The worker and the orchestrator described in Agent orchestration run as GitHub Actions workflows, so the project moves forward without anyone's computer being on. This page explains how the pieces fit, how to switch the automation on and off, and what it costs.
Workflows¶
| Workflow | Role | Instructions | Started by |
|---|---|---|---|
.github/workflows/agent-worker.yml |
Worker | .github/agents/worker.md |
status:ready or status:plan-approved label added by the repository owner; a comment or review-thread reply by the repository owner tagged **[orchestrator]**; an untagged comment or review-thread reply by the repository owner on a pull request; a Greptile review; hourly at minute 40; manual |
.github/workflows/agent-orchestrator.yml |
Orchestrator | .github/agents/orchestrator.md |
A comment or review-thread reply by the repository owner tagged **[worker]**; an untagged comment by the repository owner; a Greptile review; the ci workflow finishing on a pull request from a branch of this repository; hourly at minute 10; manual |
The agents post, label and push through the repository owner's token (see "Why a personal
access token" below), so their comments and labels are authored by the owner's account. Every
start condition on a comment or label therefore requires the owner as author, tagged or not.
The one exception is the orchestrator's start on Greptile's review summary comment, which
requires greptile-apps[bot] as author. See "Start conditions in a public repository" under
"Security notes".
Both run anthropics/claude-code-action in automation mode (a prompt is given, so it does
not wait for an @claude mention), pinned to a release commit. Each run starts from a fresh
checkout of main, reads the rulebook and does one step. The worker also installs the project
so it can run the checks; the orchestrator does not, because it never runs pull request code
and judges test results from CI instead.
The hand-off between the two is event-driven. A typical issue:
- The orchestrator adds
status:ready→ the worker starts and posts a plan. - The
**[worker]**comment starts the orchestrator, which approves the plan and addsstatus:plan-approved→ the worker starts and opens a pull request. - CI finishing and the Greptile review start the orchestrator and the worker; the worker answers findings, the orchestrator checks the merge conditions and merges. It merges only when CI and a Greptile review have both completed on the exact head commit.
- The merge closes the issue. In the same run the orchestrator confirms the issue is closed,
removes its
status:*label and selects the next issue; if GitHub has not closed the issue yet, the next run does this.
The hourly runs are meant to catch anything an event missed, but GitHub delays or drops scheduled runs when it is busy (on the first day only two of the hourly runs fired), so every hand-off should have an event of its own. If work seems stuck, start the agent by hand from the Actions tab ("Run workflow").
Two details keep the hand-offs moving without the maintainer:
- Greptile reviews every push.
.greptile/config.jsonsetsautoReviewto["open", "push"], so each fix the worker pushes gets a new review, and the orchestrator can require a review of the exact head commit before merging. If no review of the head commit has appeared two hours after CI finished on it, the orchestrator escalates rather than waiting indefinitely. When a review finds nothing new, Greptile only edits its summary comment and submits no pull request review, so the orchestrator also starts when that summary comment is created or edited. - Commits carry the maintainer's identity. The action sets the git author itself
(
claude[bot]by default). The worker workflow passes the repository owner asbot_nameandbot_id, so commits are authored with the owner's GitHub no-reply address and match theSigned-off-byline required by the Developer Certificate of Origin.
The token cannot read GitHub's check-runs API, so the orchestrator reads CI results from the
Actions runs of the ci workflow. It reads the Greptile review from the pull request's reviews
(their commit_id) or, when Greptile posts only a summary comment, from the "Last reviewed
commit" link in that comment.
Why a personal access token¶
Events caused by the default GITHUB_TOKEN do not start other workflows (GitHub's rule to
prevent loops). The agents need exactly that: a label added by the orchestrator must start the
worker, and a push by the worker must start CI. The workflows therefore act through a
fine-grained personal access token limited to this repository. Its actions appear under the
maintainer's account, which is why every agent comment starts with a role tag.
Loops are prevented by the workflows' start conditions instead:
- Each agent only starts on the other agent's tag, on the maintainer's untagged comments, on
Greptile, on CI, or on the schedule; never on its own comments. The worker ignores reviews
and review comments on pull requests whose branch is not named
issue-...(for example a maintainer's change to the workflows), because those have no issue for it to work on. - A worker run ends only when its step is finished (pull request opened, plan posted, review
items answered by fixes or reasoned replies), with a
Progress:comment, or with the issue set tostatus:blockedand a report (a stop condition or the three-Progress:limit). A run that stopped after a partial push would leave no event to start the next run. - Each role has its own concurrency group, so at most one worker and one orchestrator run at a time. Extra events wait; GitHub keeps only the newest waiting run per group, which is enough because every run re-reads the current state.
- Runs are capped: worker 120 turns and 120 minutes, orchestrator 40 turns and 30 minutes.
GitHub cancels a job at its time limit and anything the worker has not pushed is lost, so the
worker is given its start time, pushes finished parts early, times long computations (such
as detection-probability sweeps) on a small size before running them, and leaves a
Progress:comment when it cannot finish (on the issue while implementing, on the pull request while fixing review findings). That comment starts the orchestrator, which answers in the same place telling the worker to continue; the next worker run continues from the pushed branch. The orchestrator escalates instead after threeProgress:comments on an issue without a pull request (probably too large for one run), or after three on a pull request with no Greptile review in between (the fixes are not producing pushes). - The rulebook tells both agents to change nothing when nothing needs doing.
Setup (maintainer)¶
- Claude token. On your own machine run
claude setup-tokenand store the result as the repository secretCLAUDE_CODE_OAUTH_TOKEN. Runs use your Claude plan's usage. - GitHub token. Create a fine-grained personal access token with access to this
repository only, with an expiry date, and these repository permissions:
Contents: read and write; Issues: read and write; Pull requests: read and write;
Actions: read; Commit statuses: read; Metadata: read. Store it as the repository secret
AGENT_GH_TOKEN. - Branch protection for
main. Require a pull request before merging and require thetestanddocschecks to pass. This keeps any agent from pushing tomaineven if an instruction fails. - Stop any other worker or orchestrator (a local Claude Code session or a scheduled task doing the same job), so two of the same role do not act at once.
- Switch on. Create the repository variable
AGENTS_ENABLEDwith the valuetrue. Optionally start one run by hand from the Actions tab ("Run workflow") to watch it.
Switching off¶
Set the repository variable AGENTS_ENABLED to false (or delete it). Runs already in
progress finish; no new run starts. Nothing else needs to change.
Costs and limits¶
- GitHub Actions minutes. Private repositories on GitHub Pro include 3,000 minutes per
month. Most event-triggered runs that find nothing to do finish in a few minutes; runs that
implement an issue can take up to the 120-minute limit, and an issue that needs several
runs (see the
Progress:comments above) uses that much per run. - Claude usage. Runs consume the usage of the plan behind
CLAUDE_CODE_OAUTH_TOKEN, shared with any other use of that plan. - Scheduled workflows run from the default branch only.
Publishing to PyPI¶
.github/workflows/publish.yml uploads snappnt to PyPI
when the maintainer publishes a GitHub release. It is not an agent workflow: no Claude session
runs in it, and agents never create releases (see "Releases" in
Contributing and workflow).
| Job | Permissions | Steps |
|---|---|---|
build |
contents: read |
Checks out the release tag. Fails unless the tag is v followed by project.version in pyproject.toml (tag v0.1.0 for version 0.1.0). Builds the sdist and, from the sdist, the wheel (uv build). Installs the wheel into a clean virtual environment and runs snappnt info, snappnt sim and snappnt acquire from outside the source tree; fails unless snappnt acquire detects the simulated satellite and its result matches the simulator's truth. Stores the two files as a workflow artifact. |
publish |
id-token: write |
Runs in the GitHub environment pypi. Downloads the artifact and uploads it with pypa/gh-action-pypi-publish. Does not check out the repository. |
The upload uses PyPI's
Trusted Publishing: the publish job asks GitHub
for an OpenID Connect token that names this repository, the workflow file publish.yml and
the environment pypi, and PyPI accepts the upload only if those match the publisher
registered for the project. No PyPI API token is stored in the repository's secrets.
The same install-and-run check runs on every pull request as the wheel job of
.github/workflows/ci.yml, so a file missing from the wheel is found before a release.
Setup (maintainer, once before the first release)¶
- On pypi.org, with two-factor authentication enabled on the account, add a pending Trusted
Publisher ("Publishing" in the account settings) for the project
snappnt: ownerh-shiono, repositorysnappnt, workflowpublish.yml, environmentpypi. A pending publisher turns into the project's publisher on the first upload, which creates the project. - In the GitHub repository settings, under "Environments", create the environment
pypi. Optionally add the maintainer as a required reviewer; thepublishjob then waits for approval in the Actions tab before it receives the token and uploads.
Until both are done, the publish job of a release fails at the upload and nothing reaches
PyPI. A version once uploaded cannot be uploaded again, even after it is deleted on PyPI, so a
broken release is fixed with a new version number.
Security notes¶
- The agents act with the personal access token's permissions. Keep it limited to this repository and give it an expiry date.
- The workflows start only on events listed in "Start conditions in a public repository" below. The agents still read every comment on an issue or pull request. Both are instructed to treat only untagged comments by the repository owner's account as instructions; comments from any other account are information.
- The orchestrator's checkout keeps no git credentials, it never checks out pull request code, and it merges only branches of this repository named after the issue. Pull request code runs only in CI, which has no write-capable token.
anthropics/claude-code-action,actions/checkoutandastral-sh/setup-uvare pinned to commit hashes, so a moved tag cannot change the code that receives the secrets. The actions used bypublish.yml(actions/upload-artifact,actions/download-artifactandpypa/gh-action-pypi-publish) are pinned the same way. Update the hashes deliberately, after reading the release notes. The actionlint binary used by CI is pinned by version and SHA-256 checksum (see "Checking the workflow files" below).publish.ymlstarts only when a GitHub release is published, which needs write access to the repository; issues, comments and pull requests from other accounts cannot start it. Its top-level permissions are empty. Only thepublishjob may request an OpenID Connect token (id-token: write), and that job runs no code from the repository: it downloads the built files and runs the pinned upload action. Thebuildjob, which runs the build backend and snappnt itself, has read access to the repository contents only. The checkout keeps no git credentials, and theuvcache is off, so nothing restored from a cache written by an earlier run goes into a release. The PyPI publisher is bound to the workflow filepublish.ymland the environmentpypi, so PyPI refuses a token requested by another workflow or by a job outside that environment.- The start conditions are checked statically on every pull request and push to
main; see "Checking the workflow files" below. - Transmit commands, pushes to
main, and repository settings commands are denied through--disallowedToolsin the workflows, in addition to the rules in the instructions.
Start conditions in a public repository¶
Once the repository is public, anyone with a GitHub account can open issues, comment, review,
reply in review threads, and open pull requests from forks. Only accounts with triage access or
more can add labels, and only accounts with write access can edit another account's comment.
Each start condition below holds only when AGENTS_ENABLED is true, and is safe for these
reasons:
| Workflow | Event | Condition | Why an outside account cannot meet it |
|---|---|---|---|
| Both | schedule |
Hourly | Not caused by any account. Scheduled runs use the workflow file on main. |
| Both | workflow_dispatch |
Manual start | Needs write access to the repository. |
| Worker | issues (labeled) |
Label status:ready or status:plan-approved, added by the repository owner (sender) |
Outside accounts cannot add labels, and the issue template applies no labels. The owner check also covers anyone given triage access later. |
| Worker | issue_comment |
Body starts with **[orchestrator]**, author is the repository owner |
Author check. |
| Worker | issue_comment |
On a pull request, untagged, author is the repository owner | Author check. |
| Worker | pull_request_review_comment |
Branch of this repository named issue-..., author is the repository owner, tagged **[orchestrator]** or untagged |
Author check and branch check. |
| Worker | pull_request_review |
Branch of this repository named issue-..., reviewer is greptile-apps[bot] |
Reviewer check and branch check. |
| Orchestrator | issue_comment (created) |
Body starts with **[worker]** or is untagged, author is the repository owner |
Author check. |
| Orchestrator | issue_comment (created or edited) |
On a pull request, author is greptile-apps[bot], body is Greptile's summary |
Author check. comment.user is the comment's author, not whoever edited it, and only accounts with write access can edit another account's comment. |
| Orchestrator | pull_request_review_comment |
Branch of this repository, author is the repository owner, tagged **[worker]** or untagged |
Author check and branch check. |
| Orchestrator | pull_request_review |
Branch of this repository, reviewer is greptile-apps[bot] |
Reviewer check and branch check. |
| Orchestrator | workflow_run (ci completed) |
The ci run was for a pull request from a branch of this repository (workflow_run.head_repository) |
Only accounts with write access can push branches to this repository. Without this check a pull request from a fork would start the orchestrator: a workflow_run run gets the secrets even when the run that triggered it came from a fork (GitHub documentation, "Events that trigger workflows"). |
Notes on the branch checks:
- For
pull_request_reviewandpull_request_review_commenton a pull request from a fork, GitHub passes no secrets to the run, so such a run could not act anyway. The branch check (pull_request.head.repo.full_nameequal to this repository) makes the workflow skip it instead of starting a run that fails. - An
issue_commentevent carries no branch information. It runs in the context of the default branch of this repository (GitHub documentation, "Events that trigger workflows"), not of the fork, so it gets the secrets even on a pull request from a fork. If Greptile reviews a pull request from a fork, its summary comment there starts the orchestrator (TODO: not checked whether Greptile reviews pull requests from forks; settled by opening one from a test fork). That run never checks out pull request code and merges only branches of this repository named after the issue, but it does read the pull request's text, which an outside account wrote. The same holds for scheduled runs, which read every open issue and pull request. The protection there is the rule that comments and text from other accounts are information, never instructions.
Checking the workflow files¶
A misspelled property in a start condition does not cause an error on GitHub: it evaluates to empty, so the condition silently never holds (a hand-off stops) or silently stops excluding someone (a security check is lost). Two checks run in CI to catch this:
- The
actionlintjob in.github/workflows/ci.ymlruns actionlint over every file in.github/workflows/. It checks the syntax and types of${{ }}expressions andif:conditions, the properties of contexts such asgithub(a misspelledgithub.event_namefails the job), the inputs of actions, and the shell scripts inrun:steps with shellcheck. The job downloads a fixed release of actionlint and verifies its SHA-256 checksum against the value written in the workflow, taken from the release'sactionlint_<version>_checksums.txt. To update it, change the version and the checksum together. tests/test_workflow_event_paths.py, run with the other tests. actionlint typesgithub.eventas an object with any properties, so it does not check the names of webhook payload properties: a misspelledgithub.event.comment.user.loginpasses it. The test reads every expression in.github/workflows/*.yml(eachif:condition and the text inside each${{ }}; YAML comments and string literals are skipped), collects everygithub.event.<...>path in them, and fails on any path that is not in an allow list kept in the test. GitHub matches property names without regard to case, so the test compares paths in lower case. Each entry in the list names the events that carry the property and links to the GitHub documentation where it was checked. A new property in a workflow therefore needs a new entry, checked against the webhook payload documentation, before the tests pass.
Neither check evaluates the conditions: they do not show that a condition holds for the events it is meant to accept. That still rests on reading each condition against the table in "Start conditions in a public repository".
To run actionlint locally, download the release archive for your platform from
https://github.com/rhysd/actionlint/releases, check it against the release's checksums file,
and run actionlint from the repository root. Install shellcheck as well to get the same
script checks as CI.