Documentation
¶
Overview ¶
Package apply executes a plan.Plan. It is the only code in labelsync that writes to GitHub.
The mode decides whether deleting is allowed at all ¶
Apply creates missing configured labels, updates existing ones, and recolours displaced squatters. Under plan.ModeAppend that is all it does: a delete action is refused before the first write rather than executed. Under plan.ModePrune deletes execute, and the plan handed over is expected to carry only the candidates a user chose — the selection happens in the command, ahead of this package, because a prompt is not something the executor should own.
The order is a crash-consistency guarantee ¶
Actions go out in exactly the order the planner emitted them: renames, then squatter recolours, then creates, then updates, then deletes. That order is what makes every intermediate state coherent, so a run killed halfway through a repository leaves it consistent rather than with a configured label sharing a colour with a squatter that was supposed to have moved off it. Deletes are last for the same reason turned up a notch: they are the one step nothing recovers from, so every recoverable action for the repository has already been attempted by the time one goes out. Nothing here reorders for throughput, and nothing here writes to two repositories at once: the token bucket paces writes at roughly one a second anyway, so parallelism would buy no wall-clock time and would cost the guarantee.
A failed repository is not a failed run ¶
A repository that becomes unreachable partway through is abandoned and the run continues with the next one. The failure is already recorded in github.Failures by the time it is returned, so the end-of-run summary names it and the exit code carries [exit.Skipped]. Every other error — a cancelled context, a rate-limit wait past --max-wait — ends the run, because it is not about one repository.
Index ¶
Constants ¶
const ReportKind = "applied"
ReportKind is the "kind" of the record Report marshals to.
Like the planner's summary and repository kinds it is not an action kind and is never sent to the API. It shares the "kind" key so a consumer reading the NDJSON stream has one discriminator: `jq 'select(.kind == "applied")'` picks out what the run actually did, which on a partially failed run is not the same thing as what it planned. The string is a wire contract — added to, never renamed.
Variables ¶
This section is empty.
Functions ¶
Types ¶
type Report ¶
type Report struct {
Kind string `json:"kind"` // always ReportKind
// Repositories is how many repositories were written to or confirmed clean —
// every one the run reached, including the ones that needed nothing.
Repositories int `json:"repositories"`
Created int `json:"created"`
Updated int `json:"updated"`
Deleted int `json:"deleted"`
Unchanged int `json:"unchanged"`
// Abandoned names the repositories a failure cut short, in the order they
// were reached. Being here does not say whether anything was written before
// the failure, which is the honest answer: the write that failed is the only
// one whose outcome is known.
Abandoned []string `json:"abandoned,omitempty"`
}
Report is what a run of Apply did, as opposed to what its plan proposed. The two differ exactly when a repository failed partway.
func Apply ¶
Apply executes p under mode and reports what it did.
Deletes are refused unless the mode asked for them ¶
Append mode never deletes, and the guard is here rather than only in the planner because this is the package holding the destructive call. A plan carrying a delete under plan.ModeAppend — read back from a file, or computed under prune and applied by a caller that forgot which mode it was in — is refused before the **first** write of the run rather than partway through, so a refused apply has changed nothing.
Under plan.ModePrune the deletes in p execute. p is expected to hold only the candidates the user accepted: narrowing it is plan.RetainDeletes' job and the command's decision, and nothing here can tell a candidate that was chosen from one that was never offered.
type Writer ¶
type Writer interface {
CreateLabel(ctx context.Context, owner, repo string, label github.Label) error
PatchLabel(ctx context.Context, owner, repo, current string, patch github.LabelPatch) error
// DeleteLabel is reached only under [plan.ModePrune]. It is on the interface
// unconditionally so that an append-mode fake still has to declare it, which
// is what makes "append mode never called this" an assertion a test can make
// rather than a method it cannot see.
DeleteLabel(ctx context.Context, owner, repo, name string) error
}
Writer is the half of *github.Client that applying a plan uses.
It is an interface so the semantics here — the order, the abandonment of a failed repository, the refusal to delete — are testable against a fake that records calls, without an HTTP mock deciding whether the test passes. The end-to-end suite drives the real client against httptest; this narrows what has to be simulated to answer "did it do the right things, in the right order".