Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 39 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -700,6 +700,10 @@ JWT authentication: sign a JWT with `{ iss: "<app_id>" }` using the app's privat

Installation access tokens act as the configured GitHub App bot for repository writes. Repository ownership, selected repository access, and requested App permissions remain enforced. Pull request merges require `contents: write` on the base repository. Pull request branch updates require `pull_requests: write` on the pull request repository and `contents: write` on the head repository.

User tokens can discover accessible installations with `GET /user/installations` and their repositories with `GET /user/installations/:installation_id/repositories`. Both endpoints paginate; discovery respects installation repository selection and explicit user access through ownership, collaboration, or organization membership. Public visibility alone does not grant discovery access.

Installation tokens can list their repositories with `GET /installation/repositories`. The endpoint paginates and intersects the token's selection with current installation grants. Removed or suspended installations cannot discover repositories.

Inspect secret-free metadata for minted installation tokens at `GET /_emulate/installation-tokens`.

**App webhook delivery**: When events occur on repos where a GitHub App is installed, the emulator mirrors real GitHub behavior:
Expand Down Expand Up @@ -844,6 +848,7 @@ Every endpoint below is fully stateful. Creates, updates, and deletes persist in
- `GET /user` - authenticated user
- `PATCH /user` - update profile
- `GET /users/:username` - get user
- `GET /user/:account_id` - resolve the current user profile by durable ID
- `GET /users` - list users
- `GET /users/:username/repos` - list user repos
- `GET /users/:username/orgs` - list user orgs
Expand Down Expand Up @@ -895,22 +900,54 @@ Every endpoint below is fully stateful. Creates, updates, and deletes persist in
- `PUT /repos/:owner/:repo/pulls/:number/merge` - merge (with branch protection enforcement)
- `GET /repos/:owner/:repo/pulls/:number/commits` - list commits
- `GET /repos/:owner/:repo/pulls/:number/files` - list files
- `POST/DELETE /repos/:owner/:repo/pulls/:number/requested_reviewers` - manage reviewers
- `GET/POST/DELETE /repos/:owner/:repo/pulls/:number/requested_reviewers` - list or manage user and team reviewers; removals emit `pull_request.review_request_removed`
- `PUT /repos/:owner/:repo/pulls/:number/update-branch` - update branch

Updating only a pull request's `base` branch emits `pull_request.edited` with the new base ref and SHA.

Pull request authors cannot be requested to review their own pull request. Submitting a review removes the author from the outstanding requested users; pending reviews retain the request. Team requests remain until explicitly removed.

### Comments
- Issue comments: full CRUD on `/repos/:owner/:repo/issues/:number/comments`
- Review comments: full CRUD on `/repos/:owner/:repo/pulls/:number/comments`
- Review replies: `POST /repos/:owner/:repo/pulls/:number/comments/:id/replies`
- Commit comments: full CRUD on `/repos/:owner/:repo/commits/:sha/comments`
- Repo-wide listings for each type

Issue responses and issue-comment webhooks include `pull_request` metadata when the issue represents a pull request.

Review comments validate paths and lines against stored commit diffs. Comments on unpushed files or lines return `422` until those changes are pushed. Git refs and Contents API writes advance open pull-request heads and emit `pull_request.synchronize` webhooks. Review replies retain their parent's location. Legacy diff positions, binary diffs, and full multiline-range validation are not covered.

### Reactions

- `GET/POST /repos/:owner/:repo/issues/:number/reactions` - list/create issue or pull-request body reactions
- `GET/POST /repos/:owner/:repo/issues/comments/:id/reactions` - list/create issue-comment reactions
- `GET/POST /repos/:owner/:repo/pulls/comments/:id/reactions` - list/create review-comment reactions
- `DELETE` on any of these paths with `/:reaction_id` - remove a reaction

Lists support `content`, `page`, and `per_page`. Repeating a reaction by the same user returns the existing reaction; issue and comment summaries reflect stored reactions. Reaction changes do not emit webhooks.

### Reviews
- `GET /repos/:owner/:repo/pulls/:number/reviews` - list
- `POST /repos/:owner/:repo/pulls/:number/reviews` - create (with inline comments)
- `GET/PUT /repos/:owner/:repo/pulls/:number/reviews/:id` - get/update
- `DELETE /repos/:owner/:repo/pulls/:number/reviews/:id` - discard a pending review and its comments
- `POST /repos/:owner/:repo/pulls/:number/reviews/:id/events` - submit
- `PUT /repos/:owner/:repo/pulls/:number/reviews/:id/dismissals` - dismiss

