gitlab

package
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Jul 21, 2026 License: MIT Imports: 9 Imported by: 0

Documentation

Overview

Package gitlab decodes GitLab webhook payloads into the narrow, normalized shape iterion's inbound handler consumes. It deliberately models only the fields the merge-request review flow needs — never the whole event — so the handler can persist selected fields + a payload hash without retaining the raw body.

Index

Constants

View Source
const (
	AccessGuest      = 10
	AccessReporter   = 20
	AccessDeveloper  = 30
	AccessMaintainer = 40
	AccessOwner      = 50
)

GitLab numeric access levels.

View Source
const EventHeaderIssue = "Issue Hook"

EventHeaderIssue is the value GitLab sends in X-Gitlab-Event for an issue lifecycle event ("Issue Hook"). Unlike GitHub there is no dedicated "labeled" action — labeling an issue arrives as an action:"update" with the added label in changes.labels, so we diff previous→current to detect it.

View Source
const EventHeaderMergeRequest = "Merge Request Hook"

EventHeader is the value GitLab sends in X-Gitlab-Event for an MR.

View Source
const EventHeaderNote = "Note Hook"

EventHeaderNote is the value GitLab sends in X-Gitlab-Event for a comment / discussion reply ("Note Hook"). It is how a user "talks back" to the bot — a reply in the bot's discussion thread or a `/revi` command on the MR.

Variables

This section is empty.

Functions

func AuthorizeReplier

func AuthorizeReplier(ctx context.Context, api API, in ReplierAuth) (ok bool, reason string, err error)

AuthorizeReplier decides whether a note's author may trigger the bot: **(in the explicit allowlist) OR (a project member at >= MinRole)**. An allowlist hit short-circuits the role check (no API call). The role check queries the forge with the bot's token.

func FormatThreadTranscript

func FormatThreadTranscript(notes []DiscussionNote, botUserID int64, maxChars int) string

FormatThreadTranscript renders a discussion's notes as the plain-text transcript the converse bot receives as {{vars.thread_context}}: chronological, system notes skipped, the bot's own notes labelled so the model knows which earlier statements are its own. maxChars caps the result — the FIRST note (the thread anchor, typically the review comment the operator replied to) is always kept, then the most recent notes fill the remaining budget with an omission marker for the middle.

func InAllowlist

func InAllowlist(allow []string, username string, id int64) bool

InAllowlist matches an author by username (case-insensitive, optional @) or numeric id.

func NotesHaveAuthor

func NotesHaveAuthor(notes []DiscussionNote, userID int64) bool

NotesHaveAuthor reports whether any note is authored by userID — the "is this a Revi thread" classification driving the reply-in-thread trigger (a plain reply in a thread the bot is part of is "talking to Revi").

func RoleLevel

func RoleLevel(name string) int

RoleLevel maps a role name to its GitLab access level. Unknown / empty defaults to Developer — the sensible "can talk to the bot" floor.

Types

type API

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

API is a minimal GitLab REST client for the conversational auth checks (loop-guard + role-gate). BaseURL is "https://<host>" (no /api/v4); auth is a PRIVATE-TOKEN header carrying the bot's forge_token.

func (API) CurrentUser

func (a API) CurrentUser(ctx context.Context) (User, error)

CurrentUser returns the account the token authenticates as — used to skip the bot's own notes (loop-guard).

func (API) Discussion

func (a API) Discussion(ctx context.Context, projectID, mrIID int64, discussionID string) ([]DiscussionNote, error)

Discussion fetches the notes of one MR discussion thread, in API order (chronological). An empty discussionID or a 404 returns (nil, nil) — callers treat "no thread" as benign.

func (API) MemberAccessLevel

func (a API) MemberAccessLevel(ctx context.Context, projectID, userID int64) (level int, member bool, err error)

MemberAccessLevel returns the user's effective access level on the project (inherited memberships included) and whether they are a member at all.

type BoolChange added in v0.50.0

type BoolChange struct {
	Previous bool `json:"previous"`
	Current  bool `json:"current"`
}

