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> --webgh 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/repoGH_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.
| Command | Notes |
|---|---|
gh auth login --with-token, gh auth status | |
gh auth login --web | Approve 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 graphql | Any route or query listed below |
gh repo view, gh repo view --json | Includes the README |
gh repo clone | |
gh repo create | Personal 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 edit | gh pr edit --base fails: Ghost cannot retarget a pull request |
gh pr ready, gh pr ready --undo | |
gh pr close, gh pr reopen | A closed pull request reopens while its head branch exists |
gh pr merge | |
gh release create | With 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-asset | Files 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 … --paginate | Follows 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(withfiles,commits,reviews,latestReviews,reviewDecision,mergeableandmergeStateStatus),PullRequestReview,PullRequestCommit,Commit,PullRequestChangedFile,Release repository.pullRequests(states,headRefName,baseRefName,labels,orderBy) andrepository.pullRequest(number:)repository.releases(first,after,orderBybyCREATED_ATorNAME) andrepository.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 /metaGET /user,GET /users/:loginGET /user/keys,POST /user/keysGET /repos/:owner/:repo,GET /repos/:owner/:repo/readmePOST /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 apull_requestkey, as on GitHubGET,POST /issues/:number/comments;GET /issues/comments;GET,PATCH,DELETE /issues/comments/:comment_idGET,POST,PUT,DELETE /issues/:number/labels;DELETE /issues/:number/labels/:namePOST,DELETE /issues/:number/assignees;GET /issues/:number/assignees/:assigneeGET /assignees,GET /assignees/:assigneeGET,POST /labels;GET,PATCH,DELETE /labels/:name
Pull requests, under /api/v3/repos/:owner/:repo:
GET,POST /pulls;GET,PATCH /pulls/:number.GET /pulls/:numberwithAccept: application/vnd.github.diffanswers the diff as text, and with.patchone mailbox-style patch per commitGET /pulls/:number/commits,GET /pulls/:number/files,GET /pulls/:number/reviews,GET /pulls/:number/reviews/:review_idGET,PUT /pulls/:number/merge. A merge Ghost refuses, such as one with conflicts, answers 405; ashathe 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_idGET /releases/:release_id/assets;GET,PATCH,DELETE /releases/assets/:asset_id.GET /releases/assets/:asset_idwithAccept: application/octet-streamanswers the file itselfPOST /api/uploads/repos/:owner/:repo/releases/:release_id/assets?name=…&label=…, theupload_urlof every release, as on GitHub Enterprise. The body is the raw file, and itsContent-Typeis the type the file is served withDELETE /git/refs/:ref, forheads/<branch>andtags/<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. Aredirect_urimust 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 aredirect_uri, people return to the first callback URL. - Enable device flow lets the tool use the device flow, as
ghdoes. 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.verifiedin the database.ghis verified. - Under Settings → Authorized apps, users see every app holding a key for their account,
ghincluded, and revoke it. Revoking deletes their keys for that app, and the app asks for consent again next time.
Known differences
databaseIdand the RESTidare integers for users, organizations, repositories, issues, pull requests, comments, labels, releases and release assets. SSH keys still answernull.- Milestones, reactions, issue types, projects and sub-issues are always empty.
- A closed issue's
stateReasonis alwaysCOMPLETED. Ghost does not record why an issue was closed. - A comment's
authorAssociationisOWNERorNONE, neverMEMBERorCOLLABORATOR. auto_init,gitignore_templateandlicense_templateare accepted and ignored when creating a repository over REST. The repository starts empty.gh issue createandgh issue editwith--milestoneor--projectfail: Ghost has no milestones or projects.gh repo create --disable-issues,--disable-wikiand--homepagefail: they needupdateRepository, which is not served.- A query with an unknown field answers HTTP 400. GitHub answers 200.
ghhandles both. searchonly finds issues in one repository, named withrepo:. Qualifiers Ghost cannot filter on, such asmilestone:andsort:, are ignored.- API keys made in Ghost's settings hold every scope. Keys from
gh auth login --webor an OAuth app hold the scopes requested and approved that Ghost knows (repo,read:org,gist, …). Unknown scopes, such asworkflow, 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_reasonis accepted and not stored; a closed issue answerscompleted.- A comment's
html_urlends 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/labelswith a bare string body is refused: Ghost only parses JSON objects and arrays.GET /assigneeslists the owner, collaborators and organization members.GET /assignees/:assigneeanswers 204 for any user, as Ghost lets anyone be assigned.documentation_urlin REST errors always points athttps://docs.github.com/rest.- Pull requests have no checks or commit statuses yet (#61):
statusCheckRollupis always null andgh pr checksshows none. - No auto-merge, merge queue, review requests or branch protection:
autoMergeRequestis null,reviewRequestsis empty, andreviewDecisionisAPPROVED,CHANGES_REQUESTEDor null, neverREVIEW_REQUIRED. maintainerCanModifyis always false, and a pull request's base branch cannot be changed.- A pull request's REST
idanddatabaseIdare its issue's. GitHub gives the two different ids. - A commit author's
useris the account with that verified email, primary or added, or null. - Not served: creating or editing reviews over REST, review comments over GraphQL,
updatePullRequestBranch, andgh pr checks. - The latest release is the newest published release that is not a prerelease, by when it was published.
make_latestis accepted and ignored. GitHub orders by the release commit's date and letsmake_latestchoose. - A release's
target_commitishis 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_urlandzipball_urlare null: Ghost has no source archives yet (#66). An asset'sdigestis null and itsstatealwaysuploaded.- 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
nameordering 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 releasebody_html,body_textandmentions_countfields. - 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/issuesand/orgs/:org/issues.
Not yet supported
- Gists
- GitHub Apps and fine-grained tokens