- The primary key is unchanged. Nothing writes the column, nothing references it, and Ghost's own API does not return it. A schema comment on each says it exists for the GitHub compatibility layer.
Mappers live in `apps/api/src/lib/github/rest.ts` under the spec's mapper exception, each with a one-line reason: `IssuesService` hands back Ghost's DTOs and the REST shape renames, nests and adds URLs to them.
A list loads every user its page mentions (authors, assignees, closers) in one query by username, and the `merged_at` of its pull requests in one query by issue id.
- GitHub silently drops `labels` and `assignees` sent by someone without push access when creating or editing an issue. The compat layer drops them unless the viewer has triage access or above, so the service's permission checks are not reached with values GitHub would ignore.
- Add and remove read the current set and call `setIssueLabels` / `setIssueAssignees`. Two concurrent adds can lose one, as with the GraphQL mutations; marked
- A comment route loads the comment by `numeric_id` with its issue's number and repository; a comment from another repository answers 404. Only the comment's author can edit it, as in Ghost (GitHub also lets maintainers).
- Ghost lets any user be assigned. `GET /assignees` lists the people with access (owner, accepted collaborators, organization members); `GET /assignees/:assignee`
-`POST /issues/:number/assignees` from someone who cannot triage is ignored and answers the unchanged issue, as GitHub documents. The label routes on an issue leave the permission check to `setIssueLabels`
`apps/docs/content/docs/github-cli/index.mdx` lists the new routes and `gh label` commands, replaces "`
)
. Milestone 2 of
[
the compatibility spec
]
(
2026-10-09-gh-cli-compat-design.md
)
.
`
gh api … --paginate
`
and typed SDKs (octokit, go-github, PyGithub, Terraform's GitHub provider) work against Ghost. Every route, parameter, status code and required response field below was checked against GitHub's OpenAPI description (
`
github/rest-api-description
`
,
`
descriptions/api.github.com/api.github.com.json
`
).
numeric_id
`
`
id
`
, and its comment methods take
`
commentID int64
`
, so comment routes keyed by id are unreachable without an integer. Clients also key their own state on
`
id
`
, so
`
null
`
everywhere makes every object collide.
`
,
`
issue
`
,
`
issue_comment
`
and
`
label
`
each gain:
,
{
mode
:
'
number
'
}
)
.
generatedAlwaysAsIdentity
()
.
notNull
()
.
unique
()
`
databaseId
`
on every type that has one, replacing today's
`
null
`
.
`
+
`
Link
`
. Filters
`
state
`
,
`
labels
`
,
`
assignee
`
(
`
none
`
,
`
*
`
, login),
`
creator
`
,
`
sort
`
,
`
direction
`
,
`
since
`
.
`
milestone
`
,
`
mentioned
`
,
`
type
`
,
`
issue_field_values
`
are ignored. Includes pull requests.
|
`
+
`
Location
`
|
`
|
`
|
`
+
`
Link
`
,
`
since
`
|
`
+
`
Location
`
|
`
+
`
Link
`
,
`
sort
`
(
`
created
`
,
`
updated
`
),
`
direction
`
,
`
since
`
|
`
|
`
|
`
+
`
Link
`
|
`
(all labels on the issue)
|
`
|
`
; 404 when the label is not on the issue
|
`
(no
`
Location
`
); at most 10 logins
|
`
|
`
+
`
Link
`
|
`
+
`
Link
`
|
`
+
`
Location
`
|
`
|
`
; body
`
new_name
`
,
`
color
`
,
`
description
`
|
`
title
`
(string or integer),
`
body
`
,
`
labels
`
(strings or
`
{ name }
`
),
`
assignees
`
, the deprecated
`
assignee
`
,
`
state
`
,
`
state_reason
`
(
`
completed
`
,
`
not_planned
`
,
`
duplicate
`
,
`
reopened
`
),
`
duplicate_issue_id
`
and
`
milestone
`
.
`
milestone
`
other than
`
null
`
answers 422.
`
state_reason
`
and
`
duplicate_issue_id
`
are accepted and not stored.
`
{ labels: string[] }
`
,
`
string[]
`
,
`
{ name }[]
`
and
`
{ labels: { name }[] }
`
. GitHub's
`
PUT
`
also takes a bare string; Ghost's JSON parser is strict and refuses it, listed as a difference.
`
carries
`
first
`
,
`
prev
`
,
`
next
`
and
`
last
`
with page numbers;
`
last
`
comes from the list's count. go-github and PyGithub read
`
page
`
out of
`
Link
`
as an integer, so a cursor there would end their loops after one page. GraphQL keeps keyset paging.
`
offset
`
,
`
includePullRequests
`
,
`
since
`
,
`
assigned
`
) that Ghost's own controller never passes, so its DTO and API are unchanged.
with the issue number each belongs to.
)
);
`
locked
`
is
`
false
`
.
`
/orgs/:org/issues
`
: cross-repository lists with no service behind them.
`
restUser
`
in
`
users.controller.ts
`
moves there and is shared.
`
`
,
`
avatar_url
`
,
`
gravatar_id
`
(
`
''
`
),
`
url
`
,
`
html_url
`
,
`
type
`
(
`
User
`
or
`
Organization
`
),
`
site_admin
`
(
`
false
`
), and the URL templates GitHub requires (
`
followers_url
`
,
`
following_url
`
,
`
gists_url
`
,
`
starred_url
`
,
`
subscriptions_url
`
,
`
organizations_url
`
,
`
repos_url
`
,
`
events_url
`
,
`
received_events_url
`
) in GitHub's form under
`
/api/v3/users/:login
`
. Nothing serves most of those; typed decoders expect the keys.
`
`
number
`
,
`
title
`
,
`
body
`
|
`
numeric_id
`
,
`
I_…
`
(
`
PR_…
`
for a pull request), the issue
|
`
created_at
`
,
`
updated_at
`
|
the issue
|
`
when closed, else
`
null
`
|
`
assignee
`
,
`
closed_by
`
|
`
simple-user
`
;
`
assignee
`
is the first assignee or
`
null
`
|
`
|
`
labels_url
`
,
`
comments_url
`
,
`
events_url
`
,
`
timeline_url
`
|
API origin
|
`
or
`
/pull/:n
`
|
`
milestone
`
|
`
false
`
,
`
null
`
,
`
null
`
|
`
when the author owns the repository, else
`
NONE
`
, as in GraphQL
|
`
(
`
/api/v3/repos/:o/:r/pulls/:n
`
, served from milestone 3),
`
html_url
`
,
`
diff_url
`
,
`
patch_url
`
,
`
merged_at
`
|
`
repository
`
,
`
reactions
`
,
`
sub_issues_summary
`
,
`
type
`
,
`
pinned_comment
`
,
`
issue_field_values
`
,
`
performed_via_github_app
`
.
`
`
),
`
url
`
(
`
/labels/:name
`
, encoded),
`
name
`
,
`
color
`
,
`
description
`
,
`
default
`
(
`
false
`
),
`
archived_at
`
and
`
archived_by
`
(
`
null
`
).
`
`
),
`
url
`
(
`
/issues/comments/:id
`
),
`
issue_url
`
,
`
html_url
`
,
`
body
`
,
`
user
`
,
`
created_at
`
,
`
updated_at
`
,
`
author_association
`
.
`
html_url
`
keeps the web app's anchor (
`
#issuecomment-<Ghost id>
`
) so the link opens on the comment.
does: a private repository answers 404 to anonymous callers and to tokens without
`
repo
`
.
.
,
`
PATCH /issues/:number
`
,
`
POST
`
and
`
PUT /issues/:number/labels
`
. Confirmed against github.com on 2026-10-11.
`
ponytail:
`
.
`
reopenIssue
`
when it changes, so closing a closed issue answers 200 as on GitHub. The GraphQL
`
updateIssue
`
gets the same fix.
and
`
GET /issues/:number/assignees/:assignee
`
answer 204 for any existing user. An unknown login in a write answers 422 on
`
assignees
`
.
.
`
), and answers validation failures in GitHub's
`
validation-error
`
shape with 422:
`
|
`
; class-validator starts each message with the field
|
`
,
`
milestone
`
|
`
closed_by
`
),
`
Link
`
building (first, middle, last, single page),
`
per_page
`
clamping, label body normalization for every accepted shape, the 422 error shapes.
`
repo
`
.
`
numeric_id
`
is an integer on every object and equals GraphQL
`
databaseId
`
; sign-up still works. Paging across 35 issues. Pull requests in
`
GET /issues
`
. Silent drops for a reader, unknown labels created, add and remove composition with the documented status codes, comment by
`
numeric_id
`
including another repository's id, duplicate label as 422
`
already_exists
`
, closing twice.
`
.
**
`
gh label create/edit/delete/list/clone
`
,
`
gh api repos/:o/:r/issues --paginate
`
, and a comment edit through
`
gh api
`
.
databaseId
`
and the REST
`
id
`
are always
`
null
`
", and adds the out-of-scope list and these differences: only a comment's author edits it,
`
state_reason
`
is not stored, comment
`
html_url
`
anchors use Ghost's id, a bare-string label body is refused, how assignees are checked,