Forge (GitHub/GitLab)¶
Forge ABC¶
forge
¶
Git forge abstraction for GitLab and GitHub.
Provides a polymorphic interface for merge/pull request operations,
pipeline status checking, and review comment handling. Follows the
same ABC pattern as agentic_ci.backend and agentic_ci.harness.
Usage::
from agentic_ci.forge import Forge
forge = Forge.detect("https://gitlab.com/org/repo/-/merge_requests/42")
status = forge.mr_status("https://gitlab.com/org/repo/-/merge_requests/42")
Label helpers (callers choose names and create-vs-attach policy)::
forge = Forge.detect(repo_url)
if not forge.label_exists(repo_url, "autofix"):
forge.create_label(repo_url, "autofix")
forge.add_mr_labels(mr_url, ["autofix"])
Comment author trust (keep feedback only from repository owners, organization members and collaborators on GitHub, or Developers and above on GitLab; the GitHub check uses author_association, not repository permissions)::
forge = Forge.detect(mr_url, github_token=token)
threads = filter_trusted_threads(forge.review_comments(mr_url))
comments = filter_trusted_comments(forge.general_comments(mr_url))
GitHub comments carry author_association and GitLab comments carry
author_access_level. Only TRUSTED_GITHUB_ASSOCIATIONS and levels
at or above MIN_TRUSTED_GITLAB_ACCESS_LEVEL (Developer) are trusted.
Accounts that do not meet that bar but whose feedback is wanted, such as
review bots, are passed as trusted_authors and matched by account name
(see :func:comment_author_id)::
threads = filter_trusted_threads(
forge.review_comments(mr_url), trusted_authors={"coderabbitai[bot]"}
)
ForgeError
¶
Bases: Exception
Raised when a forge API operation fails.
MergeRequestResult
¶
Bases: tuple[str | None, str | None]
Result of creating or reusing a merge/pull request.
The result is a two-item (url, error) tuple, preserving the complete
historical tuple contract. Consumers that need to distinguish a newly
created request from a reused one should inspect :attr:created.
Forge
¶
Bases: ABC
Abstract base for git forge (GitLab/GitHub) API operations.
Concrete implementations handle authentication and API differences.
Use Forge.detect(url) to get the right implementation for a URL.
detect(url, *, github_token=None)
classmethod
¶
Return the correct Forge implementation for a URL.
Inspects the hostname to choose between GitLab and GitHub.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
url
|
str
|
Any URL on the forge (repo URL, MR/PR URL, etc.). |
required |
github_token
|
str | None
|
Token for GitHub API authentication. |
None
|
Raises:
| Type | Description |
|---|---|
ForgeError
|
If the URL hostname is not recognized. |
Source code in src/agentic_ci/forge/__init__.py
create_merge_request(repo_url, source_branch, target_branch, title, description, draft=False)
abstractmethod
¶
Create an MR/PR.
Returns a :class:MergeRequestResult. The result's created field
is true only when this call created the MR/PR; it is false when an
existing open MR/PR was reused or the operation failed. The result
continues to support url, error = unpacking for existing callers.
Only an open MR/PR whose source branch lives in the target repository
itself is reused, never one opened from a fork that happens to use
the same branch name.
Source code in src/agentic_ci/forge/__init__.py
mr_status(mr_url, *, ignored_checks=None)
abstractmethod
¶
Get MR/PR state, source branch, repositories, and pipeline status.
Returns a dict with:
state:"open","merged"or"closed". GitLab's transient"locked"(an MR in the middle of being merged) is passed through, not reported as open.source_branch: the source branch name.pipeline_status: the overall CI status.source_repo/target_repo: opaque identities of the repository the source branch lives in and the repository the MR/PR targets (GitLab project IDs as strings, GitHubowner/name). They are equal, compared case-insensitively, exactly when the MR/PR is not from a fork;source_repois""when unknown (a deleted fork).target_repo_path: the target repository's current path (group/sub/repoorowner/name), which follows renames and transfers. It has the forge's canonical casing; compare it with a configured repository case-insensitively, as both forges treat paths.
When ignored_checks is provided, check runs whose name (or
commit statuses whose context) is in the set are excluded
before determining the overall pipeline status.
Source code in src/agentic_ci/forge/__init__.py
review_comments(mr_url)
abstractmethod
¶
Get unresolved review comment threads with diff positions.
Returns a list of dicts with keys:
thread_id, file, line, body, author, comments.
body joins every comment as "<author>: <body>" lines and
author is the thread starter. comments lists each comment
of the thread as a dict with author, body, created_at
and the author's trust field: author_association on GitHub, or
author_username and author_access_level on GitLab. The
thread dict also carries the starter's trust field. Use
:func:filter_trusted_threads to drop untrusted comments.
Source code in src/agentic_ci/forge/__init__.py
general_comments(mr_url, since=None, skip_patterns=None)
abstractmethod
¶
Get general (non-diff-positioned) MR/PR comments.
Returns a list of dicts with keys: author, body, created_at,
plus the author's trust field: author_association on GitHub, or
author_username and author_access_level on GitLab. Use
:func:filter_trusted_comments to drop untrusted comments.
Comments created before since (ISO 8601) are excluded.
Comments containing any string in skip_patterns are excluded.
If skip_patterns is None, a default list is used.
Source code in src/agentic_ci/forge/__init__.py
reply(mr_url, thread_id, message)
abstractmethod
¶
resolve(mr_url, thread_id)
abstractmethod
¶
update_description(mr_url, *, title=None, description=None)
abstractmethod
¶
Update an existing MR/PR title and/or description.
Only the provided keyword arguments are updated; omitted fields are left unchanged.
Raises ForgeError on API failure.
Source code in src/agentic_ci/forge/__init__.py
label_exists(repo_url, label)
abstractmethod
¶
Return whether label exists on the repository/project.
Does not create the label. Returns False when the label is
missing (including HTTP 404). Raises ForgeError on other
API failures.
Source code in src/agentic_ci/forge/__init__.py
create_label(repo_url, label, *, color=None, description=None)
abstractmethod
¶
Create a repository/project label.
Callers must opt in explicitly; attaching labels does not
implicitly create them via this API. When color is omitted,
DEFAULT_LABEL_COLOR is used.
Raises ForgeError on API failure (including if the label
already exists).
Source code in src/agentic_ci/forge/__init__.py
add_mr_labels(mr_url, labels)
abstractmethod
¶
Attach labels to an existing MR/PR, preserving current labels.
No-op when labels is empty. Does not check whether labels
exist on the repository first — callers that need that policy
should use :meth:label_exists / :meth:create_label.
Raises ForgeError on API failure.
Source code in src/agentic_ci/forge/__init__.py
pipeline_failures(mr_url, *, ignored_checks=None)
abstractmethod
¶
Get failed CI job names and log tails.
Returns {"pipeline_status": str, "failed_jobs": [{"name", "id", "log"}]}.
When ignored_checks is provided, those checks are excluded from
both the pipeline status derivation and the failed_jobs list.
An additional ignored_checks_status key reports the aggregate
status of the ignored checks alone.
Source code in src/agentic_ci/forge/__init__.py
comment_author_id(comment)
¶
Return the account name that trusted_authors matches, lowercased.
A GitHub comment (one with an author_association key) is identified
by its login in author, which ends in [bot] for a GitHub App, so
an App cannot be confused with a user account of the same name. A
GitLab comment is identified by author_username, never by
author, which is the display name any user can set. Returns ""
when the account is unknown (a deleted account, for example).
Source code in src/agentic_ci/forge/__init__.py
is_trusted_comment(comment, trusted_authors=())
¶
Return whether a forge comment was written by a trusted author.
A comment whose :func:comment_author_id is in trusted_authors
(account names, compared without case) is trusted. Otherwise a GitHub
comment (one with an author_association key) is trusted when the
association is in TRUSTED_GITHUB_ASSOCIATIONS, and any other comment
only when its author_access_level is an integer at or above
MIN_TRUSTED_GITLAB_ACCESS_LEVEL. Comments carrying neither field
are untrusted, so the check fails closed.
Source code in src/agentic_ci/forge/__init__.py
filter_trusted_comments(comments, trusted_authors=())
¶
Return only the comments written by trusted authors.
Takes the output of :meth:Forge.general_comments (or any list of
comment dicts) and keeps those for which :func:is_trusted_comment
holds, with trusted_authors as extra trusted account names.
Source code in src/agentic_ci/forge/__init__.py
filter_trusted_threads(threads, trusted_authors=())
¶
Return review threads reduced to their trusted comments.
Takes the output of :meth:Forge.review_comments. Each thread's
comments keeps only trusted comments and body is rebuilt from
them, so an untrusted reply inside a trusted author's thread is
dropped. A thread with no trusted comment left, or without a
comments list, is dropped. The thread's author fields (author,
author_username, author_access_level, author_association)
are taken from the first trusted comment, so a thread an outsider
started carries none of the outsider's account data (a GitLab display
name is free text). Of the other thread fields only thread_id,
file and line are kept; any other key is dropped.
trusted_authors are extra trusted account names (see
:func:is_trusted_comment). Input dicts are not modified.
Source code in src/agentic_ci/forge/__init__.py
parse_gitlab_mr_url(url)
¶
Parse a GitLab MR URL into (project_path, mr_iid).
Raises ForgeError if the URL does not match the expected pattern.
Source code in src/agentic_ci/forge/__init__.py
parse_github_pr_url(url)
¶
Parse a GitHub PR URL into (owner/repo, pr_number).
Raises ForgeError if the URL does not match the expected pattern.
Source code in src/agentic_ci/forge/__init__.py
repo_path_from_url(url)
¶
Extract the repository path from a GitLab or GitHub URL.
Strips trailing slashes and .git suffixes.
Source code in src/agentic_ci/forge/__init__.py
detect_forge(url, *, github_token=None)
¶
GitHub¶
github
¶
GitHub forge implementation.
Provides GitHubForge for interacting with the GitHub REST and
GraphQL APIs (pull requests, check runs, review threads).
GitHubForge(token=None)
¶
Bases: Forge
GitHub REST + GraphQL API implementation of the Forge interface.
Source code in src/agentic_ci/forge/github.py
graphql(query, variables=None)
¶
Execute a GitHub GraphQL query or mutation.
Returns the data dict from the response.
Raises ForgeError on HTTP or GraphQL errors.
Source code in src/agentic_ci/forge/github.py
check_runs(repo_path, sha)
¶
Fetch all check runs for a commit SHA.
Returns (check_runs, accessible) where accessible is
False when the token lacks checks:read permission (403).
Source code in src/agentic_ci/forge/github.py
commit_statuses(repo_path, sha)
¶
Fetch commit statuses for a SHA.
GitHub has two parallel CI reporting mechanisms: Check Runs
(used by GitHub Actions) and Commit Statuses (the older API
used by external CI like Prow, Jenkins, and other integrations).
check_runs() only covers the first; this method covers the
second so _derive_pipeline_status() sees the full picture.
Merge-management contexts (tide, Mergify) are excluded
since they reflect merge policy, not CI results.
See https://docs.github.com/en/rest/commits/statuses
Source code in src/agentic_ci/forge/github.py
generate_github_jwt(app_id, private_key_pem)
¶
Generate a GitHub App JWT signed with RS256.
The JWT is valid for 10 minutes (GitHub maximum).
Requires the PyJWT and cryptography packages.
Install with: pip install agentic-ci[forge]
Source code in src/agentic_ci/forge/github.py
get_installation_token(jwt_token, installation_id)
¶
Exchange a GitHub App JWT for an installation access token.
Returns the token string (valid for 1 hour).
Raises RuntimeError on failure.
Source code in src/agentic_ci/forge/github.py
resolve_app_token(repo_url, github_config)
¶
Resolve a GitHub App installation token for a repo URL.
Extracts the GitHub org from repo_url, looks up the matching
App configuration in github_config, and returns a short-lived
installation token.
Returns None (with an error log) on any failure.
Source code in src/agentic_ci/forge/github.py
GitLab¶
gitlab
¶
GitLab forge implementation.
Provides GitLabForge for interacting with the GitLab REST API
(merge requests, pipelines, discussions).
GitLabForge(*, adapter=None)
¶
Bases: Forge
GitLab REST API implementation of the Forge interface.
Source code in src/agentic_ci/forge/gitlab.py
session
property
¶
The authenticated HTTP session for direct API calls.
project_id(project_path)
¶
Look up the numeric GitLab project ID from a project path.
Raises ForgeError if the project cannot be found.
Source code in src/agentic_ci/forge/gitlab.py
member_access_level(pid, user_id)
¶
Return a user's effective access level on a project.
Uses members/all so levels inherited from parent groups count.
Returns 0 when the user is not a member or the membership is not
active (for example awaiting seat approval or blocked),
and None when the level could not be resolved (for example HTTP
403). Results are cached per client, so repeated calls cost one
request per (pid, user_id).
Source code in src/agentic_ci/forge/gitlab.py
pipeline_metadata(project_path, pipeline_id)
¶
Get pipeline metadata.
Returns the raw pipeline dict from the GitLab API.
Source code in src/agentic_ci/forge/gitlab.py
pipeline_jobs(project_path, pipeline_id, *, scope=None)
¶
Get all jobs for a pipeline (paginated).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
project_path
|
str
|
GitLab project path (e.g. |
required |
pipeline_id
|
int
|
Numeric pipeline ID. |
required |
scope
|
str | None
|
Optional job scope filter (e.g. |
None
|
Returns a list of raw job dicts from the GitLab API.
Source code in src/agentic_ci/forge/gitlab.py
job_trace(project_path, job_id)
¶
Get the full trace log for a job.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
project_path
|
str
|
GitLab project path (e.g. |
required |
job_id
|
int
|
Numeric job ID. |
required |
Returns the trace text as a string.
Source code in src/agentic_ci/forge/gitlab.py
retry_job(project_path, job_id)
¶
Retry a failed or canceled GitLab CI job.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
project_path
|
str
|
GitLab project path (e.g. |
required |
job_id
|
int
|
Numeric job ID to retry. |
required |
Returns the new job dict from the API response (includes the
retried job's id, web_url, and status).
Raises:
| Type | Description |
|---|---|
ForgeError
|
On API failure. A 401/403 response raises an
actionable error indicating the token needs the |
Source code in src/agentic_ci/forge/gitlab.py
pipeline_schedules(project_path)
¶
List all pipeline schedules for a project (paginated).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
project_path
|
str
|
GitLab project path (e.g. |
required |
Returns a list of raw schedule dicts from the GitLab API.
Source code in src/agentic_ci/forge/gitlab.py
pipeline_schedule(project_path, schedule_id)
¶
Get details of a specific pipeline schedule.
The returned dict includes last_pipeline with the most recent
pipeline triggered by this schedule.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
project_path
|
str
|
GitLab project path (e.g. |
required |
schedule_id
|
int
|
Numeric pipeline schedule ID. |
required |
Returns the raw schedule dict from the GitLab API.
Source code in src/agentic_ci/forge/gitlab.py
add_mr_block(blocked_mr_url, blocking_mr_url)
¶
Set MR dependency: blocked_mr cannot merge until blocking_mr merges.
Requires GitLab Premium.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
blocked_mr_url
|
str
|
URL of the MR that should be blocked. |
required |
blocking_mr_url
|
str
|
URL of the MR that must merge first. |
required |
Raises ForgeError on failure.
Source code in src/agentic_ci/forge/gitlab.py
mr_diff_position(mr_url)
¶
Get the first changed line position and diff refs from a GitLab MR.
This is a GitLab-specific operation not available on other forges.
Returns {"file", "line", "base_sha", "head_sha", "start_sha"}.
Source code in src/agentic_ci/forge/gitlab.py
paginate(url, params=None)
¶
Fetch all pages of a paginated GitLab API endpoint.
Source code in src/agentic_ci/forge/gitlab.py
CLI¶
cli
¶
CLI subcommands for forge operations.
Registered as the agentic-ci forge subcommand group.
Usage::
agentic-ci forge mr-status <URL>
agentic-ci forge mr-comments <URL>
agentic-ci forge mr-general-comments <URL> [--since ISO]
agentic-ci forge mr-reply <URL> <thread_id> <message>
agentic-ci forge mr-resolve <URL> <thread_id>
agentic-ci forge pipeline-failures <URL>
agentic-ci forge mr-diff-position <URL>
agentic-ci forge label-exists <REPO_URL> <label>
agentic-ci forge label-create <REPO_URL> <label> [--color HEX] [--description TEXT]
agentic-ci forge mr-add-labels <URL> --labels LABEL [LABEL ...]
agentic-ci forge github-token --app-id ID --installation-id ID --private-key PEM
cmd_mr_status(args)
¶
Get MR/PR state, source branch, and pipeline status.
Source code in src/agentic_ci/forge/cli.py
cmd_mr_comments(args)
¶
Get unresolved review comment threads.
Source code in src/agentic_ci/forge/cli.py
cmd_mr_general_comments(args)
¶
Get general (non-diff-positioned) MR/PR comments.
Source code in src/agentic_ci/forge/cli.py
cmd_mr_reply(args)
¶
Reply to a review thread.
cmd_mr_resolve(args)
¶
Resolve a review thread.
cmd_pipeline_failures(args)
¶
Get failed CI job names and logs.
Source code in src/agentic_ci/forge/cli.py
cmd_mr_update(args)
¶
Update MR/PR title and/or description.
Source code in src/agentic_ci/forge/cli.py
cmd_label_exists(args)
¶
Check whether a repository/project label exists.
Source code in src/agentic_ci/forge/cli.py
cmd_label_create(args)
¶
Create a repository/project label.
Source code in src/agentic_ci/forge/cli.py
cmd_mr_add_labels(args)
¶
Attach labels to an existing MR/PR.
Source code in src/agentic_ci/forge/cli.py
cmd_mr_diff_position(args)
¶
Get the first changed line position and diff refs (GitLab only).
Source code in src/agentic_ci/forge/cli.py
cmd_github_token(args)
¶
Generate a short-lived GitHub App installation token.
Source code in src/agentic_ci/forge/cli.py
register_subcommands(forge_parser)
¶
Register forge subcommands on the given parser.
Called from the main agentic-ci CLI to wire up the forge
subcommand group.
Source code in src/agentic_ci/forge/cli.py
138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 | |
Session¶
session
¶
HTTP session and adapter configuration for forge API calls.
Provides auth-injecting adapters for GitLab (PRIVATE-TOKEN) and GitHub (Bearer token), plus a pre-configured session with retry logic.
ForgeAuthError
¶
Bases: RuntimeError
Raised when forge API authentication credentials are missing.
GitLabHTTPAdapter
¶
Bases: HTTPAdapter
Requests adapter that injects PRIVATE-TOKEN for GitLab REST API.
GitHubHTTPAdapter(token, **kwargs)
¶
build_session(*, gitlab_adapter=None, github_token=None)
¶
Build a requests session with forge-specific auth adapters.
The session automatically injects the correct auth headers based
on the request URL prefix (gitlab.com or api.github.com).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
gitlab_adapter
|
GitLabHTTPAdapter | None
|
Custom GitLab HTTP adapter. Defaults to
|
None
|
github_token
|
str | None
|
Token for GitHub API authentication. |
None
|
Source code in src/agentic_ci/forge/session.py
extract_api_error(resp)
¶
Extract a human-readable error message from a forge API error response.
Tries message, then errors[0].message, falling back to
"Unknown error".