gitlab

package
v0.1.5 Latest Latest
Warning

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

Go to latest
Published: Aug 5, 2026 License: MIT Imports: 18 Imported by: 0

Documentation

Overview

Package gitlab implements provider.Provider for GitLab.com and self-hosted GitLab instances. See ADR-0020 for the implementation choices (hand-rolled HTTP client, bare-token webhook verification, etc.).

Index

Constants

This section is empty.

Variables

View Source
var ErrGlabNotConfigured = errors.New("glab: no token configured for host")

ErrGlabNotConfigured signals that the glab config file is missing the requested host, or doesn't exist at all. Caller can fall through to an env-var PAT.

Functions

func LoadGlabToken

func LoadGlabToken(host string) (token string, err error)

LoadGlabToken reads the OAuth token from the `glab` CLI's config file — the same token `glab auth status` reports. Returns it as a Bearer token (AuthMode=AuthBearer) so callers can build a Provider that piggybacks on the user's interactive `glab auth login`. ErrGlabNotConfigured if the file or the gitlab.com section is missing.

Useful for spike / personal-laptop deployments where the user has already done `glab auth login` and doesn't want to mint a separate PAT. Production deployments should still use a service-account PAT via env.

func RefreshGlabToken

func RefreshGlabToken(host string) (token string, err error)

RefreshGlabToken forces `glab` to run its own internal access-token refresh (via the refresh token it manages, stored alongside the access token in its config file) before reading whatever token ends up on disk via LoadGlabToken.

glab's OAuth access token is short-lived and only refreshed lazily, when something actually invokes glab — reading the config file directly (LoadGlabToken alone) can return a genuinely expired access token if nothing has triggered glab's own refresh recently, even though `glab auth status` would report a healthy login the moment it's run (ADR-0065). `glab api user` is used as the poke: a real authenticated API call, confirmed (by direct testing) to trigger glab's refresh-if-needed logic before it succeeds.

The poke is best-effort: if it fails (glab not on PATH, a genuine re-login requirement, a transient network blip), this doesn't return early — LoadGlabToken still runs and its result (or lack of one) is what the caller ultimately sees, same as if RefreshGlabToken hadn't poked at all. A failed poke isn't proof the token is unusable; a successful poke doesn't guarantee LoadGlabToken succeeds either (e.g. the host section could still be missing) — this only maximises the chance the token on disk is current when read.

Types

type AuthMode

type AuthMode int

AuthMode picks the HTTP header GitLab uses to authenticate. PATs go in PRIVATE-TOKEN; OAuth tokens (e.g. from `glab` config) go in Authorization: Bearer.

const (
	AuthPAT    AuthMode = 0 // PRIVATE-TOKEN: <pat>      (default; backwards-compatible)
	AuthBearer AuthMode = 1 // Authorization: Bearer <oauth-token>
)

type Config

type Config struct {
	BaseURL string // defaults to https://gitlab.com
	// Token is a static personal/project/group access token. Required
	// unless TokenSource is set. PATs don't expire on their own the way an
	// OAuth access token does, so a static string is fine here.
	Token string
	// TokenSource, if set, takes precedence over Token and is called to
	// resolve the bearer/PAT value fresh on every request instead of
	// caching one at construction time. Use this for tokens that can go
	// stale behind the Provider's back — e.g. `glab auth login`'s OAuth
	// access token, which `glab` itself transparently refreshes in its own
	// config file (see LoadGlabToken and ADR-0063): a Provider built with a
	// one-time Token snapshot would keep using an expired access token
	// forever, since it has no way to notice `glab` refreshed a new one.
	TokenSource func() (string, error)
	AuthMode    AuthMode      // defaults to AuthPAT (the v1 behaviour)
	Timeout     time.Duration // defaults to 30s per request
}

Config wires a Provider.

type Provider

type Provider struct {
	// contains filtered or unexported fields
}

Provider is the GitLab implementation of provider.Provider.

func New

func New(cfg Config) (*Provider, error)

New constructs a Provider. Returns an error if neither Token nor TokenSource is set so callers fail fast at daemon start rather than discovering at first API call.

func (*Provider) AuthenticatedUser

func (p *Provider) AuthenticatedUser(ctx context.Context) (provider.User, error)

AuthenticatedUser → GET /api/v4/user.

func (*Provider) CloseMR

func (p *Provider) CloseMR(ctx context.Context, projectID string, mrIID int) error

CloseMR → PUT /api/v4/projects/:id/merge_requests/:iid with state_event=close.

func (*Provider) CreateMR

func (p *Provider) CreateMR(ctx context.Context, projectID string, draft provider.MRDraft) (provider.MR, error)

CreateMR → POST /api/v4/projects/:id/merge_requests. GitLab signals Draft MRs via a "Draft: " title prefix (the modern replacement for "WIP:"); we add it here when the caller asks for one.

func (*Provider) DeregisterWebhook

func (p *Provider) DeregisterWebhook(ctx context.Context, projectID, webhookID string) error

