# GitHub API

> Link GitHub issues to test cases, runs and defects, create issues from defects, and launch GitHub Actions workflows over the integrator API.

These endpoints work with the organization's [GitHub integration](https://docs.probara.net/en/guides/integrations/github/):
the GitHub accounts where the Probara GitHub App is installed. Installing the app, disconnecting an
account, changing a project's repositories and its pull request comments setting need a signed-in
session; with an API token they answer `403 forbidden`. Everything else works with an
[API token](https://docs.probara.net/en/developers/authentication/) and through the [MCP tools](https://docs.probara.net/en/developers/mcp-tools/).

Every endpoint needs the Pro or Enterprise plan; on other plans it answers `403 forbidden`. A
`{projectId}` is the project **code**, such as `SHOP`. Repositories are written `owner/name`.

## Repositories a project may use

A project offers the repositories of its **mapping**. When the mapping is empty, it offers every
repository the organization's installations can see. Every endpoint below that takes a
`repository` checks it against that rule.

| Method and path                                                              | What it does                                                                                                                               |
| ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `GET /api/v1/projects/{projectId}/integrations/github/repositories`          | The project's mapping: `items` with `connectionUlid`, `repositoryId`, `repositoryFullName`, `isDefault`. Empty means no mapping.           |
| `GET /api/v1/projects/{projectId}/integrations/github/linkable-repositories` | The repositories the project offers now, mapping or not: `connectionUlid`, `repositoryId`, `fullName`, `isDefault`.                        |
| `PUT /api/v1/projects/{projectId}/integrations/github/repositories`          | Replaces the mapping with `{ "repositories": [{ "connectionUlid", "repositoryId", "isDefault" }] }`. Session only, `integrations.manage`.  |
| `GET /api/v1/projects/{projectId}/integrations/github/settings`              | `{ "pullRequestComments": "on_failure" }`: when closed GitHub Actions runs comment on their pull request: `always`, `on_failure` or `off`. |
| `PUT /api/v1/projects/{projectId}/integrations/github/settings`              | Sets `pullRequestComments` to `always`, `on_failure` or `off`. Session only, `integrations.manage`.                                        |

## Link issues

The same four endpoints exist for test cases, runs and defects. `{entity}` is
`test-cases/{caseUlid}`, `runs/{runUlid}` or `defects/{defectUlid}`.

| Method and path                                          | What it does                                                    |
| -------------------------------------------------------- | --------------------------------------------------------------- |
| `GET /api/v1/{entity}/github-issues`                     | Lists the entity's links.                                       |
| `POST /api/v1/{entity}/github-issues/link`               | Links an issue. Answers `201` with the link.                    |
| `POST /api/v1/{entity}/github-issues/{linkUlid}/refresh` | Reads the issue from GitHub again and answers the updated link. |
| `DELETE /api/v1/{entity}/github-issues/{linkUlid}`       | Removes the link. The issue stays in GitHub. Answers `204`.     |

```json
{
  "connectionUlid": "01J9Z3K4M5N6P7Q8R9S0T1V2W3",
  "repository": "acme/shop",
  "issueNumber": 42,
  "linkKind": "requirement"
}
```

`linkKind` is `requirement` or `defect`, and defaults to `requirement` on a test case and `defect`
on a run or a defect. Only `defect` links on a defect sync the defect's status with the issue;
requirement links on test cases are what GitHub traceability reports count as coverage.

A link carries `repositoryFullName`, `issueNumber`, `linkKind`, and the issue's `title`, `state`
(`open` or `closed`), `stateReason`, `labels`, `authorLogin`, `htmlUrl` and `issueUpdatedAt` as
GitHub last answered, with `syncedAt`, when Probara last read them. These cached fields are
answered even while GitHub is unreachable, and refresh on every change GitHub reports.

Reading links needs access to the entity. Changing them needs `test-cases.write` on a test case,
`runs.execute` on a run and `defects.write` on a defect.

### Search a repository's issues

`GET /api/v1/projects/{projectId}/integrations/github/issues/search?connectionUlid=…&repository=acme/shop&q=checkout`
searches the repository's issues by words of the title, or reads one by number with `q=42` or
`q=#42`. `q` holds up to 200 characters and is searched as plain text: it cannot add GitHub search
qualifiers. It answers up to 20 issues, never pull requests.

## Create an issue from a defect

| Method and path                                               | What it does                                                                                                      |
| ------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| `GET /api/v1/defects/{defectUlid}/github-issues/draft`        | The prefilled `title` and `body`. Add `?resultUlid=` (a result linked to the defect) to prefill from that result. |
| `GET /api/v1/projects/{projectId}/integrations/github/labels` | The labels of a repository (`connectionUlid`, `repository`), with `name`, `color` and `description`.              |
| `POST /api/v1/defects/{defectUlid}/github-issues`             | Creates the issue and links it to the defect as `defect`.                                                         |

```json
{
  "connectionUlid": "01J9Z3K4M5N6P7Q8R9S0T1V2W3",
  "repository": "acme/shop",
  "resultUlid": "01J9Z3M7Q2R4S6T8V0W2X4Y6Z8",
  "labels": ["bug"]
}
```

`title` (up to 256 characters) and `body` (GitHub Markdown, up to 65,536 characters) are optional:
without them, the draft is used: its body is the defect description as Markdown, with each image at its stored asset URL. `labels` holds up to 20 names of the repository. It needs
`defects.write`.

The create is sent to GitHub once. It answers `201` with the new link, or `200` when a retry finds
the issue an earlier request opened. When GitHub's answer is lost, the request answers
`409 github_issue_creation_unconfirmed` with `Retry-After`: retry after that delay, and Probara
looks for the issue before opening another one.

## Launch a workflow

| Method and path                                                  | What it does                                                                              |
| ---------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| `GET /api/v1/projects/{projectId}/integrations/github/workflows` | The repository's active workflows (`connectionUlid`, `repository`): `id`, `name`, `path`. |
| `GET /api/v1/projects/{projectId}/integrations/github/branches`  | The repository's branches, up to 500.                                                     |
| `POST /api/v1/projects/{projectId}/integrations/github/launches` | Creates an automated run and starts the workflow on a branch.                             |

```json
{
  "connectionUlid": "01J9Z3K4M5N6P7Q8R9S0T1V2W3",
  "repository": "acme/shop",
  "ref": "main",
  "workflowId": "probara-e2e.yml",
  "runName": "Nightly E2E"
}
```

`workflowId` is an id from the workflows list or the workflow's file name. `runName` defaults to the
workflow name and the branch. `inputs` adds up to 24 more `workflow_dispatch` string inputs;
Probara always sends the run's ULID as `probara_run_id`. The workflow contract is in
[Running tests with GitHub Actions](https://docs.probara.net/en/guides/integrations/github-actions/#2-prepare-the-workflow).
It needs `runs.execute`, and accepts an `Idempotency-Key`.

- `201`: GitHub accepted the dispatch. The body is the run, with `githubBuild.launchStatus`
  `dispatched`.
- `202`: GitHub's answer was lost. The run is kept with `launchStatus` `unconfirmed`; its build fills
  in if the workflow did start.
- A refusal (`github_workflow_*`, `github_ref_not_found`) leaves no run behind.

`GET /api/v1/runs/{runUlid}` returns `githubBuild` for a launched run (`null` for any other run):
`launchStatus`, `repository`, `ref`, `workflowName`, `htmlUrl`, `status`, `conclusion`, `runAttempt`,
`startedAt` and `completedAt`. The build fields stay `null` until GitHub reports the workflow run.
Probara never closes the run from the build: the workflow's final job does.

## Traceability sources

GitHub connections appear in `GET /api/v1/projects/{projectId}/traceability/sources` with
`provider: "github"`. The repositories, labels and milestones a report can use are under
`/sources/{connectionUlid}/repositories`, `/labels?repository=` and `/milestones?repository=`. See
the [traceability reports API](https://docs.probara.net/en/developers/api/traceability-reports/#github-reports).

## Errors

Every error uses the [standard error envelope](https://docs.probara.net/en/developers/api-reference/#errors).

| Status | Code                                | When                                                                                                          |
| ------ | ----------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `403`  | `forbidden`                         | The plan does not include integrations, the caller lacks the permission, or a session-only route got a token. |
| `403`  | `github_repository_not_mapped`      | The project's mapping leaves the repository out.                                                              |
| `404`  | `github_issue_not_found`            | The repository has no issue with that number.                                                                 |
| `404`  | `github_workflow_not_found`         | The repository has no such workflow.                                                                          |
| `409`  | `github_issue_already_linked`       | The entity already links that issue.                                                                          |
| `409`  | `github_connection_stale`           | The installation was suspended or uninstalled in GitHub.                                                      |
| `409`  | `github_issue_creation_in_progress` | Another request is creating this defect's issue in that repository.                                           |
| `409`  | `github_issue_creation_unconfirmed` | GitHub's answer to an earlier create was lost. Retry after `Retry-After`.                                     |
| `422`  | `github_repository_not_visible`     | The installation cannot see the repository.                                                                   |
| `422`  | `github_issue_is_pull_request`      | The number is a pull request.                                                                                 |
| `422`  | `github_issues_disabled`            | Issues are turned off in the repository.                                                                      |
| `422`  | `github_issue_rejected`             | GitHub refused the new issue, for example for an unknown label.                                               |
| `422`  | `github_workflow_not_dispatchable`  | The workflow is disabled or has no `workflow_dispatch` trigger.                                               |
| `422`  | `github_ref_not_found`              | The repository has no such branch.                                                                            |
| `422`  | `github_workflow_missing_run_input` | The workflow does not declare the `probara_run_id` input under `workflow_dispatch`.                           |
| `422`  | `github_workflow_dispatch_failed`   | GitHub refused the dispatch, for example for an undeclared input. `details.message` holds GitHub's reason.    |
| `422`  | `validation_failed`                 | The body or a query parameter is malformed.                                                                   |
| `429`  | `too_many_requests`                 | The organization's GitHub request limit is reached. `Retry-After` says when to retry.                         |
| `503`  | `github_app_not_configured`         | The GitHub App is not set up in this environment.                                                             |
| `503`  | `github_unavailable`                | GitHub cannot be reached right now.                                                                           |
