Skip to content

GitHub REST

Before the engine was built, the schema format was validated by hand-authoring real APIs and recording what broke. GitHub was one of them: 22 endpoints across 7 resources (repo, branch, content, pull, issue, team) and 2 actors (user, admin). The findings below drove real changes to the schema.

It stresses things a marketplace API doesn’t:

  • Pre-issued credentials. A personal access token isn’t obtained by logging in; you already have it. The “auth” call verifies it rather than producing it.
  • Header-based pagination. Link: <...>; rel="next" rather than a cursor in the body.
  • Base64 in a JSON body. The create-file endpoint wants file content encoded inside a JSON field.
  • A genuinely deep chain. repo → branch → content → pull → merge.
repo.create → branch.create → content.create → pull.create → pull.merge

Authored as five operations with one depends_on each, which the resolver expands into the full ordering — the same mechanic as marketplace’s 7-step chain.

Multi-actor review flows composed cleanly: a pull request opened by user, reviewed by admin, merged by user, expressed purely through actor: plus depends_on:.

A GitHub PAT lives in your keychain, not in a login response. The pattern that works today:

actors/user.yaml
name: user
description: GitHub user authenticated with a personal access token
auth:
strategy: bearer
token: "{{secret.GITHUB_PAT}}"
session:
ttl: 24h

strategy: bearer makes no network call — the token is attached directly. And because inject: and actor auth config resolve env and secret like anywhere else, no synthetic “extract the value I already had” step is needed.

That was Finding 1 in the validation: the original spec assumed inject: only saw actor session variables, which would have forced a hook on every operation. Relaxing it to the general resolution rules fixed the class of problem, and it’s how the engine behaves now.

Note the ttl: 24h. Per-actor TTLs matter here: a PAT is good for a day, where a short-lived JWT actor wants 15m.

Creating a repo needs a name nobody has used:

create:
method: POST
path: /user/repos
actor: user
body:
name: "reqloom-test-repo-{{$.uuid}}"
private: true
expect_status: 201
extract:
repo_name: $.name
owner: $.owner.login

{{$.uuid}} per request is exactly the case builtins exist for — see variables.

GitHub’s create-file endpoint wants encoded content. The validation flagged this as Finding 3 and proposed YAML transformer tags (!base64, !json, !file). Those tags were not implemented — !secret remains the only YAML tag. What shipped instead was an encoding function:

create:
method: PUT
path: /repos/{{repo.owner}}/{{repo.repo_name}}/contents/REQLOOM.md
actor: user
body:
message: "Add REQLOOM.md"
content: "{{$.base64.encode('Hello from Reqloom')}}"
branch: "{{branch.branch_name}}"
expect_status: 201

Same outcome, different spelling. $.base64, $.hex, and $.url all have encode/decode — see encoding functions.

GitHub paginates with a Link header. Extracting a header works:

extract:
link_header: $.headers.Link

But Link is a comma-separated list of <url>; rel="next" tuples, and you want only the URL whose rel is next. The validation asked for named parsers (link-header[rel=next]) as Finding 2. The header source shipped; the named-parser library did not. There is no parse: key.

Today you’d extract the raw header and pick it apart in a post_response hook, or use a regex extraction:

extract:
next_url:
path: '<([^>]+)>;\s*rel="next"'
source: regex

This is a real remaining gap, and it’s on the roadmap territory rather than something the schema solves elegantly yet.

The validation’s conclusion was pass with three required changes. Everything else in GitHub’s surface — multi-actor PR flows, team management, issue lifecycles, per-actor token lifetimes — was expressible declaratively, with no JS hooks.

The comparison that mattered: hand-authoring this chain in YAML was dramatically more compact than the equivalent Postman collection, because the prerequisites are declared once instead of scripted per request.

GitHub patternWhat to use
Pre-issued token (PAT, API key)strategy: bearer + {{secret.X}}, no login call
Long-lived credentialA longer session.ttl — 24h
Unique resource name per run{{$.uuid}} in the body
Base64 a body field{{$.base64.encode(...)}}
Header value$.headers.<Name> extraction
Structured header valueRegex extraction, or a post_response hook