BoolChange is the {previous,current} pair GitLab uses for a boolean attribute inside `changes`.

type Changes added in v0.50.0

type Changes struct {
	Draft          *BoolChange `json:"draft"`
	WorkInProgress *BoolChange `json:"work_in_progress"` // deprecated alias GitLab still emits
}

Changes is the top-level `changes` object GitLab sends on `update` events. We decode only the draft-transition fields. Pointers so a missing key is distinguishable from a present {false,false}.

type Commit

type Commit struct {
	ID string `json:"id"`
}

type DiscussionNote

type DiscussionNote struct {
	AuthorID       int64
	AuthorUsername string
	Body           string
	System         bool
}

DiscussionNote is one note of an MR discussion thread — just the fields the conversational layer needs: authorship for the reply-in-thread classification (NotesHaveAuthor) and body for the thread transcript the converse bot receives (FormatThreadTranscript).

type IssueAttributes

type IssueAttributes struct {
	IID         int64  `json:"iid"`
	Title       string `json:"title"`
	Description string `json:"description"`
	State       string `json:"state"`  // "opened" | "closed"
	Action      string `json:"action"` // "open" | "update" | "close" | "reopen"
	URL         string `json:"url"`
}

type IssueChanges

type IssueChanges struct {
	Labels *LabelsChange `json:"labels"`
}

IssueChanges carries the before/after diff GitLab includes on an update. Only `labels` is modelled — it is what distinguishes "a label was added" from any other issue edit.

type IssueEvent

type IssueEvent struct {
	ObjectKind       string          `json:"object_kind"`
	EventType        string          `json:"event_type"`
	User             User            `json:"user"`
	Project          Project         `json:"project"`
	ObjectAttributes IssueAttributes `json:"object_attributes"`
	Labels           []Label         `json:"labels"` // current labels after the change
	Changes          IssueChanges    `json:"changes"`
}

IssueEvent is the subset of GitLab's issue webhook we decode: the issue itself plus the labels diff that tells us which label was just added.

type IssueNoteable

type IssueNoteable struct {
	IID         int64  `json:"iid"`
	Title       string `json:"title"`
	Description string `json:"description"`
	URL         string `json:"url"`
	State       string `json:"state"` // "opened" | "closed" — gates command handling
}

IssueNoteable is the issue a note is attached to (present on Issue notes — the surface a human uses to fire a /command that opens an MR back-linking the issue).

type Label

type Label struct {
	Title string `json:"title"`
}

type LabelsChange

type LabelsChange struct {
	Previous []Label `json:"previous"`
	Current  []Label `json:"current"`
}

type MergeRequestEvent

type MergeRequestEvent struct {
	ObjectKind       string           `json:"object_kind"`
	EventType        string           `json:"event_type"`
	User             User             `json:"user"` // the actor that opened/reopened the MR
	Project          Project          `json:"project"`
	ObjectAttributes ObjectAttributes `json:"object_attributes"`
	Labels           []Label          `json:"labels"`
	// Changes records the before/after of attributes touched by this event.
	// GitLab has no dedicated "ready_for_review" action — a draft MR marked
	// ready arrives as an `update` whose Changes.Draft went true→false, so
	// the handler must inspect this to distinguish it from a plain push.
	Changes Changes `json:"changes"`
}

MergeRequestEvent is the subset of GitLab's merge_request webhook we decode.

type MergeRequestNoteable

type MergeRequestNoteable struct {
	IID          int64  `json:"iid"`
	State        string `json:"state"` // "opened" | "closed" | "merged" — gates re-review
	SourceBranch string `json:"source_branch"`
	TargetBranch string `json:"target_branch"`
	Title        string `json:"title"`
	Description  string `json:"description"`
	URL          string `json:"url"`
	LastCommit   Commit `json:"last_commit"`
}

MergeRequestNoteable is the MR a note is attached to (present on MR notes).

type NoteAttributes

