Ghost docs
GitHub CLI

GitHub CLI

Use the GitHub CLI (gh) with Ghost, and what differs from GitHub

Using the GitHub CLI

Ghost answers the GitHub API at the API host, so the GitHub CLI (gh) works against it.

Setup

Log in through the browser:

gh auth login --hostname api.<domain> --web

gh prints a one-time code and opens <web>/device. Sign in to Ghost, enter the code and approve. gh receives an API key holding the scopes it asked for. GitHub CLI then shows under Settings → Authorized apps, where you can revoke it.

Or create an API key in your account settings and log in with it:

echo "$GHOST_TOKEN" | gh auth login --hostname api.<domain> --with-token
gh auth status --hostname api.<domain>

To let git use the same credentials for clones and pushes over HTTPS:

gh auth setup-git --hostname api.<domain>

Point gh at Ghost for a shell session with GH_HOST, or name the host in -R:

export GH_HOST=api.<domain>
gh issue list -R api.<domain>/owner/repo

GH_HOST cannot carry a port, so the API must be served on 443.

Supported commands

Each of these is run against a real Ghost server by the API's end-to-end suite.

CommandNotes
gh auth login --with-token, gh auth status
gh auth login --webApprove the code at <web>/device
gh auth refresh --scopes …Approve again; gh switches to the new key. The old key stays valid until you revoke it
gh api, gh api graphqlAny route or query listed below
gh repo view, gh repo view --jsonIncludes the README
gh repo clone
gh repo createPersonal and organization repositories
gh issue list--state, --label, --author, --assignee, --search
gh issue view, gh issue view --comments
gh issue create--label, --assignee
gh issue comment, gh issue edit
gh issue close, gh issue reopen
gh label list, gh label create, gh label edit, gh label delete, gh label clone
gh pr create
gh pr list
gh pr view, gh pr view --comments, gh pr view --json …
gh pr status
gh pr diff
gh pr checkout
gh pr comment, gh pr editgh pr edit --base fails: Ghost cannot retarget a pull request
gh pr ready, gh pr ready --undo
gh pr close, gh pr reopenA closed pull request reopens while its head branch exists
gh pr merge
gh release createWith files, --draft, --prerelease, --target. --generate-notes and --discussion-category fail
gh release list, gh release view, gh release view --json …
gh release upload, gh release download, gh release delete-assetFiles of any type
gh release edit--tag fails: Ghost cannot move a release to another tag
gh release delete, gh release delete --cleanup-tag
gh api … --paginateFollows the Link header of every REST list
gh ssh-key list, gh ssh-key add

What is served

GraphQL at /api/graphql:

  • Queries: viewer, user, organization, repositoryOwner, repository, node, nodes, search(type: ISSUE)
  • Types: User, Organization, Repository, Ref, Label, Issue, IssueComment, PullRequest (with files, commits, reviews, latestReviews, reviewDecision, mergeable and mergeStateStatus), PullRequestReview, PullRequestCommit, Commit, PullRequestChangedFile, Release
  • repository.pullRequests (states, headRefName, baseRefName, labels, orderBy) and repository.pullRequest(number:)
  • repository.releases (first, after, orderBy by CREATED_AT or NAME) and repository.release(tagName:), which finds drafts for people who can write to the repository
  • Mutations: createIssue, updateIssue, closeIssue, reopenIssue, addComment, addLabelsToLabelable, removeLabelsFromLabelable, addAssigneesToAssignable, removeAssigneesFromAssignable, createRepository, createPullRequest, updatePullRequest, closePullRequest, reopenPullRequest, mergePullRequest, markPullRequestReadyForReview, convertPullRequestToDraft. Comments, labels and assignees take a pull request's id as they take an issue's

REST at /api/v3:

  • GET /, GET /meta
  • GET /user, GET /users/:login
  • GET /user/keys, POST /user/keys
  • GET /repos/:owner/:repo, GET /repos/:owner/:repo/readme
  • POST /user/repos, POST /orgs/:org/repos

