gitlab

package
v0.14.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 27, 2026 License: MIT Imports: 23 Imported by: 0

Documentation

Overview

Package gitlab binds GitLab in as a target-system plugin (analogous to spec/13 for Zammad): a REST client (API v4) for the agent actions and webhook processing (token-verified, idempotent). The unit of work is the issue.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Branch

type Branch struct {
	Name    string `json:"name"`
	Default bool   `json:"default"`
	Commit  struct {
		ShortID   string `json:"short_id"`
		CreatedAt string `json:"created_at"`
	} `json:"commit"`
}

Branch is an entry of the branch list. Default marks the project's default branch.

type CheckoutResult

type CheckoutResult struct {
	// Path is the repository ROOT in the sandbox — also for a partial checkout,
	// where the fetched subtree lies underneath it. It is the path that goes
	// into dev agent and into commit ("checkout_path"), because file lists there
	// are relative to the repository root.
	Path string `json:"path"`
	Ref  string `json:"ref,omitempty"`
	// SubPath is the requested subdirectory, LocalPath the place it landed
	// (Path/SubPath). Without a subPath the two are Path.
	SubPath   string `json:"sub_path,omitempty"`
	LocalPath string `json:"local_path,omitempty"`
	Files     int    `json:"files"`
	// Evicted nennt die Arbeitskopien, die dieser Abruf verdrängt hat. Als
	// Feld und nicht nur als Satz im Hinweis: ein Agent hatte nach dreizehn
	// Teil-Abrufen einen 700-MB-Baum stehen, holte fünf kleine Projekte — und
	// sein Baum war weg. Der Hinweistext sagte es, in einem langen Absatz, und
	// ging unter. Eine Liste geht nicht unter.
	Evicted []string `json:"evicted,omitempty"`
	Hint    string   `json:"hint"`
}

CheckoutResult is the answer of the checkout action to the agent: where the code lies and how it goes on working with it.

func Checkout

func Checkout(ctx context.Context, gc *Client, projectID int, ref, subPath, workdir string) (CheckoutResult, error)

Checkout materialises a project's source code in the sandbox: it downloads the repository archive through the API (the brokered token stays in the daemon, it never lands in the file system — unlike with a git clone using a credential remote) and unpacks it under <workdir>/repos/.

subPath narrows it to a subdirectory (a partial checkout for large repos) and lands UNDERNEATH the repository directory, at the place it occupies upstream. Several partial checkouts of the same ref therefore grow into one working tree instead of standing side by side as stumps. Only the fetched subtree is replaced on this route; what was fetched earlier stays. A full checkout replaces everything — the agent always works on the current code.

type Client

type Client struct {
	BaseURL string
	Token   string
	HTTP    *http.Client
}

Client speaks the GitLab REST API (v4) with a (brokered) API token. The token comes from the SecretStore per call — it is never persisted.

func NewClient

func NewClient(baseURL, token string) *Client

func (*Client) ApproveMR

func (c *Client) ApproveMR(ctx context.Context, projectID, mrIID int) error

ApproveMR — POST /projects/{id}/merge_requests/{iid}/approve: a reviewer's formal approval. The QA agent uses it as the green signal to the manager: "feature tested, all green" — the merging itself stays with the human. If approval is not enabled in the project, GitLab reports an error; the confirming comment_mr then suffices.

func (*Client) AssignIssue

func (c *Client) AssignIssue(ctx context.Context, projectID, issueIID int, userIDs []int) error

AssignIssue — PUT /projects/{id}/issues/{iid} with assignee_ids: assigns the issue to a person (for testing a bugfix answer, say).

func (*Client) Comment

func (c *Client) Comment(ctx context.Context, projectID, issueIID int, body string, internal bool) (Note, error)

Comment — POST /projects/{id}/issues/{iid}/notes. internal=true is an internal note (visible only to project members from reporter upwards), internal=false a public comment — visible to external reporters too.

func (*Client) CommentMR

func (c *Client) CommentMR(ctx context.Context, projectID, mrIID int, body string) (Note, error)

CommentMR — POST /projects/{id}/merge_requests/{iid}/notes: the agent's answer in the review dialogue of its MR.

func (*Client) CommitFiles

func (c *Client) CommitFiles(ctx context.Context, projectID int, branch, startBranch, message string, actions []CommitAction) (Commit, error)

