# API procedure and fidelity checks

These examples use the authenticated GitHub CLI. Pass multiline bodies through
structured JSON on stdin or `--body-file`; do not interpolate Markdown into shell
commands. Use bounded request timeouts and account for secondary rate limits.

## Read-only source export

For each exact source `OWNER/REPO`:

| Data | REST endpoint (prefix `/repos/OWNER/REPO`) |
|---|---|
| Issues plus PR issue records | `/issues?state=all&per_page=100` |
| PR list and details | `/pulls?state=all&per_page=100`, `/pulls/N` |
| Ordinary issue/PR comments | `/issues/comments?per_page=100` |
| PR commits | `/pulls/N/commits?per_page=100` |
| PR reviews | `/pulls/N/reviews?per_page=100` |
| Inline review comments | `/pulls/N/comments?per_page=100` |
| Timeline | `/issues/N/timeline?per_page=100` |
| Labels/milestones | `/labels?per_page=100`, `/milestones?state=all&per_page=100` |

Use `gh api --paginate --slurp` and flatten pages. PR commit REST responses are
limited to 250 commits; compare against the PR detail's `commits` count, and use
paginated GraphQL commits or verified Git ancestry for larger PRs. Other API caps
(such as PR files) also need explicit completeness checks. Retain comments' HTML
URLs and node IDs so anchors can be remapped, not merely copied to a new host path.

Retrieve reviews and resolved/unresolved threads separately if full review fidelity
is requested. REST comments alone do not prove complete thread/reaction/attachment
coverage. Keep inaccessible source attachments as documented links/gaps rather
than silently claiming that their bytes were copied.

## Closing evidence (GraphQL)

```graphql
query($owner: String!, $name: String!, $number: Int!) {
  repository(owner: $owner, name: $name) {
    issue(number: $number) {
      id number state stateReason
      closedByPullRequestsReferences(first: 100) {
        pageInfo { hasNextPage endCursor }
        nodes { id number url merged }
      }
      timelineItems(first: 100, itemTypes: [CLOSED_EVENT]) {
        pageInfo { hasNextPage endCursor }
        nodes {
          ... on ClosedEvent {
            createdAt
            actor { login }
            closer {
              __typename
              ... on PullRequest { id number url merged }
              ... on Commit { oid url }
            }
          }
        }
      }
    }
  }
}
```

Paginate **nested** connections too. Use the latest closing event only when the
issue is currently closed; earlier closure followed by reopening is historical.
If closer is null, report no recorded PR/commit closer. Do not infer a PR from a
nearby merge time, branch name, author or a matching keyword alone.

Commit-triggered closure: link the same commit in the copy only after verifying
that it exists there. `/commits/SHA/pulls` and original PR commit inventories can
identify associated/containing PRs; label them as such. Multiple containing PRs
do not establish multiple closures.

## Native relationship mutation

Only after both target issue and PR exist and the source relationship is verified:

```graphql
mutation($issue: ID!, $prs: [ID!]!) {
  addCloseIssueReferences(input: {
    issueId: $issue, pullRequestIds: $prs
  }) { issue { id } }
}
```

The list is limited to ten IDs per call. Use destination IDs, never source IDs.
Inspect GraphQL `errors` even when HTTP status is 200. Reread the target issue's
`closedByPullRequestsReferences`, state and Development UI. If unavailable for an
account/API version, preserve mapped bidirectional links and state explicitly
that native relationship creation was unavailable; do not claim sidebar success.

PR-body `Closes #N` text and a native closing reference are not interchangeable:
closing-keyword behavior depends on target/default branch and PR state. When
updating already-merged archive PRs, verify that source-open issues remain open.

## Minimal presentation

Original title, original rendered body, then:

```markdown
<sub>Imported from [upstream #12](SOURCE_URL) · original-author · opened ORIGINAL_UTC · merged ORIGINAL_MERGED_UTC.</sub>
<!-- upstream-pr-import:OWNER/REPO#12; original_updated_at: ORIGINAL_UTC -->
```

Use `open upstream` or `closed unmerged ORIGINAL_UTC` when appropriate. An issue
uses `upstream-issue-import`; a comment can use:

```markdown
Original comment text, with references mapped.

<sub>[Original comment](SOURCE_COMMENT_URL) by original-author · ORIGINAL_UTC.</sub>
<!-- upstream-discussion:SOURCE_COMMENT_URL -->
```

Reviews can say `Original review … · approved` while remaining an ordinary
attributed comment. Keep source links unchanged in provenance, including comment
anchors. A new native approval cannot represent another person's old approval.

## Resumability and completion ledger

Write private state after each verified object, using atomic replacement. Persist
source/destination identities, expected content hashes, ref baselines, original
states, desired import mode, created object IDs and last verified phase. Never
delete or reset an existing target merely to restart an interrupted import.

For a write timeout, look up the stable source marker. One match: verify and
continue; zero matches: retry the intended write; multiple matches: stop that
object and resolve duplication. Respect unrelated edits made since the previous
run; do not overwrite them with stale exported content without reconciling them.

## Authoritative documentation

- [Pull request REST API](https://docs.github.com/en/rest/pulls/pulls)
- [Issue comments REST API](https://docs.github.com/en/rest/issues/comments)
- [Issue GraphQL types and closing-reference mutations](https://docs.github.com/en/graphql/reference/issues)
- [GitHub migration tooling](https://docs.github.com/en/migrations/overview/about-githubs-migration-tooling)

Recheck schemas and migration eligibility when a tool rejects a documented field;
do not guess unsupported timestamp or actor parameters.