Omit `event` when creating a review to keep it pending. Each reviewer can have one pending review per pull request; review listings and review-specific reads expose it only to its author. Individual comment reads and repository-wide comment listings also hide pending review comments from other users and anonymous readers. Inline comments are validated before a review is stored. Pending edits emit no public webhooks, while submission and submitted-summary edits emit review events.

### GraphQL collaboration

The `POST /graphql` endpoint supports a collaboration subset backed by the same pull requests and comments as REST:

- `repository.pullRequest` with paginated `reviewThreads`, comment identities, and resolution state
- `addPullRequestReviewThread` to add an inline comment to an existing pending review using its `pullRequestReviewId`
- `convertPullRequestToDraft` and `markPullRequestReadyForReview`
- `resolveReviewThread` and `unresolveReviewThread`

Draft/ready transitions and public thread resolution changes emit the corresponding webhooks. This is not a complete GitHub GraphQL schema; diff-validation limitations also apply to GraphQL comments.

### Labels & Milestones
- Labels: full CRUD, add/remove from issues, replace all
- Milestones: full CRUD, state transitions, issue counts
Expand Down Expand Up @@ -955,6 +992,7 @@ Every endpoint below is fully stateful. Creates, updates, and deletes persist in
- Secrets: repo + org CRUD

### Checks
- Commit statuses: create and list by commit or ref, newest first, with pagination and signed `status` webhooks. Status history is retained; combined-status summaries are not implemented.
- Check runs: create, update, get, annotations, rerequest, list by ref/suite. Ref based lookups accept branch and tag refs containing slashes.
- Check suites: create, get, preferences, rerequest, list by ref. Ref based lookups accept branch and tag refs containing slashes.
- Automatic suite status rollup from check run results
Expand Down
40 changes: 39 additions & 1 deletion apps/web/content/docs/github.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ Expiry is informational and does not change authorization behavior. `createEmula

Installation access tokens act as the configured GitHub App bot for repository writes. Repository ownership, selected repository access, and requested App permissions remain enforced. Pull request merges require `contents: write` on the base repository. Pull request branch updates require `pull_requests: write` on the pull request repository and `contents: write` on the head repository.

User tokens can discover accessible installations with `GET /user/installations` and their repositories with `GET /user/installations/:installation_id/repositories`. Both endpoints paginate; discovery respects installation repository selection and explicit user access through ownership, collaboration, or organization membership. Public visibility alone does not grant discovery access.

Installation tokens can list their repositories with `GET /installation/repositories`. The endpoint paginates and intersects the token's selection with current installation grants. Removed or suspended installations cannot discover repositories.

The CLI can generate omitted keys when you request a private delivery file:

```bash
Expand Down Expand Up @@ -77,6 +81,7 @@ Organization `members` are optional. Each entry references a seeded user by `log
- `GET /user` - authenticated user
- `PATCH /user` - update profile
- `GET /users/:username` - get user
- `GET /user/:account_id` - resolve the current user profile by durable ID
- `GET /users` - list users
- `GET /users/:username/repos` - list user repos
- `GET /users/:username/orgs` - list user orgs
Expand Down Expand Up @@ -132,24 +137,56 @@ Organization `members` are optional. Each entry references a seeded user by `log
- `PUT /repos/:owner/:repo/pulls/:number/merge` - merge (with branch protection enforcement)
- `GET /repos/:owner/:repo/pulls/:number/commits` - list commits
- `GET /repos/:owner/:repo/pulls/:number/files` - list files
- `POST/DELETE /repos/:owner/:repo/pulls/:number/requested_reviewers` - manage reviewers
- `GET/POST/DELETE /repos/:owner/:repo/pulls/:number/requested_reviewers` - list or manage user and team reviewers; removals emit `pull_request.review_request_removed`
- `PUT /repos/:owner/:repo/pulls/:number/update-branch` - update branch

Updating only a pull request's `base` branch emits `pull_request.edited` with the new base ref and SHA.

Pull request authors cannot be requested to review their own pull request. Submitting a review removes the author from the outstanding requested users; pending reviews retain the request. Team requests remain until explicitly removed.

## Comments

- Issue comments: full CRUD on `/repos/:owner/:repo/issues/:number/comments`
- Review comments: full CRUD on `/repos/:owner/:repo/pulls/:number/comments`
- Review replies: `POST /repos/:owner/:repo/pulls/:number/comments/:id/replies`
- Commit comments: full CRUD on `/repos/:owner/:repo/commits/:sha/comments`
- Repo-wide listings for each type

Issue responses and issue-comment webhooks include `pull_request` metadata when the issue represents a pull request.