type NoteAttributes struct {
	ID           int64  `json:"id"`
	Note         string `json:"note"`
	NoteableType string `json:"noteable_type"` // "MergeRequest" | "Issue" | "Commit" | ...
	DiscussionID string `json:"discussion_id"`
	AuthorID     int64  `json:"author_id"`
	URL          string `json:"url"`
}

type NoteEvent

type NoteEvent struct {
	ObjectKind       string                `json:"object_kind"`
	EventType        string                `json:"event_type"`
	User             User                  `json:"user"`
	Project          Project               `json:"project"`
	ObjectAttributes NoteAttributes        `json:"object_attributes"`
	MergeRequest     *MergeRequestNoteable `json:"merge_request"`
	Issue            *IssueNoteable        `json:"issue"`
}

NoteEvent is the subset of GitLab's note webhook we decode. We model MR notes (the conversational /revi flow) and Issue notes (the /command → open-MR-and-back-link flow); commit/snippet notes are filtered out upstream.

type ObjectAttributes

type ObjectAttributes struct {
	IID          int64  `json:"iid"`
	Action       string `json:"action"`
	SourceBranch string `json:"source_branch"`
	TargetBranch string `json:"target_branch"`
	Title        string `json:"title"`
	Description  string `json:"description"`
	URL          string `json:"url"`
	OldRev       string `json:"oldrev"`
	LastCommit   Commit `json:"last_commit"`
	// Draft is GitLab's work-in-progress flag (14.0+); WorkInProgress is the
	// deprecated alias older GitLab still sends. Either set ⇒ the MR must not
	// auto-launch a bot; the trigger is the update that clears it.
	Draft          bool `json:"draft"`
	WorkInProgress bool `json:"work_in_progress"`
}

type Parsed

type Parsed struct {
	ProjectID      int64
	ProjectPath    string // group/sub/repo
	ProjectWebURL  string
	CloneURL       string
	MRIID          int64
	Action         string // open|reopen|update|...
	SourceBranch   string
	TargetBranch   string
	Title          string
	Description    string
	MRURL          string
	HeadSHA        string
	OldRev         string
	Labels         []string
	SenderUsername string // the actor that opened/reopened the MR (e.g. "renovate")
	// Draft reports whether the MR is currently a work-in-progress draft. A
	// draft MR never auto-triggers a bot (IsReviewable is false).
	Draft bool
	// BecameReady is true when THIS event is the draft→ready transition
	// (GitLab's `changes.draft` went true→false on an `update`). It is the
	// GitLab equivalent of GitHub's `ready_for_review` action — the moment a
	// draft becomes reviewable — since GitLab has no dedicated action for it.
	BecameReady bool
}

Parsed is the normalized merge-request view the handler consumes.

func ParseMergeRequest

func ParseMergeRequest(body []byte) (Parsed, error)

ParseMergeRequest decodes a GitLab merge_request webhook body.

func (Parsed) IsReviewable

func (p Parsed) IsReviewable() bool