DeregisterWebhook → DELETE /api/v4/projects/:id/hooks/:hook_id. Idempotent (404 treated as success — already gone).

func (*Provider) GetMRState

func (p *Provider) GetMRState(ctx context.Context, projectID string, mrIID int) (provider.MRState, error)

GetMRState → GET /api/v4/projects/:id/merge_requests/:iid. Returns the MR's current `state` field ("opened" | "closed" | "merged" | "locked") plus `has_conflicts`, used by the poller to detect lifecycle transitions and merge conflicts from the same request.

func (*Provider) IsBot

func (p *Provider) IsBot(u provider.User) bool

IsBot inspects the user.bot field set by GitLab on bot accounts. Some long-lived integrations (Danger, sonar) use regular accounts; the caller can layer name-pattern matching on top if needed.

func (*Provider) ListNotesSince

func (p *Provider) ListNotesSince(ctx context.Context, projectID string, mrIID int, since provider.NoteCursor) ([]provider.NotePoll, error)

ListNotesSince → GET /api/v4/projects/:id/merge_requests/:iid/discussions. Returns notes whose `id` exceeds the watermark (i.e. arrived since the last poll). The poller stores the highest id seen on AgentState.

This sources from /discussions rather than the flat /notes endpoint: GitLab's /notes response only populates discussion_id on plain top-level notes — inline diff comments (DiffNote) come back with an empty discussion_id there. /discussions nests every note (diff or plain) under its owning discussion object, whose id is authoritative for both, so a reply to an inline review comment threads instead of always falling back to a new top-level comment.

func (*Provider) Name

func (p *Provider) Name() string

func (*Provider) NormaliseEvent

func (p *Provider) NormaliseEvent(headers http.Header, body []byte) (provider.Event, error)

NormaliseEvent decodes a GitLab webhook POST into provider.Event. Routes by the X-Gitlab-Event header; returns provider.ErrIgnore for event kinds we didn't subscribe to (a project hook fires for everything the project enables, not just our subscription).

func (*Provider) PostComment

func (p *Provider) PostComment(ctx context.Context, projectID string, mrIID int, body string) error

PostComment → POST /api/v4/projects/:id/merge_requests/:iid/notes.

func (*Provider) ReactToNote

func (p *Provider) ReactToNote(ctx context.Context, projectID string, mrIID int, noteID int64, _, emoji string) error

ReactToNote → POST /api/v4/projects/:id/merge_requests/:iid/notes/:note_id/award_emoji. GitLab has a single notes endpoint (see streamNote), so stream is unused — kept in the signature for parity with GitHub, which needs it to pick an endpoint. See ADR-0050.

func (*Provider) RegisterWebhook

func (p *Provider) RegisterWebhook(ctx context.Context, projectID, callbackURL, secret string, events []provider.EventKind) (string, error)

RegisterWebhook → POST /api/v4/projects/:id/hooks. Event flags map onto GitLab's webhook event toggles. Idempotency is the caller's job (workflow state tracks WebhookID); GitLab will happily create duplicate hooks.

func (*Provider) ReplyToDiscussion

func (p *Provider) ReplyToDiscussion(ctx context.Context, projectID string, mrIID int, discussionID string, body string) error

ReplyToDiscussion → POST /api/v4/projects/:id/merge_requests/:iid/discussions/:discussion_id/notes. Posts within the existing thread rather than as a new top-level note, so the reply shows up nested under the comment it addresses.

func (*Provider) ResolveDiscussion

func (p *Provider) ResolveDiscussion(ctx context.Context, projectID string, mrIID int, discussionID string) error

ResolveDiscussion → PUT /api/v4/projects/:id/merge_requests/:iid/discussions/:discussion_id?resolved=true. Marks the thread as resolved (collapsed in the UI) — called after the agent successfully pushes a change addressing a reviewer comment. Empty discussionID is a no-op so callers don't need to guard.

func (*Provider) RetryPipelineJob

func (p *Provider) RetryPipelineJob(ctx context.Context, projectID string, jobID int64) error

RetryPipelineJob → POST /api/v4/projects/:id/jobs/:job_id/retry. Used by the deterministic CI-flake-retry path; the agent isn't involved.

func (*Provider) UpdateMRDescription

func (p *Provider) UpdateMRDescription(ctx context.Context, projectID string, mrIID int, description string) error

UpdateMRDescription → PUT /api/v4/projects/:id/merge_requests/:iid.

func (*Provider) UpdateMRTitle

func (p *Provider) UpdateMRTitle(ctx context.Context, projectID string, mrIID int, title string) error

UpdateMRTitle → PUT /api/v4/projects/:id/merge_requests/:iid.

func (*Provider) VerifySignature

func (p *Provider) VerifySignature(headers http.Header, _ []byte, secret string) bool

VerifySignature checks the X-Gitlab-Token header against the registered secret using constant-time comparison. GitLab does *not* use HMAC — the header is the bare token. The token comparison is the only auth we have for inbound webhooks, so do it carefully. See ADR-0020.

Jump to

Keyboard shortcuts

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