Issues, comments and labels, under /api/v3/repos/:owner/:repo:

  • GET, POST /issues; GET, PATCH /issues/:number. The list includes pull requests, marked with a pull_request key, as on GitHub
  • GET, POST /issues/:number/comments; GET /issues/comments; GET, PATCH, DELETE /issues/comments/:comment_id
  • GET, POST, PUT, DELETE /issues/:number/labels; DELETE /issues/:number/labels/:name
  • POST, DELETE /issues/:number/assignees; GET /issues/:number/assignees/:assignee
  • GET /assignees, GET /assignees/:assignee
  • GET, POST /labels; GET, PATCH, DELETE /labels/:name

Pull requests, under /api/v3/repos/:owner/:repo:

  • GET, POST /pulls; GET, PATCH /pulls/:number. GET /pulls/:number with Accept: application/vnd.github.diff answers the diff as text, and with .patch one mailbox-style patch per commit
  • GET /pulls/:number/commits, GET /pulls/:number/files, GET /pulls/:number/reviews, GET /pulls/:number/reviews/:review_id
  • GET, PUT /pulls/:number/merge. A merge Ghost refuses, such as one with conflicts, answers 405; a sha the head has moved on from answers 409

Releases and their assets, under /api/v3/repos/:owner/:repo:

  • GET, POST /releases; GET /releases/latest; GET /releases/tags/:tag, which answers published releases only, as on GitHub; GET, PATCH, DELETE /releases/:release_id
  • GET /releases/:release_id/assets; GET, PATCH, DELETE /releases/assets/:asset_id. GET /releases/assets/:asset_id with Accept: application/octet-stream answers the file itself
  • POST /api/uploads/repos/:owner/:repo/releases/:release_id/assets?name=…&label=…, the upload_url of every release, as on GitHub Enterprise. The body is the raw file, and its Content-Type is the type the file is served with
  • DELETE /git/refs/:ref, for heads/<branch> and tags/<tag>. A branch is deleted under Ghost's rules: never the default branch or one an open pull request uses

Lists take page and per_page (30 by default, at most 100) and send a Link header with first, prev, next and last. Labels named on an issue that the repository does not have yet are created with color ededed, as on GitHub.

GraphiQL is at /api/graphql in a browser, signed in with your Ghost session.

OAuth apps

Other tools sign users in to Ghost the way they sign in to GitHub. Building an OAuth app walks through registering one and every request it makes. Register a tool under Settings → OAuth apps, or for an organization under <web>/<org>/settings/oauth-apps, where every admin and owner of the organization manages it. Give it a name, a homepage URL, one or more callback URLs, and optionally a description and a logo, which people see when the app asks for access. Ghost shows the client secret once; Generate a new secret replaces it, and the old one stops working at once.

The tool sends users to https://api.<domain>/login/oauth/authorize and exchanges the code at https://api.<domain>/login/oauth/access_token, as on GitHub. Users approve the requested scopes on <web>/login/oauth/authorize. The tool receives an API key holding the approved scopes Ghost knows. PKCE with S256 is supported and optional.

  • Callback URLs must use https, or http on localhost, so one app can list its production callback beside a local one. A redirect_uri must have the same scheme, host and port as one of them, with a path equal to its path or under it. Anything else gets an error page and no redirect. Without a redirect_uri, people return to the first callback URL.
  • Enable device flow lets the tool use the device flow, as gh does. It is off by default.
  • Deleting an app revokes its keys for every user. Deleting an organization deletes its apps and their keys.
  • A Verified check on the approval page means the instance's operators vouch for the app. Only an operator sets it, through oauth_client.metadata.verified in the database. gh is verified.
  • Under Settings → Authorized apps, users see every app holding a key for their account, gh included, and revoke it. Revoking deletes their keys for that app, and the app asks for consent again next time.