IsReviewable reports whether the MR action should AUTO-trigger a review. A DRAFT MR is never auto-reviewable — the author is still iterating, so auto-running a bot wastes budget and churns an unfinished branch. Otherwise a review fires when the MR is created (open), reopened, or marked ready (the `update` that clears draft — GitLab's stand-in for a ready action). Plain pushes to the MR ("update" with a new head, draft unchanged) deliberately do NOT re-trigger — auto-review-on-every-push was found too heavy; re-review after a push is on-demand via the `/revi` note command.

func (Parsed) SubjectID

func (p Parsed) SubjectID() string

SubjectID is the stable per-MR identifier used in delivery records.

type ParsedIssue

type ParsedIssue struct {
	ProjectID      int64
	ProjectPath    string
	CloneURL       string
	DefaultBranch  string // base ref a command's MR is opened against
	IssueIID       int64
	Action         string // "open" | "update" | "close" | "reopen"
	Title          string
	Description    string
	URL            string // the issue's own web URL — the back-link target
	State          string // "opened" | "closed"
	AddedLabels    []string
	AuthorID       int64
	AuthorUsername string
}

ParsedIssue is the normalized issue-lifecycle view the handler consumes: the repo to clone, the issue to implement + back-link, and the labels that were freshly added on this event.

func ParseIssue

func ParseIssue(body []byte) (ParsedIssue, error)

ParseIssue decodes a GitLab issue webhook body and computes the freshly-added labels (current − previous from changes.labels).

func (ParsedIssue) SubjectID

func (p ParsedIssue) SubjectID() string

SubjectID is the stable per-issue id used in delivery records + idempotency.

type ParsedNote

type ParsedNote struct {
	ProjectID    int64
	ProjectPath  string
	CloneURL     string
	MRIID        int64
	SourceBranch string
	TargetBranch string
	MRTitle      string
	MRDesc       string
	MRURL        string
	HeadSHA      string

	// MRState gates command handling: re-review only acts on "opened"
	// MRs (closed/merged notes are filtered, not errors).
	MRState string

	// Issue fields — set when the note is attached to an Issue (the
	// /command-opens-MR surface) instead of an MR. IssueURL is the subject
	// back-linked as source_issue_ref; DefaultBranch (from the project) is the
	// base ref a command's MR is opened against (an issue note carries no
	// source/target branch of its own).
	IssueIID      int64
	IssueTitle    string
	IssueDesc     string
	IssueURL      string
	IssueState    string // "opened" | "closed" — gates command handling
	DefaultBranch string

	NoteID       int64
	NoteBody     string
	DiscussionID string // the thread to reply in
	NoteURL      string

	AuthorID       int64
	AuthorUsername string
}

ParsedNote is the normalized note view the handler consumes — the MR it targets, the discussion thread to reply in, the author to authorize, and the note body.

func ParseNote

func ParseNote(body []byte) (ParsedNote, error)

ParseNote decodes a GitLab note webhook body.

func (ParsedNote) Command

func (p ParsedNote) Command() (cmd, args string)

Command extracts a leading slash-command from the note body, e.g. "/revi please re-review" → ("revi", "please re-review"). Returns ("", "") when the note does not start with a command. Delegates to webhooks.ParseSlashCommand so every comment surface shares one grammar (case-insensitive, tolerant of leading blank / quote-reply lines).

func (ParsedNote) IsIssueNote

func (p ParsedNote) IsIssueNote() bool

IsIssueNote reports whether this note is attached to an Issue (the surface the /command-opens-MR flow handles). An MR note is never an issue note: when both blocks are somehow present the MR identity takes precedence so the conversational path keeps owning MR comments.

func (ParsedNote) IsMergeRequestNote

func (p ParsedNote) IsMergeRequestNote() bool

IsMergeRequestNote reports whether this note is attached to an MR (the noteable the conversational /revi flow handles).

func (ParsedNote) IsReviewCommand

func (p ParsedNote) IsReviewCommand() bool

IsReviewCommand is the `/revi` specialization of Command(): true only for a note on an OPEN merge request whose leading slash-command is `revi` (args tolerated and ignored v1). Built on the generic extractor so the forge-conversations layer and the re-review trigger share one command grammar (quote-reply tolerance included).

func (ParsedNote) SubjectID

func (p ParsedNote) SubjectID() string

SubjectID is the stable per-note identifier used in delivery records + idempotency (one launch per note).

type Project

type Project struct {
	ID                int64  `json:"id"`
	PathWithNamespace string `json:"path_with_namespace"`
	WebURL            string `json:"web_url"`
	GitHTTPURL        string `json:"git_http_url"`
	DefaultBranch     string `json:"default_branch"`
}

type ReplierAuth

type ReplierAuth struct {
	AuthorID       int64
	AuthorUsername string
	ProjectID      int64
	Allowlist      []string // usernames (with/without @) or numeric ids
	MinRole        string   // role name; "" → developer
}

ReplierAuth is the input to the conversational authorization decision.

type User

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

User is the GitLab account that authored the note — the candidate "replier" the handler authorizes (role-gate + allowlist), and whose identity the loop-prevention check compares against the bot.

Jump to

Keyboard shortcuts

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