annotate

package
v0.4.2 Latest Latest
Warning

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

Go to latest
Published: Sep 3, 2026 License: GPL-3.0 Imports: 3 Imported by: 0

Documentation

Overview

Package annotate emits CI job-log structure: the fold markers and the warning/error notices a CI provider recognizes.

The shape mirrors [cache.RemoteBackend]: magus core names no provider. It holds an Annotator, asks whether it is Active, and calls generic verbs. Provider syntax lives behind an implementation, so supporting a new one is a new implementation and no change to any call site.

The vocabulary is the UNION of what real providers need, not the intersection, which is nearly empty:

  • Groups carry an ID separate from their title, because GitLab sections and TeamCity blocks are keyed by a machine name.
  • Groups state whether they want to be collapsed; providers honor what they can.
  • EndGroup may legitimately do nothing: Buildkite has no end marker.
  • Annotations carry a source location and a diagnostic code, so magus's own MGSxxxx codes survive.

A provider that cannot express something ignores it. Unsupported is the normal case, not an error: CodeBuild and CircleCI have no markers at all.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ClampPrefixes

func ClampPrefixes(in []string) []string

ClampPrefixes bounds a provider's declared quote prefixes, dropping empty entries (which would match every line) and over-long ones.

func DefangWith

func DefangWith(text string, prefixes []string) string

DefangWith neutralizes any line in text that begins with one of the given command prefixes, by dropping the prefix's first character so the provider no longer recognizes the line as a command.

Providers hand over their prefixes once rather than being asked per line: this runs over every replayed line of a failing build, the one path where crossing into a spell's VM per call would cost more than the feature is worth.

Dropping the first character rather than inserting one keeps the result plain ASCII and legible - "::error::x" becomes ":error::x". Leading whitespace is preserved.

The drop repeats until no prefix matches. A single pass would UPGRADE a nested prefix into the command it was not: ":::error::x" is inert to the runner, and one drop makes it "::error::x", which the runner executes.

func RegisterOpener

func RegisterOpener(fn func() Annotator)

RegisterOpener installs the hook that supplies a spell-backed Annotator. The bindings layer registers it at init, so core selects a provider without linking the Buzz VM - the same indirection [cache.RegisterRemoteBackendOpener] uses for remote cache backends.

Types

type Annotation

type Annotation struct {
	Level   Level
	Message string
	// Title is a short headline shown above Message where supported.
	Title string
	// Code is a diagnostic identifier (magus emits MGSxxxx). Azure carries
	// it natively; others fold it into the title.
	Code string
	// File is workspace-relative. Line/EndLine/Col/EndCol are 1-based and
	// zero means unset, so a file-level annotation needs no sentinel.
	File                       string
	Line, EndLine, Col, EndCol int
}

Annotation is a message surfaced outside the job log, typically on a pull request. Every field beyond Message is optional; a provider uses what it supports.

SECURITY: Message and Title are UNTRUSTED - they carry a failing process's output, so their content is whatever some test, compiler or transitive dependency chose to print. A provider must never interpolate them where their content becomes syntax (a shell command, a URL), or a dependency that printed a payload would be executing it on every CI machine. Sanitize is a floor, not a license to be careless.

func Sanitize

func Sanitize(a Annotation) Annotation

Sanitize returns a copy of a bounded to the limits above and stripped of control characters.

Control characters are the sharp edge: the message comes from a failing process, so it can carry escape sequences that would re-take the terminal or the job log when a provider echoes it. Tab is kept (ordinary in compiler output); newline is kept because providers encode it themselves.

Truncation is marked, so a reader can tell a bounded message from a process that printed that much.

type Annotator

type Annotator interface {
	// Active reports whether this provider is running the job. False makes
	// every other method a no-op, so callers do not branch.
	Active() bool
	// StartGroup opens a foldable section; EndGroup closes the one with
	// the given ID. Providers that auto-close on the next group treat
	// EndGroup as a no-op.
	StartGroup(g Group) error
	EndGroup(id string) error
	// Annotate raises a message outside the job log.
	Annotate(a Annotation) error
	// Defang returns text safe to replay into the job log, neutralizing any
	// provider command syntax it contains.
	//
	// Not cosmetic: magus replays captured subprocess output, so a test
	// printing "::error::" or a GitLab section marker would be interpreted
	// by the runner, forging annotations or closing a section magus opened.
	//
	// Providers supply their command prefixes once rather than being
	// consulted per line (see DefangWith), which is what keeps this
	// affordable over every replayed line of a failing build.
	Defang(text string) string
}

Annotator writes one provider's job-log structure.

Implementations are constructed around their destination and are expected to be cheap: core may call Active per build step. Nothing here assumes the output is a stream - Buildkite raises annotations by invoking its agent binary, so an implementation may shell out.

func Detect

func Detect() Annotator

Detect returns the Annotator for the provider running this job, or Nop when none is active.

Every provider is a spell: magus ships no CI syntax of its own, so a workspace opts in by naming one (magus.ci.provider), and adding support for a new system is a spell someone writes rather than a change to magus. A spell that reports itself inactive - the github spell outside Actions - yields Nop, so an unconditional wiring costs nothing elsewhere.

Detect hands a provider no destination: a provider spell emits markers with std\print to the real stdout, because a workflow command is only a command if the runner reads it there. magus captures and replays subprocess output, so a marker sent through a writer magus holds would land in the stream Annotator.Defang scrubs.

type Group

type Group struct {
	// ID is a stable, opaque identifier for the section. Providers that key
	// sections by name (GitLab, TeamCity) match a close against its open by
	// this; providers that address sections only by title ignore it.
	//
	// magus passes something meaningful and readable, such as a project
	// path. It does NOT normalize the character set, because what is legal
	// differs per provider and encoding one provider's rule here would put
	// that provider back into the generic layer. A provider with charset
	// restrictions normalizes in its own spell.
	ID string
	// Title is the human-readable heading.
	Title string
	// Collapsed asks for the section to start folded. It is a request, not
	// a guarantee: GitHub always folds regardless, and providers with no
	// notion of folding ignore it. Callers should set it false for output
	// the reader needs to see without clicking - a failure, above all.
	Collapsed bool
}

Group describes a foldable section of the job log.

func SanitizeGroup

func SanitizeGroup(g Group) Group

SanitizeGroup bounds a group's fields the way Sanitize bounds an annotation's. A group's title embeds a project and target name, which come from a magusfile rather than from process output, so this is a weaker threat than an annotation - but the same boundary applies, and a title is echoed into the job log all the same.

type Level

type Level int

Level is an annotation's severity. Providers spell these differently and some support only a subset; an implementation maps what it can and drops the rest.

const (
	LevelNotice Level = iota
	LevelWarning
	LevelError
)

func (Level) String

func (l Level) String() string

String returns the lowercase level name, which is the token most provider syntaxes embed directly.

type Nop

type Nop struct{}

Nop is the Annotator used when no provider is detected. Every method succeeds and does nothing, so core needs no nil checks.

func (Nop) Active

func (Nop) Active() bool

func (Nop) Annotate

func (Nop) Annotate(Annotation) error

func (Nop) Defang

func (Nop) Defang(text string) string

func (Nop) EndGroup

func (Nop) EndGroup(string) error

func (Nop) StartGroup

func (Nop) StartGroup(Group) error

Jump to

Keyboard shortcuts

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