Known differences

  • databaseId and the REST id are integers for users, organizations, repositories, issues, pull requests, comments, labels, releases and release assets. SSH keys still answer null.
  • Milestones, reactions, issue types, projects and sub-issues are always empty.
  • A closed issue's stateReason is always COMPLETED. Ghost does not record why an issue was closed.
  • A comment's authorAssociation is OWNER or NONE, never MEMBER or COLLABORATOR.
  • auto_init, gitignore_template and license_template are accepted and ignored when creating a repository over REST. The repository starts empty.
  • gh issue create and gh issue edit with --milestone or --project fail: Ghost has no milestones or projects.
  • gh repo create --disable-issues, --disable-wiki and --homepage fail: they need updateRepository, which is not served.
  • A query with an unknown field answers HTTP 400. GitHub answers 200. gh handles both.
  • search only finds issues in one repository, named with repo:. Qualifiers Ghost cannot filter on, such as milestone: and sort:, are ignored.
  • API keys made in Ghost's settings hold every scope. Keys from gh auth login --web or an OAuth app hold the scopes requested and approved that Ghost knows (repo, read:org, gist, …). Unknown scopes, such as workflow, are dropped.
  • OAuth apps always have a client secret. Public clients that authenticate with PKCE alone are not supported.
  • OAuth apps can turn on expiring user tokens with refresh tokens. GitHub offers them only to GitHub Apps. gh's built-in app keeps tokens that never expire.
  • Without repo, a private repository answers as if it did not exist.
  • Only a comment's author can edit it. On GitHub, people with write access can too.
  • state_reason is accepted and not stored; a closed issue answers completed.
  • A comment's html_url ends in #issuecomment- and Ghost's id for the comment, not its integer id, so the link opens on the comment in Ghost.
  • PUT /issues/:number/labels with a bare string body is refused: Ghost only parses JSON objects and arrays.
  • GET /assignees lists the owner, collaborators and organization members. GET /assignees/:assignee answers 204 for any user, as Ghost lets anyone be assigned.
  • documentation_url in REST errors always points at https://docs.github.com/rest.
  • Pull requests have no checks or commit statuses yet (#61): statusCheckRollup is always null and gh pr checks shows none.
  • No auto-merge, merge queue, review requests or branch protection: autoMergeRequest is null, reviewRequests is empty, and reviewDecision is APPROVED, CHANGES_REQUESTED or null, never REVIEW_REQUIRED.
  • maintainerCanModify is always false, and a pull request's base branch cannot be changed.
  • A pull request's REST id and databaseId are its issue's. GitHub gives the two different ids.
  • A commit author's user is the account with that verified email, primary or added, or null.
  • Not served: creating or editing reviews over REST, review comments over GraphQL, updatePullRequestBranch, and gh pr checks.
  • The latest release is the newest published release that is not a prerelease, by when it was published. make_latest is accepted and ignored. GitHub orders by the release commit's date and lets make_latest choose.
  • A release's target_commitish is the commit its tag points at, or the default branch once the tag is deleted. GitHub keeps the branch the tag was cut from.
  • A draft release creates its tag straight away. GitHub creates it when the draft is published.
  • tarball_url and zipball_url are null: Ghost has no source archives yet (#66). An asset's digest is null and its state always uploaded.
  • Release assets are streamed from Ghost with status 200, never redirected to storage. GitHub may do either.
  • Uploading an asset counts against the repository owner's storage quota when the instance sets one, and is refused over the per-file limit.
  • A release's name ordering in GraphQL ignores case; a release without a name sorts by its tag.
  • Not served: POST /releases/generate-notes, release reactions and discussions, and the release body_html, body_text and mentions_count fields.
  • Not served: locking (Ghost has no locking yet, #111), milestones, issue types, sub-issues, issue dependencies, issue field values, comment pins, label archiving, reactions, issue events and timelines, and the cross-repository lists /issues, /user/issues and /orgs/:org/issues.

Not yet supported

  • Gists
  • GitHub Apps and fine-grained tokens

On this page