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 ¶
ClampPrefixes bounds a provider's declared quote prefixes, dropping empty entries (which would match every line) and over-long ones.
func DefangWith ¶
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 GreenRun ¶ added in v0.4.3
GreenRun names a green CI run the provider has on record: the provider's own reference for the run (a URL or id a person can open) and the head commit it verified.
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 ¶
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.
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) Annotate ¶
func (Nop) Annotate(Annotation) error
func (Nop) StartGroup ¶
type RunSource ¶ added in v0.4.3
RunSource is the optional provider capability behind CI verdict inheritance: the newest green run of this same pipeline for the current branch or pull request. A provider that cannot answer - not a PR context, no API access, no green run yet - reports ok=false, and the caller proceeds as if the capability did not exist.