CommitFiles — POST /projects/{id}/repository/commits: one commit with all file changes in a single API call. startBranch != "" creates the branch as a copy of it (the agent's push route: the token stays in the daemon, a git remote with credentials never exists).

func (*Client) CreateIssue

func (c *Client) CreateIssue(ctx context.Context, projectID int, title, description, labels string, assigneeID int) (Issue, error)

CreateIssue — POST /projects/{id}/issues: files a new issue (ticket). For the intake of bug reports that do NOT come from GitLab itself (reported by email, say) — the agent turns the report into a traceable ticket. title is mandatory; description (Markdown), labels (comma-separated) and assignee (a user id, 0 = no assignment) are optional.

func (*Client) CreateMergeRequest

func (c *Client) CreateMergeRequest(ctx context.Context, projectID int, sourceBranch, targetBranch, title, description string, assigneeID, reviewerID int) (MergeRequest, error)

CreateMergeRequest — POST /projects/{id}/merge_requests: opens the MR for a pushed feature branch. assigneeID/reviewerID (as a rule the agent's manager) are optional (0 = do not set); the source branch is removed automatically after the merge.

func (*Client) CurrentUser

func (c *Client) CurrentUser(ctx context.Context) (User, error)

CurrentUser — GET /user: the profile of the token holder (the bot user). Needed to tell one's own last comment in an MR thread apart from someone else's review feedback.

func (*Client) DownloadArchive

func (c *Client) DownloadArchive(ctx context.Context, projectID int, ref, subPath string) (io.ReadCloser, error)

DownloadArchive streams the repository archive (tar.gz) — GET /projects/{id}/repository/archive.tar.gz, optionally narrowed to a ref (branch, tag, SHA) and a subdirectory (subPath) — the latter makes large repos manageable through a partial checkout. Deliberately not routed through do(): the body is binary and can be large; the caller closes the reader.

func (*Client) DownloadUpload

func (c *Client) DownloadUpload(ctx context.Context, projectID int, ref string) (filename, contentType string, body io.ReadCloser, err error)

DownloadUpload downloads an upload attached to an issue/MR (a screenshot) in brokered fashion — GET /projects/{id}/uploads/{secret}/{filename}. Like DownloadArchive it goes past the JSON do() (the body is binary), the token stays in the daemon. ref is the reference from the Markdown: the bare path "/uploads/<secret>/<file>", the full web URL or already "<secret>/<file>". Returns: the file name, the content type and the reader (to be closed by the caller).

func (*Client) Escalate

func (c *Client) Escalate(ctx context.Context, projectID, issueIID int, note string) error

Escalate posts an internal note and removes the assignment (assignee_ids empty) so that a human takes the issue over.

func (*Client) FileExists

func (c *Client) FileExists(ctx context.Context, projectID int, filePath, ref string) (bool, error)

FileExists — HEAD /projects/{id}/repository/files/{path}?ref=…: decides whether a commit creates the file (create) or changes it (update) — the commits API demands the right action and refuses the wrong one.

func (*Client) GetCommitDiff

func (c *Client) GetCommitDiff(ctx context.Context, projectID int, sha string) ([]CommitDiff, error)

GetCommitDiff — GET /projects/{id}/repository/commits/{sha}/diff: what a commit actually changes. Individual file diffs are truncated to maxDiffBytesPerFile so that huge commits do not blow up the agent's context.

func (*Client) GetIssue

func (c *Client) GetIssue(ctx context.Context, projectID, issueIID int) (Issue, error)

GetIssue — GET /projects/{id}/issues/{iid}

func (*Client) GetIssueNote

func (c *Client) GetIssueNote(ctx context.Context, projectID, issueIID, noteID int) (Note, error)

GetIssueNote — GET /projects/{id}/issues/{iid}/notes/{note_id}: a single comment in full. The way back for one that the action layer shortened.

func (*Client) GetJobLog

func (c *Client) GetJobLog(ctx context.Context, projectID, jobID int) (string, bool, error)

GetJobLog — GET /projects/{id}/jobs/{job_id}/trace: the log of a CI job. Traces can be huge; reading is capped and the end of the log is returned.

func (*Client) GetMRApprovals

func (c *Client) GetMRApprovals(ctx context.Context, projectID, mrIID int) (MRApprovals, error)

GetMRApprovals — GET /projects/{id}/merge_requests/{iid}/approvals: who has approved. The merge gate reads it to establish that the agent's own approval is on record — merging something one has not oneself accepted is exactly what the gate is meant to prevent.

func (*Client) GetMRNote

func (c *Client) GetMRNote(ctx context.Context, projectID, mrIID, noteID int) (Note, error)

GetMRNote — GET /projects/{id}/merge_requests/{iid}/notes/{note_id}.

func (*Client) GetMergeRequest

func (c *Client) GetMergeRequest(ctx context.Context, projectID, mrIID int) (MergeRequestDetail, error)

GetMergeRequest — GET /projects/{id}/merge_requests/{iid}

func (*Client) GetProject

func (c *Client) GetProject(ctx context.Context, projectID int) (ProjectDetail, error)

GetProject — GET /projects/{id}

func (*Client) ListBranches

func (c *Client) ListBranches(ctx context.Context, projectID int, search string) ([]Branch, error)

ListBranches — GET /projects/{id}/repository/branches, optionally with a name search. With it an agent finds the right ref instead of guessing branch names.

func (*Client) ListCommits

func (c *Client) ListCommits(ctx context.Context, projectID int, ref, path, since string) ([]Commit, error)

ListCommits — GET /projects/{id}/repository/commits: the history of a ref, optionally narrowed to a path (a file/directory) and a start date (since, ISO 8601). With it an agent checks whether there are commits since an issue was created that already fix the reported fault.

func (*Client) ListIssues

func (c *Client) ListIssues(ctx context.Context, projectID int, state, labels, search, milestone string, assigned bool) ([]Issue, error)

ListIssues finds issues — with projectID through GET /projects/{id}/issues, without one (projectID=0) through the global GET /issues with scope=all: all issues the token may see. state is "opened" (the default), "closed" or "all"; labels (comma-separated), search and milestone (the milestone TITLE as it stands in GitLab) narrow it optionally. assigned=true returns only issues assigned to the token's bot user (scope=assigned_to_me) — for agents that, according to their playbook, only work on their own assignment.

func (*Client) ListMRNotes

func (c *Client) ListMRNotes(ctx context.Context, projectID, mrIID, limit, page int) (NotesPage, error)

ListMRNotes — GET /projects/{id}/merge_requests/{iid}/notes: an MR's discussion state including review comments on the diff, windowed like ListNotes.

func (*Client) ListMergeRequests

func (c *Client) ListMergeRequests(ctx context.Context, projectID int, state, search, targetBranch string) ([]MergeRequest, error)

ListMergeRequests — GET /projects/{id}/merge_requests. state is "opened", "merged", "closed" or "all" (default: all); search filters on title and description, targetBranch on the target branch.

func (*Client) ListMyOpenMergeRequests

func (c *Client) ListMyOpenMergeRequests(ctx context.Context) ([]MergeRequest, error)

ListMyOpenMergeRequests — GET /merge_requests?scope=created_by_me&state=opened: the open merge requests the token's bot user opened itself. The cheap pre-check for the review loop (HasWork) — without a project_id, that is across projects, like ListIssues(0, …). The filter runs through the token identity, no username is needed.

func (*Client) ListNotes

func (c *Client) ListNotes(ctx context.Context, projectID, issueIID, limit, page int) (NotesPage, error)

ListNotes — GET /projects/{id}/issues/{iid}/notes: the newest limit comments of an issue (page counts backwards into the history), chronological within the window.

func (*Client) ListPipelineJobs

func (c *Client) ListPipelineJobs(ctx context.Context, projectID, pipelineID int) ([]Job, error)

ListPipelineJobs — GET /projects/{id}/pipelines/{pipeline_id}/jobs: the jobs of a CI run with their status. The entry point into diagnosing a red pipeline.

func (*Client) ListPipelines

func (c *Client) ListPipelines(ctx context.Context, projectID int, ref string) ([]Pipeline, error)

ListPipelines — GET /projects/{id}/pipelines, optionally narrowed to a ref: did the CI on my branch pass, before I hand the MR over for review respectively after reworking it?

func (*Client) ListProjects

func (c *Client) ListProjects(ctx context.Context) ([]Project, error)

ListProjects — GET /projects?membership=true: all projects in which the bot user is a member. The entry point for agents that do not yet know their project_ids.

func (*Client) ListReviewMergeRequests

func (c *Client) ListReviewMergeRequests(ctx context.Context, reviewerUsername string) ([]MergeRequest, error)

ListReviewMergeRequests — GET /merge_requests?scope=all&reviewer_username=<user>&state=opened: the open merge requests in which the bot user is entered as reviewer — the review queue of a QA/test agent, across projects (like ListMyOpenMergeRequests, only from the reviewer's rather than the author's point of view). It carries the review loop from the other side: the developer agent sets the QA agent as reviewer, who finds the MR through this.

scope=all is what makes it work, and its absence is silent: GitLab defaults this endpoint to scope=created_by_me, so without it the query asks for merge requests the bot opened ITSELF and is also reviewer on. A QA agent opens none — the answer is an empty list, HTTP 200, in fifty milliseconds. The `nur-wenn: gitlab:review` heartbeat then reports "no work" every quarter of an hour and the agent sleeps through its review queue, without a single error anywhere.

func (*Client) ListTree

func (c *Client) ListTree(ctx context.Context, projectID int, path, ref string, recursive bool) ([]TreeEntry, error)

ListTree — GET /projects/{id}/repository/tree: leafing through the repository tree without downloading anything. For repos too large for a checkout: navigate first, then read files selectively.

func (*Client) LookupUser

func (c *Client) LookupUser(ctx context.Context, username string) (User, error)

LookupUser — GET /users?username=…: resolves a GitLab username into the numeric user id (the issue API knows only assignee_ids). The API's username filter matches exactly but returns a list.

func (*Client) MergeMR

func (c *Client) MergeMR(ctx context.Context, projectID, mrIID int, sha string, removeSourceBranch bool) (MergeRequestDetail, error)

MergeMR — PUT /projects/{id}/merge_requests/{iid}/merge. sha pins the state to be merged: GitLab merges only if the head is still that commit, so a commit pushed after the review can never be merged unseen. Passing an empty sha is deliberately not supported — the gate in the plugin always has the state it checked.

func (*Client) RawFile added in v0.14.0

func (c *Client) RawFile(ctx context.Context, projectID int, filePath, ref string, max int64) ([]byte, error)

RawFile holt eine Datei roh, mit einem Limit, das der Aufrufer setzt.

Getrennt von ReadFile, weil die beiden verschiedene Fragen beantworten: ReadFile liefert Text für den Agenten und schneidet bei maxReadFileBytes ab (mit "truncated": true, damit niemand mit einer halben Datei weiterarbeitet). RawFile schreibt in den Arbeitsbaum — da ist eine abgeschnittene Datei kein Hinweis, sondern ein Schaden, und deshalb gibt es hier statt eines Abschneidens einen Fehler.

func (*Client) ReadFile

func (c *Client) ReadFile(ctx context.Context, projectID int, filePath, ref string) (content string, truncated bool, err error)

ReadFile — GET /projects/{id}/repository/files/{path}/raw: reading a single file without a checkout. The file path is URL-encoded completely (including "/"), as the GitLab API demands.

func (*Client) ReadFileFrom added in v0.14.0

func (c *Client) ReadFileFrom(ctx context.Context, projectID int, filePath, ref string, offset int) (content string, truncated bool, err error)

ReadFileFrom liest ab einer Stelle. Das Abschneiden bei maxReadFileBytes war sauber gemeldet und trotzdem eine Falle: eine große Datei kam unbrauchbar an, und es gab keinen Weg, den Rest zu holen. Gelesen wird über einen Range-Header; kann die Gegenstelle das nicht (kein 206), wird der Anfang verworfen — dieselbe Auskunft, nur teurer.

func (*Client) RetryPipeline

func (c *Client) RetryPipeline(ctx context.Context, projectID, pipelineID int) (Pipeline, error)

RetryPipeline — POST /projects/{id}/pipelines/{pipeline_id}/retry: starts the failed jobs of a CI run again — for the case that the cause lay outside the code and has been fixed in the meantime (repo access granted afterwards, the runner back again).

func (*Client) SetAutoMerge

func (c *Client) SetAutoMerge(ctx context.Context, projectID, mrIID int, sha string, removeSourceBranch bool) (MergeRequestDetail, error)

SetAutoMerge — same endpoint as MergeMR, but instead of an immediate merge it hands the merge over to GitLab: it completes it itself once the head pipeline (still pinned by sha) turns green, re-checking every other merge condition at that moment. For an MR whose pipeline just has not concluded yet — everything else about it already checks out — so a second heartbeat does not have to come back and ask again.

Both parameters are sent on purpose. `auto_merge` is the current name; `merge_when_pipeline_succeeds` has been deprecated in its favour since GitLab 17.11 but is what older instances understand — and Covey is installed against whatever GitLab an organization runs. The endpoint links the two with an OR (`to_boolean(params[:merge_when_pipeline_succeeds]) || to_boolean(params[:auto_merge])`), and undeclared parameters are dropped rather than rejected, so both ends of that range work with one request. When the deprecated name disappears, this is one line.

func (*Client) SetLabels

func (c *Client) SetLabels(ctx context.Context, projectID, issueIID int, add, remove []string) (Issue, error)

SetLabels — PUT /projects/{id}/issues/{iid} with add_labels/remove_labels: sets and removes labels on an EXISTING issue without touching the others. Exactly this partial operation is what an agent needs that maintains an item's working state on the board ("ready" → "in progress", say): were it to write the full labels list, every state change would delete the subject-matter labels along with it.

GitLab answers with the updated issue — we return that so the agent sees the state reached instead of querying it again.

func (*Client) SetMRLabels

func (c *Client) SetMRLabels(ctx context.Context, projectID, mrIID int, add, remove []string) (MergeRequest, error)

SetMRLabels is SetLabels for a merge request instead of an issue: same additive/subtractive add_labels/remove_labels body, but PUT /projects/{id}/merge_requests/{iid} — GitLab does not accept an issue path for MR labels or vice versa, they are genuinely separate resources.

This exists because the label-driven handoffs several agents' playbooks rely on (needs-arch-review, ready-for-qa, qa-passed/qa-failed, security-veto) all live on merge requests, not issues — set_labels used to hard-require issue_iid and had no way to reach an MR at all. Every one of those calls failed with "project_id or issue_iid missing" whenever an agent passed mr_iid instead, which one agent worked around by inventing a comment-based convention instead of labels (see the org's wiki) rather than recognizing the tool itself was missing this path.

func (*Client) SetMRReviewer

func (c *Client) SetMRReviewer(ctx context.Context, projectID, mrIID int, reviewerIDs []int) (MergeRequestDetail, error)

SetMRReviewer — PUT /projects/{id}/merge_requests/{iid} with reviewer_ids: enters the reviewer(s) of an existing MR. That is how the developer agent hands its MR over to the QA agent deliberately (or hands it back), without the assignment to the manager getting lost.

func (*Client) SetMRState

func (c *Client) SetMRState(ctx context.Context, projectID, mrIID int, stateEvent string) error

SetMRState — same idea as SetState, for a merge request: PUT /projects/{id}/merge_requests/{iid} with state_event ("close"|"reopen"). GitLab has no separate close endpoint for MRs, just this field on the same resource merge/approve already write to.

func (*Client) SetState

func (c *Client) SetState(ctx context.Context, projectID, issueIID int, stateEvent string) error

SetState — PUT /projects/{id}/issues/{iid} with state_event ("close"|"reopen").

func (*Client) UploadFile

func (c *Client) UploadFile(ctx context.Context, projectID int, filename string, data []byte) (UploadResult, error)

UploadFile uploads a file (a screenshot) to a project — POST /projects/{id}/ uploads, multipart. Like DownloadUpload it goes past the JSON do() (the body is multipart), the token stays in the daemon. The returned "markdown" (![alt](/uploads/<secret>/<file>)) is embedded in a comment_mr body.

type Commit

type Commit struct {
	ID         string `json:"id"`
	ShortID    string `json:"short_id"`
	Title      string `json:"title"`
	AuthorName string `json:"author_name"`
	CreatedAt  string `json:"created_at"`
	WebURL     string `json:"web_url"`
}

Commit is an entry of the commit history — enough context to recognise whether a reported bug has been fixed in the meantime (title, author, date).

type CommitAction

type CommitAction struct {
	Action   string `json:"action"` // "create" | "update" | "delete"
	FilePath string `json:"file_path"`
	Content  string `json:"content,omitempty"`
	Encoding string `json:"encoding,omitempty"`
}

CommitAction is an entry in the actions array of the commits API: creating, changing or deleting a file. Contents travel base64-encoded — that way binary files and special characters survive the JSON transport too.

type CommitDiff

type CommitDiff struct {
	OldPath     string `json:"old_path"`
	NewPath     string `json:"new_path"`
	NewFile     bool   `json:"new_file"`
	DeletedFile bool   `json:"deleted_file"`
	Diff        string `json:"diff"`
	Truncated   bool   `json:"truncated,omitempty"`
}

CommitDiff is the diff of a file within a commit.

type CommitResult

type CommitResult struct {
	Branch        string   `json:"branch"`
	BranchCreated bool     `json:"branch_created"`
	Commit        Commit   `json:"commit"`
	Files         []string `json:"files"`
	Deleted       []string `json:"deleted,omitempty"`
	Hint          string   `json:"hint"`
}

CommitResult is the answer of the commit action: what was pushed and how things continue (opening a merge request).

func CommitFromCheckout

func CommitFromCheckout(ctx context.Context, gc *Client, projectID int, branch, startBranch, message, checkoutPath string, files, deleted []string, workdir string) (CommitResult, error)

CommitFromCheckout pushes locally edited files from the sandbox checkout as one commit onto a feature branch — through the commits API, so that the brokered token stays in the daemon (no git remote with credentials in the sandbox). If the branch does not exist yet, it is branched off the start branch (default: the project's default branch). Direct commits onto the default branch are forbidden fail-closed — the route into the main branch leads exclusively through a merge request.

type DownloadUploadResult

type DownloadUploadResult struct {
	Path        string `json:"path"`
	Filename    string `json:"filename"`
	ContentType string `json:"content_type,omitempty"`
	Bytes       int64  `json:"bytes"`
	Hint        string `json:"hint"`
}

DownloadUploadResult is the answer of the download_upload action: where the image lies in the sandbox and how the agent looks at it.

func DownloadUploadToSandbox

func DownloadUploadToSandbox(ctx context.Context, gc *Client, projectID int, ref, workdir string) (DownloadUploadResult, error)

DownloadUploadToSandbox fetches an upload attached to an issue/MR (a screenshot) into the sandbox in brokered fashion — the token stays in the daemon, the file lands under <workdir>/uploads/. The agent then reads it with the Read tool (vision) and can actually look at the screenshot. ref is the reference from the issue description: "/uploads/<secret>/<file>.png", the full web URL or already "<secret>/<file>".

type Issue

type Issue struct {
	ID          int      `json:"id"`
	IID         int      `json:"iid"`
	ProjectID   int      `json:"project_id"`
	Title       string   `json:"title"`
	Description string   `json:"description"`
	State       string   `json:"state"`
	Labels      []string `json:"labels"`
	WebURL      string   `json:"web_url"`
	// UpdatedAt carries the intake gate above its per-issue comment budget: with
	// more open issues than issueMaxNotesChecks the gate cannot afford a
	// ListNotes per issue, and this timestamp — which GitLab already ships in the
	// list response — moves on every comment, label and edit. Without it the
	// overflow signature could only count issues and went blind whenever the
	// count stood still (see issueWorkPending).
	UpdatedAt string `json:"updated_at"`
	// Assignees makes the assignment visible to the agent — playbooks such as
	// "only work on issues assigned to you" need this information.
	Assignees []struct {
		Username string `json:"username"`
	} `json:"assignees"`
	// Milestone attaches the issue to an undertaking (a release, a tender, a
	// sprint). An agent running a whole undertaking needs the title to recognise
	// what belongs to its assignment; GitLab returns null when the issue is
	// attached to no milestone.
	Milestone *struct {
		Title   string `json:"title"`
		DueDate string `json:"due_date"`
		State   string `json:"state"`
	} `json:"milestone"`
	// Author is the reporter: whoever wrote the need down is the natural
	// recipient of the merge request that settles it.
	Author struct {
		Username string `json:"username"`
	} `json:"author"`
	// References.Full is the full reference "group/project#iid" — the project
	// path for the intake filter can be derived from it.
	References struct {
		Full string `json:"full"`
	} `json:"references"`
}

type Job

type Job struct {
	ID           int    `json:"id"`
	Name         string `json:"name"`
	Stage        string `json:"stage"`
	Status       string `json:"status"`
	AllowFailure bool   `json:"allow_failure"`
	WebURL       string `json:"web_url"`
}

Job is a CI job of a pipeline run — enough to find the failed job and pull its log.

type MRApprovals

type MRApprovals struct {
	ApprovalsRequired int  `json:"approvals_required"`
	ApprovalsLeft     int  `json:"approvals_left"`
	UserHasApproved   bool `json:"user_has_approved"`
	ApprovedBy        []struct {
		User struct {
			Username string `json:"username"`
		} `json:"user"`
	} `json:"approved_by"`
}

MRApprovals is the approval state of an MR — GET /merge_requests/{iid}/approvals. approved_by carries the users who have approved; that is how an agent checks whether ITS OWN approval is on record before it merges.

type MergeRequest

type MergeRequest struct {
	IID          int      `json:"iid"`
	ProjectID    int      `json:"project_id"`
	Title        string   `json:"title"`
	State        string   `json:"state"`
	SourceBranch string   `json:"source_branch"`
	TargetBranch string   `json:"target_branch"`
	MergedAt     string   `json:"merged_at"`
	UpdatedAt    string   `json:"updated_at"`
	WebURL       string   `json:"web_url"`
	Labels       []string `json:"labels"`
	Author       struct {
		Username string `json:"username"`
	} `json:"author"`
	// References.Full is the full reference "group/project!iid" — the project
	// path for the intake filter can be derived from it (as with Issue).
	References struct {
		Full string `json:"full"`
	} `json:"references"`
}

MergeRequest is an entry of the MR list — enough to find open or merged fixes on a topic.

type MergeRequestDetail

type MergeRequestDetail struct {
	IID          int    `json:"iid"`
	Title        string `json:"title"`
	Description  string `json:"description"`
	State        string `json:"state"`
	SourceBranch string `json:"source_branch"`
	TargetBranch string `json:"target_branch"`
	MergedAt     string `json:"merged_at"`
	WebURL       string `json:"web_url"`
	HasConflicts bool   `json:"has_conflicts"`
	// DetailedMergeStatus, e.g. "mergeable", "ci_still_running", "conflict" —
	// GitLab's own summary of why an MR is (not) mergeable.
	DetailedMergeStatus string `json:"detailed_merge_status"`
	// BlockingDiscussionsResolved: are all threads that block the merge
	// resolved? False means an open review discussion — the QA agent must not
	// merge over that.
	BlockingDiscussionsResolved bool `json:"blocking_discussions_resolved"`
	// SHA is the head of the diff at read time. Passed back on the merge, it is
	// the guarantee that exactly the reviewed state gets merged: if a commit has
	// arrived in the meantime, GitLab refuses with 409.
	SHA    string `json:"sha"`
	Author struct {
		Username string `json:"username"`
	} `json:"author"`
	Reviewers []struct {
		Username string `json:"username"`
	} `json:"reviewers"`
	HeadPipeline *Pipeline `json:"head_pipeline"`
	// MergeWhenPipelineSucceeds: GitLab's own auto-merge — set, the merge
	// completes by itself once the head pipeline turns green (and every other
	// merge condition still holds at that moment; GitLab re-checks them then,
	// not just now).
	//
	// The field keeps the old name on the RESPONSE side even where the request
	// parameter is already called auto_merge (checked against 19.2-ee, which
	// still reports merge_when_pipeline_succeeds) — hence no rename here.
	MergeWhenPipelineSucceeds bool `json:"merge_when_pipeline_succeeds"`
}

MergeRequestDetail is the full view of a single MR — including the review state (merge status, conflicts) and the CI result (head_pipeline), so that an agent can look after its own MR like a developer.

type Note

type Note struct {
	ID       int    `json:"id"`
	Body     string `json:"body"`
	Internal bool   `json:"internal"`
	System   bool   `json:"system"`
	Author   struct {
		Username string `json:"username"`
	} `json:"author"`
	CreatedAt string `json:"created_at"`
	// BodyTruncated/BodyChars are not filled by GitLab but by the action layer
	// when it shortens an over-long comment for the agent (see cutBody). omitempty,
	// so they only appear where they were actually set.
	BodyTruncated bool `json:"body_truncated,omitempty"`
	BodyChars     int  `json:"body_chars,omitempty"`
}

type NotesPage

type NotesPage struct {
	Notes   []Note
	Page    int
	Total   int  // from X-Total; -1 when GitLab did not state it
	HasMore bool // is there anything older behind this window?
}

NotesPage is ONE window of a comment thread plus what GitLab said about the whole of it. It exists because a ticket's history grows without bound: an issue that takes a daily report for a year carries hundreds of comments, and whoever loads them all pushes them into the agent's context on every call.

The window sits at the NEW end — that is where the current state of a thread is. Within the window Notes runs chronologically ascending, exactly as before, so that everything reading the thread from behind (threadSig) stays valid.

type Pipeline

type Pipeline struct {
	ID        int    `json:"id"`
	Status    string `json:"status"`
	Ref       string `json:"ref"`
	SHA       string `json:"sha"`
	WebURL    string `json:"web_url"`
	UpdatedAt string `json:"updated_at"`
}

Pipeline is the CI run of a ref/MR — status "success", "failed", "running" etc.

type Project

type Project struct {
	ID                int    `json:"id"`
	PathWithNamespace string `json:"path_with_namespace"`
	Description       string `json:"description"`
	WebURL            string `json:"web_url"`
}

type ProjectDetail

type ProjectDetail struct {
	ID                int    `json:"id"`
	PathWithNamespace string `json:"path_with_namespace"`
	DefaultBranch     string `json:"default_branch"`
	WebURL            string `json:"web_url"`
}

ProjectDetail returns the project metadata the developer workflow needs — above all the default branch as the basis for feature branches and as the target of merge requests.

type System

type System struct{}

System binds GitLab in as a target-system plugin to the target registry: the webhook entry (token check, idempotency, correlation), the agent actions and the action documentation for the system prompt.

func (System) ActionSubject

func (System) ActionSubject(action string, params json.RawMessage) string

ActionSubject: public comments (internal=false) are a guard-rail subject of their own that can be ruled more sharply — analogous to zammad:reply_external.

func (System) Execute

func (System) Execute(ctx context.Context, action string, params json.RawMessage, cred target.Credential) (any, error)

func (System) HasWork

func (System) HasWork(ctx context.Context, cred target.Credential) (bool, error)

HasWork (target.WorkChecker): the control plane's cheap pre-check for nur-wenn: heartbeats. Without a webhook GitLab takes up work purely by polling; this check saves the (expensive) agent wake-up when there is nothing to do at the moment. Work is present when ONE of the following holds:

  • There is an open issue in the intake scope (the global GET /issues, followed by the COVEY_GITLAB_INTAKE_PROJECTS filter) — what the agent would not see does not wake it either — on which the bot has not yet answered last.
  • The bot has an open merge request it opened itself with unanswered review feedback (the last non-system comment comes from someone other than the bot). That carries the review loop without a webhook.

The completion of a merge needs no branch of its own: if the associated issue is still open, it wakes through the issue branch; if it was closed automatically on the merge, there is nothing left to do. What counts everywhere is the **edge** (has something happened since the bot's last move?), not the level (is anything open anywhere?) — otherwise the same unfinished item wakes the agent afresh in every interval.

func (System) HasWorkKind

func (System) HasWorkKind(ctx context.Context, cred target.Credential, kind string) (bool, error)

HasWorkKind (target.KindWorkChecker) gates a single kind of work so that several heartbeats (nur-wenn: gitlab:issues, :mr, :review) fire separately:

  • "issues"/"issue" → is ANY open issue in the intake scope waiting for a reaction (for agents that triage all open issues)?
  • "issues:assigned"/"assigned" → is an open issue waiting that is ASSIGNED to the bot user itself (scope=assigned_to_me)? Exactly that is what an agent needs whose playbook only works on its own issues (list_issues assigned=true) — otherwise every open issue of someone else's in the scope wakes it. "Waiting" means in both cases: the bot has not yet commented there last (see issueWorkPending).
  • "mr"/"mrs" → is one of the MRs the bot opened ITSELF waiting for an answer (the author's view, the developer review loop)?
  • "review"/"reviews" → is one of the MRs in which the bot is entered as REVIEWER waiting for its review (the QA/test view)?
  • otherwise → both of HasWork, fail-open on an unknown scope.

func (System) HasWorkSigned

func (System) HasWorkSigned(ctx context.Context, cred target.Credential, kind string) (bool, string, error)

HasWorkSigned (target.SignedWorkChecker) is the actual check: besides the yes/no it returns the signature of the waiting items so that the control plane does not wake twice on the same state. An agent may thereby end a run silently — the QA colleague's feedback was an approval, there is nothing to do — without being woken again in the next interval. If, by contrast, a new contribution or a push comes along, the signature changes and the agent wakes up. Whether a piece of feedback means work (reported defects) or only information (an approval) is thus decided by the agent and not by the gate.

func (System) Name

func (System) Name() string

func (System) Probe

func (System) Probe(ctx context.Context, cred target.Credential) (string, error)

Probe reads the bot user behind the token. `/user` is GitLab's cheapest read, it changes nothing, and its failure modes are exactly the ones worth naming: wrong instance URL, expired token, token without the api scope.

The name comes back as `@username`, because that is how the agent will appear under every issue comment it writes — whoever sees it here recognises it there.

func (System) PromptDoc

func (System) PromptDoc() string

The prompt doc in four blocks. Split by SCOPE, not rewritten: an agent reads the same wording it always did, only the parts its ACCESS.md does not cover fall away. That matters because the doc sits in the context of every turn — the reviewer block alone is around 900 tokens, and a developer agent without the merge scope carried it along on every one of its turns without ever being able to act on it.

The boundaries follow the granted permissions: writing developer actions and the developer playbook need write, the QA/reviewer playbook needs merge. The action catalogue and the rules for bug reports apply to everyone.

func (System) PromptDocForScopes

func (System) PromptDocForScopes(scopes []string) string

PromptDocForScopes (target.ScopedDocSystem) narrows the doc to the scopes granted in ACCESS.md. Fail-open: without scopes the full doc stands — a missing entry must not silently take capabilities away from an agent.

func (System) WritesWorkSignature

func (System) WritesWorkSignature(subject string) bool

WritesWorkSignature (target.SignatureWriter) answers whether an executed action of this system can have changed the work signature — see the interface for what the control plane concludes from a "no".

type TreeEntry

type TreeEntry struct {
	Name string `json:"name"`
	Type string `json:"type"` // "blob" (file) | "tree" (directory)
	Path string `json:"path"`
}

TreeEntry is an entry of the repository tree (a file or a directory).

type UploadResult

type UploadResult struct {
	Alt      string `json:"alt"`
	URL      string `json:"url"`
	FullPath string `json:"full_path"`
	Markdown string `json:"markdown"`
}

UploadResult is the answer of POST /projects/{id}/uploads: the Markdown reference one can embed in a comment plus the relative URL.

type UploadResultOut

type UploadResultOut struct {
	Markdown string `json:"markdown"`
	URL      string `json:"url"`
	Filename string `json:"filename"`
	Bytes    int    `json:"bytes"`
	Hint     string `json:"hint"`
}

UploadResultOut is the answer of the upload action: the Markdown reference for embedding in a comment_mr body plus the relative URL.

func UploadFromSandbox

func UploadFromSandbox(ctx context.Context, gc *Client, projectID int, path, workdir string) (UploadResultOut, error)

UploadFromSandbox uploads a file from the sandbox (e.g. a browser screenshot) in brokered fashion to a GitLab project and returns the Markdown reference the agent embeds in comment_mr. The path is resolved safely against the working directory — no escape via ".." or an absolute path.

type User

type User struct {
	ID       int    `json:"id"`
	Username string `json:"username"`
	Name     string `json:"name"`
	State    string `json:"state"`
}

User is the minimal profile of a GitLab user for the assignment.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL