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
- func AuthorizeReplier(ctx context.Context, api API, in ReplierAuth) (ok bool, reason string, err error)
- func FormatThreadTranscript(notes []DiscussionNote, botUserID int64, maxChars int) string
- func InAllowlist(allow []string, username string, id int64) bool
- func NotesHaveAuthor(notes []DiscussionNote, userID int64) bool
- func RoleLevel(name string) int
- type API
- type BoolChange
- type Changes
- type Commit
- type DiscussionNote
- type IssueAttributes
- type IssueChanges
- type IssueEvent
- type IssueNoteable
- type Label
- type LabelsChange
- type MergeRequestEvent
- type MergeRequestNoteable
- type NoteAttributes
- type NoteEvent
- type ObjectAttributes
- type Parsed
- type ParsedIssue
- type ParsedNote
- type Project
- type ReplierAuth
- type User
Constants ¶
const ( AccessGuest = 10 AccessReporter = 20 AccessDeveloper = 30 AccessMaintainer = 40 AccessOwner = 50 )
GitLab numeric access levels.
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.
const EventHeaderMergeRequest = "Merge Request Hook"
EventHeader is the value GitLab sends in X-Gitlab-Event for an MR.
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 ¶
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").
Types ¶
type API ¶
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 ¶
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.
type BoolChange ¶ added in v0.50.0
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 DiscussionNote ¶
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 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 LabelsChange ¶
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 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 ¶
ParseMergeRequest decodes a GitLab merge_request webhook body.
func (Parsed) IsReviewable ¶
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.
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 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.