Review comments validate paths and lines against stored commit diffs. Comments on unpushed files or lines return `422` until those changes are pushed. Git refs and Contents API writes advance open pull-request heads and emit `pull_request.synchronize` webhooks. Review replies retain their parent's location. Legacy diff positions, binary diffs, and full multiline-range validation are not covered.

## Reactions

- `GET/POST /repos/:owner/:repo/issues/:number/reactions` - list/create issue or pull-request body reactions
- `GET/POST /repos/:owner/:repo/issues/comments/:id/reactions` - list/create issue-comment reactions
- `GET/POST /repos/:owner/:repo/pulls/comments/:id/reactions` - list/create review-comment reactions
- `DELETE` on any of these paths with `/:reaction_id` - remove a reaction

Lists support `content`, `page`, and `per_page`. Repeating a reaction by the same user returns the existing reaction; issue and comment summaries reflect stored reactions. Reaction changes do not emit webhooks.

## Reviews

- `GET /repos/:owner/:repo/pulls/:number/reviews` - list
- `POST /repos/:owner/:repo/pulls/:number/reviews` - create (with inline comments)
- `GET/PUT /repos/:owner/:repo/pulls/:number/reviews/:id` - get/update
- `DELETE /repos/:owner/:repo/pulls/:number/reviews/:id` - discard a pending review and its comments
- `POST /repos/:owner/:repo/pulls/:number/reviews/:id/events` - submit
- `PUT /repos/:owner/:repo/pulls/:number/reviews/:id/dismissals` - dismiss

Omit `event` when creating a review to keep it pending. Each reviewer can have one pending review per pull request; review listings and review-specific reads expose it only to its author. Individual comment reads and repository-wide comment listings also hide pending review comments from other users and anonymous readers. Inline comments are validated before a review is stored. Pending edits emit no public webhooks, while submission and submitted-summary edits emit review events.

## GraphQL collaboration

The `POST /graphql` endpoint supports a collaboration subset backed by the same pull requests and comments as REST:

- `repository.pullRequest` with paginated `reviewThreads`, comment identities, and resolution state
- `addPullRequestReviewThread` to add an inline comment to an existing pending review using its `pullRequestReviewId`
- `convertPullRequestToDraft` and `markPullRequestReadyForReview`
- `resolveReviewThread` and `unresolveReviewThread`

Draft/ready transitions and public thread resolution changes emit the corresponding webhooks. This is not a complete GitHub GraphQL schema; diff-validation limitations also apply to GraphQL comments.

## Labels & Milestones

- Labels: full CRUD, add/remove from issues, replace all
Expand Down Expand Up @@ -202,6 +239,7 @@ Organization `members` are optional. Each entry references a seeded user by `log

## Checks

- Commit statuses: create and list by commit or ref, newest first, with pagination and signed `status` webhooks. Status history is retained; combined-status summaries are not implemented.
- Check runs: create, update, get, annotations, rerequest, list by ref/suite. Ref based lookups accept branch and tag refs containing slashes.
- Check suites: create, get, preferences, rerequest, list by ref. Ref based lookups accept branch and tag refs containing slashes.
- Automatic suite status rollup from check run results
Expand Down
18 changes: 18 additions & 0 deletions packages/@emulators/github/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ npm install @emulators/github
- `POST/DELETE /repos/:owner/:repo/pulls/:number/requested_reviewers` — manage reviewers
- `PUT /repos/:owner/:repo/pulls/:number/update-branch` — update branch

Updating only a pull request's `base` branch emits `pull_request.edited` with the new base ref and SHA.

### Comments
- Issue comments: full CRUD on `/repos/:owner/:repo/issues/:number/comments`
- Review comments: full CRUD on `/repos/:owner/:repo/pulls/:number/comments`
Expand All @@ -80,9 +82,23 @@ npm install @emulators/github
- `GET /repos/:owner/:repo/pulls/:number/reviews` — list
- `POST /repos/:owner/:repo/pulls/:number/reviews` — create (with inline comments)
- `GET/PUT /repos/:owner/:repo/pulls/:number/reviews/:id` — get/update
- `DELETE /repos/:owner/:repo/pulls/:number/reviews/:id` — discard a pending review and its comments
- `POST /repos/:owner/:repo/pulls/:number/reviews/:id/events` — submit
- `PUT /repos/:owner/:repo/pulls/:number/reviews/:id/dismissals` — dismiss

Omit `event` when creating a review to keep it pending. Each reviewer can have one pending review per pull request; review listings and review-specific reads expose it only to its author. Individual comment reads and repository-wide comment listings also hide pending review comments from other users and anonymous readers. Inline comments are validated before a review is stored. Pending edits emit no public webhooks, while submission and submitted-summary edits emit review events.

### GraphQL collaboration

The `POST /graphql` endpoint supports a collaboration subset backed by the same pull requests and comments as REST:

- `repository.pullRequest` with paginated `reviewThreads`, comment identities, and resolution state
- `addPullRequestReviewThread` to add an inline comment to an existing pending review using its `pullRequestReviewId`
- `convertPullRequestToDraft` and `markPullRequestReadyForReview`
- `resolveReviewThread` and `unresolveReviewThread`

Draft/ready transitions and public thread resolution changes emit the corresponding webhooks. This is not a complete GitHub GraphQL schema; diff-validation limitations also apply to GraphQL comments.

### Labels & Milestones
- Labels: full CRUD, add/remove from issues, replace all
- Milestones: full CRUD, state transitions, issue counts
Expand Down Expand Up @@ -215,6 +231,8 @@ npx emulate start --service github --seed emulate.config.yaml \

The destination must not exist. emulate removes inherited ACLs, verifies effective owner-only access, and publishes complete JSON before opening listeners or configuring portless. Handled startup failures remove the invocation-owned artifact. A hard termination can leave a complete artifact that must be removed manually after confirming no invocation is using it. Explicit keys are excluded from the artifact. Linux requires `setfacl` and `getfacl` from the `acl` package. The flag fails closed when access controls cannot be verified and is not supported on Windows. Without `--generated-secrets-file`, CLI seed files continue requiring `private_key`.

Installation tokens can list repositories with `GET /installation/repositories`. Results paginate and intersect the token's selection with current installation grants. Removed or suspended installations cannot discover repositories. User-token discovery remains limited to explicit repository access.

## Links

- [Full documentation](https://emulate.dev/github)
Expand Down
3 changes: 2 additions & 1 deletion packages/@emulators/github/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@
"prepack": "node ../../../scripts/prepare-package.mjs"
},
"dependencies": {
"@emulators/core": "workspace:*"
"@emulators/core": "workspace:*",
"graphql": "^17.0.0"
},
"devDependencies": {
"tsup": "^8",
Expand Down
50 changes: 50 additions & 0 deletions packages/@emulators/github/src/__tests__/checks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,56 @@ describe("GitHub checks routes", () => {
app = createTestApp().app;
});

it("retains paginated commit-status history and enforces repository access", async () => {
const branch = await app.request(`${base}/repos/octocat/hello-world/branches/main`, {
headers: authHeaders(),
});
const { commit } = (await branch.json()) as { commit: { sha: string } };
const path = `${base}/repos/octocat/hello-world`;
const ref = await app.request(`${path}/git/refs`, {
method: "POST",
headers: jsonHeaders(),
body: JSON.stringify({ ref: "refs/heads/status/nested", sha: commit.sha }),
});
expect(ref.status).toBe(201);
for (const state of ["pending", "failure", "success"]) {
const response = await app.request(`${path}/statuses/${commit.sha}`, {
method: "POST",
headers: jsonHeaders(),
body: JSON.stringify({ state, context: "ci", target_url: "https://ci.example.com" }),
});
expect(response.status).toBe(201);
expect(await response.json()).toMatchObject({
state,
context: "ci",
target_url: "https://ci.example.com",
creator: { login: "octocat" },
});
}
const first = await app.request(`${path}/commits/status/nested/statuses?per_page=2`, {
headers: authHeaders(),
});
expect(first.status).toBe(200);
expect(first.headers.get("link")).toContain('rel="next"');
expect(await first.json()).toMatchObject([{ state: "success" }, { state: "failure" }]);
const last = await app.request(`${path}/commits/${commit.sha}/statuses?per_page=2&page=2`, {
headers: authHeaders(),
});
expect(await last.json()).toMatchObject([{ state: "pending" }]);
const invalid = await app.request(`${path}/statuses/${commit.sha}`, {
method: "POST",
headers: jsonHeaders(),
body: JSON.stringify({ state: "completed" }),
});
expect(invalid.status).toBe(422);
const anonymous = await app.request(`${path}/statuses/${commit.sha}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ state: "success" }),
});
expect(anonymous.status).toBe(401);
});

it("lists check runs and suites for refs containing slashes", async () => {
const commits = await app.request(`${base}/repos/octocat/hello-world/commits`, { headers: authHeaders() });
const [head] = (await commits.json()) as Array<{ sha: string }>;
Expand Down
Loading