reconcile

package
v8.167.0 Latest Latest
Warning

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

Go to latest
Published: Nov 12, 2026 License: Apache-2.0 Imports: 29 Imported by: 0

Documentation

Overview

Package reconcile is the back half of the repository set-up engine: every set-up step of a declared repository as a check and a repair, run in order by Runner.Run against GitHub and CircleCI, with the outcome as one structured Result the inventory stores.

A step's check reads the repository's state and compares it with what the declaration and the Baseline want. In ModeCheck the step reports the drift as the changes it would make; in ModeRepair it makes them. Every repair is idempotent: a second run finds no drift and changes nothing. What the engine cannot repair it reports as a Finding with the fix spelled out: a declaration whose repository is gone, a redirect on the declared name (a rename the caller follows with a correction PR), a `gen circleci` refusal, the ABS prerequisites of a first chart build, a red first release, a default icon, a repository Renovate shows no sign of scanning.

The steps, in order (Steps): create (only from an entry the caller marks as added — never from a missing repository), scaffold (rendered by the front half and pushed as the first commit before protection), settings baseline, team permissions, branch protection with the required checks on the reported-only rule, CircleCI (check only: a GitHub App pipeline builds the repository; nothing is written on CircleCI), webhooks, Renovate (check only), CODEOWNERS (a pull request), description and visibility, labels (the owning team's team/<team>, created when missing), lifecycle (archived → CircleCI's deploy key deleted, then archived on GitHub; deleted → deleted on GitHub, the entry the record), catalog and mapping (the giantswarm/github workflows), first-release verification (a vX.Y.Z tag → the GitHub App pipeline's statuses on its commit; a missed tag build is reported, never rebuilt; a release tagged otherwise, or built by an OAuth follow alone, is not verified).

What a run costs in requests is counted at the clients' transports (Counter) into Result.Requests, and per step in the log. A check of a converged repository with every step costs at most twenty-three GitHub requests, the budget of the nightly reconciler: the repository (one read, shared by the create, metadata and lifecycle steps); the root listing and the chart's Chart.yaml and values.schema.json; the workflow permission; the teams; the branch protection, the rulesets and the engine's own (two reads, App id or not); the reported checks — one page of the recently merged pull requests and the statuses and check runs of the newest head, three requests once per run, shared by every step that asks — the pipeline files workflows.yml and custom.yml (and .circleci/ config.yml once when the entry does not declare the pipeline); the deploy keys (one read, the circleci step); the webhooks (one read, shared by the circleci and webhooks steps); renovate.json5 and the Dependency Dashboard issue; CODEOWNERS; the owning team's label; the catalog and the mapping; the latest release. Measured in check mode with a person's token: giantswarm/backstage 18, giantswarm/klaus 19 (one of them a CODEOWNERS drift, which also lists the open pull requests); the ruleset reads add two to each and the deploy keys one. A planned change costs the same reads; a repair adds one write per change.

The reconciler workflow of giantswarm/github runs the steps under the App identity, `devctl repo reconcile` runs them as the person, `devctl repo setup` and `repo checks` call the same steps, and giantswarm-repo-manager runs the checks in ModeCheck to fill the inventory.

Index

Constants

View Source
const AlignFilesBranch = "teams-alignment-branch"

AlignFilesBranch is the branch giantswarm/github's align-files action pushes a repository's generated files to and opens its pull request from.

View Source
const AlignFilesLogin = "giantswarm-align-files[bot]"

AlignFilesLogin is the login of the align-files App, the author of the pull requests it opens from AlignFilesBranch.

View Source
const CircleCIAppLogin = "circleci-app[bot]"

CircleCIAppLogin is the login the CircleCI GitHub App posts its commit statuses as. A repository whose CircleCI statuses come from it builds on a GitHub App pipeline: checkout, events and statuses go through the App's installation, and whoever created the pipeline is not load-bearing.

View Source
const CircleCIContextPrefix = "ci/circleci: "

CircleCIContextPrefix is the status context GitHub receives for a CircleCI job: "ci/circleci: <job name>".

View Source
const DefaultLabelColor = "ededed"

DefaultLabelColor is the colour of a label the labels step creates when nothing declares one: GitHub's own for a label it creates on first use, the colour most team/<team> labels of the organization carry.

View Source
const E2ETestsDir = "tests/e2e"

E2ETestsDir is the directory of a repository's end-to-end tests, which Heimdall runs on its pull requests.

View Source
const GitHubReleaseJob = "publish-github-release"

GitHubReleaseJob is the generated tag pipeline's job whose one output is the tag's GitHub release. It runs after every artifact is pushed, so its failure leaves nothing unpublished once the release exists: auto-release creates the release before the pipeline starts, and the job then fails on the release already being there.

View Source
const HeimdallContext = "Heimdall - PR Gatekeeper"

HeimdallContext is the check run of Heimdall's gatekeeper, required once it has reported on a repository with an E2ETestsDir.

View Source
const LifecycleArchived = reposetup.LifecycleArchived

LifecycleArchived is the lifecycle value that archives a repository.

View Source
const LifecycleDeleted = reposetup.LifecycleDeleted

LifecycleDeleted is the lifecycle value that deletes a repository on GitHub; the entry stays in the team file as the record of the deletion.

View Source
const LineGateRulesetName = "devctl: line gate"

LineGateRulesetName is the engine's second ruleset, written for an entry that declares a lineGate alone: the fork line's land gate.

View Source
const ReviewRulesetName = "devctl: review"

ReviewRulesetName is the engine's ruleset of the review rule alone, written when Runner.AlignFilesAppID is set and the entry allows agent merges: the align-files and Renovate Apps, and the release App (Runner.ReleaseAppID) when set, bypass it, and not RulesetName, whose required checks bind their pull requests as everyone's.

View Source
const RulesetName = "devctl: default branch"

RulesetName is the name of the repository ruleset the protection step writes: one per repository, the engine's own. Every other ruleset of the repository is left alone, unless the entry's pruneRulesets deletes the undeclared active ones.

View Source
const TagBuildGrace = 2 * time.Minute

TagBuildGrace is how long after its release a tag may carry no status of its own build yet without being a missed build: CircleCI posts the first status within seconds of the tag push.

Variables

View Source
var PipelineFiles = []string{"workflows.yml", "custom.yml"}

PipelineFiles are the files of a generated pipeline that carry jobs, relative to .circleci: workflows.yml is what `devctl gen circleci` writes, custom.yml the repository-owned additions the setup workflow in config.yml merges into it at pipeline runtime.

Steps lists every step in execution order.

Functions

func AppStatuses added in v8.162.0

func AppStatuses(statuses []*github.RepoStatus) []*github.RepoStatus

AppStatuses are the statuses CircleCIAppLogin posted: the GitHub App pipeline's, each linking its workflow.

func CircleCIPosters added in v8.162.0

func CircleCIPosters(statuses []*github.RepoStatus) (app bool, others []string)

CircleCIPosters says who posted statuses: whether CircleCIAppLogin did, and the other logins, sorted.

func GateContexts

func GateContexts(files ...[]byte) ([]string, error)

GateContexts returns the status contexts of the branch-side jobs of the pipeline documents in files, sorted and without duplicates. A job counts unless its branch filter restricts it to named branches (`only:`) or ignores every branch (`ignore: /.*/`, the tag-only release jobs): those never report on a pull request and cannot gate one. Jobs of every workflow count; custom.yml may add its own workflows. A workflow that runs on a schedule (`triggers:`) or a condition (`when:`, `unless:`) and an approval job wait for something other than a push, so they cannot gate a pull request either.

func GateJobs

func GateJobs(raw []byte) ([]string, error)

GateJobs parses one CircleCI config document and returns the names of its branch-side jobs (see GateContexts).

func IsEngineRuleset added in v8.121.0

func IsEngineRuleset(name string) bool

IsEngineRuleset says whether name is one of the rulesets the engine writes.

func IsInvalidConfig

func IsInvalidConfig(err error) bool

IsInvalidConfig asserts invalidConfigError: the run could not start.

func IsNotOwner added in v8.65.0

func IsNotOwner(err error) bool

IsNotOwner asserts notOwnerError: the caller is not an owner of the organization and cannot create a repository in it.

func ListCircleCIStatuses added in v8.162.0

func ListCircleCIStatuses(ctx context.Context, gh *github.Client, owner, repo, ref string) ([]*github.RepoStatus, error)

ListCircleCIStatuses returns the CircleCI statuses of ref's commit in owner/repo, newest first, as GitHub lists them; none for a ref GitHub does not know.

func ListRepositoryRulesets added in v8.164.0

func ListRepositoryRulesets(ctx context.Context, gh *github.Client, owner, repo string) ([]*github.RepositoryRuleset, error)

ListRepositoryRulesets reads every page of repository-owned rulesets, excluding inherited organization rulesets. A failed page returns no partial list: callers must not infer that a ruleset is absent from incomplete data.

func NotOwnerRefusal added in v8.65.0

func NotOwnerRefusal(owner string) string

NotOwnerRefusal is the text a caller who is not an owner of the organization is refused with: the organization does not let members create repositories, and every path — this engine, the Dev Portal — creates the repository as the person.

func OAuthBuiltSince added in v8.162.0

func OAuthBuiltSince(statuses []*github.RepoStatus, publishedAt time.Time) bool

OAuthBuiltSince says an OAuth follow, a poster other than CircleCIAppLogin, posted a status no earlier than a release published at publishedAt (less tagBuildSlack): its job ran after the tag and may have been the tag's. A status without a creator counts as nobody's.

func ReleaseLine added in v8.162.0

func ReleaseLine(tag string) (line string, version *semver.Version, ok bool)

ReleaseLine reads a release tag of the platform's flow: a vX.Y.Z tag (isReleaseTag), line empty, or a multi-image repository's <name>/vX.Y.Z (imageReleaseTag), line the image's name. ok is false for any other tag: CircleCI's generated tag filters build none.

func StaleCircleCIContexts

func StaleCircleCIContexts(existing, live []string) []string

StaleCircleCIContexts returns the required contexts of CircleCI jobs the pipeline no longer has: every "ci/circleci: <job>" context in existing whose job is not in live. Contexts of other systems are never returned.

func StatusWorkflow added in v8.162.0

func StatusWorkflow(target string) string

StatusWorkflow is the workflow a CircleCI status's target URL names: the workflow id of a GitHub App status (https://app.circleci.com/workflow/<id>/job/<id>) or of a pipeline URL (…/pipelines/<vcs>/<org>/<repo>/<n>/workflows/<id>/jobs/<n>). An OAuth status names its job alone (https://circleci.com/gh/<org>/<repo>/<n>), and the URL itself stands for its workflow.

func TagBuildStatuses added in v8.162.0

func TagBuildStatuses(statuses []*github.RepoStatus, publishedAt time.Time) []*github.RepoStatus

TagBuildStatuses are the statuses of the build of a release tag among the CircleCI statuses of its commit, newest first: the newest of every context. A commit's statuses are the commit's, not the ref's: the branch build of the commit the tag points at posts the same contexts, and keeps posting after the tag when its jobs finish late. So the statuses are read by workflow (StatusWorkflow), and a workflow is the tag's when its pipeline started after the release (publishedAt, less tagBuildSlack).

A dynamic-config pipeline, the generated one, starts with one setup workflow (setupContext), and its continuation starts the build's workflows once the setup succeeded, under workflow ids of their own. Each such workflow belongs to the setup workflow that settled last before it started: the branch build's continuation, which may start after the release, belongs to the branch build's setup, which started before it, and drops out. A commit without a setup status (a hand-kept, static configuration) is read by each workflow's own first status. A branch pipeline started on the same commit after the release (a person's run on the default branch) is not told apart from the tag's: statuses do not name the ref.

func TeamLabel added in v8.129.0

func TeamLabel(team string) string

TeamLabel is the label a team's issues carry across the organization's repositories: team/<team> for the team file team-<team>, the label the issue automation applies on assignment.

func UnpublishedJobs added in v8.140.0

func UnpublishedJobs(failed []string) []string

UnpublishedJobs are the failed jobs of a release's tag pipeline that leave something of the release unpublished: every one but GitHubReleaseJob. The caller has read the GitHub release; a red tag pipeline whose failed jobs all drop out is a published release, not red-release.

Types

type Baseline

type Baseline struct {
	// DefaultBranch is the protected default branch.
	DefaultBranch string `json:"defaultBranch"`

	// Features.
	HasWiki     bool `json:"hasWiki"`
	HasIssues   bool `json:"hasIssues"`
	HasProjects bool `json:"hasProjects"`

	// Merge settings.
	AllowMergeCommit bool `json:"allowMergeCommit"`
	AllowSquashMerge bool `json:"allowSquashMerge"`
	AllowRebaseMerge bool `json:"allowRebaseMerge"`
	// SquashMergeCommitTitle names the squash commit: PR_TITLE is the pull
	// request's title; COMMIT_OR_PR_TITLE, GitHub's default, is the title
	// for a pull request of several commits and the commit's own subject
	// for one of a single commit. The title check validates the title
	// alone and auto-release reads the subject that lands on the branch:
	// under COMMIT_OR_PR_TITLE a one-commit pull request whose title was
	// made conventional after the commit was written merges under its
	// unconventional subject, which git-cliff neither releases nor lists.
	// PR_TITLE in [DefaultBaseline], the subject the merge tool sets too.
	SquashMergeCommitTitle string `json:"squashMergeCommitTitle"`

	// Pull requests.
	AllowUpdateBranch   bool `json:"allowUpdateBranch"`
	AllowAutoMerge      bool `json:"allowAutoMerge"`
	DeleteBranchOnMerge bool `json:"deleteBranchOnMerge"`

	// WorkflowPermissions is the default GITHUB_TOKEN permission of the
	// repository's workflows: read or write.
	WorkflowPermissions string `json:"workflowPermissions"`

	// TeamPermissions maps a team slug to the permission it holds on every
	// repository: pull, triage, push, maintain or admin.
	TeamPermissions map[string]string `json:"teamPermissions"`

	// Branch protection: classic without a devctl App id, the default
	// branch's ruleset ([RulesetName]) with one.
	RequiredReviews int `json:"requiredReviews"`
	// EnforceAdmins binds administrators to classic protection too. True in
	// [DefaultBaseline], the company baseline: what `devctl repo setup`
	// applies and what the merge tool lifts and restores around a merge.
	// The ruleset has no such switch: everyone but its bypass actors is
	// bound.
	EnforceAdmins bool `json:"enforceAdmins"`
	// StrictChecks requires a branch to be up to date before it merges.
	// False in [DefaultBaseline]: on a repository with Renovate and sweep
	// traffic every merge would invalidate every other open pull request
	// and re-run its CI.
	StrictChecks bool `json:"strictChecks"`
	// RequiredChecks are required whatever reported.
	RequiredChecks []string `json:"requiredChecks,omitempty"`
	// RequiredChecksIfReported are required once they have reported on the
	// default branch or a recently merged pull request — the generated
	// GitHub Actions gates. The generated CircleCI pipeline's jobs are
	// candidates the same way, read from the repository.
	RequiredChecksIfReported []string `json:"requiredChecksIfReported,omitempty"`
	// IgnoredChecks are regular expressions of contexts never required and
	// removed when found required: release workflows, path-filtered
	// workflows, the dependency-graph submission — contexts that report
	// on the default branch but cannot report on every pull request.
	IgnoredChecks []string `json:"ignoredChecks,omitempty"`
	// KeepRequiredChecks keeps every currently required context the run was
	// not told to remove: only an IgnoredChecks match, or a CircleCI job
	// missing from the pipeline the request hands in, stops being required.
	// Contexts that have not reported, and the CircleCI contexts of a
	// pipeline read from the repository, stay. On a repository the engine's
	// ruleset protects, only its required_status_checks rule is written:
	// the other rules and the bypass actors stay as they are. Set by
	// `devctl repo checks`, which edits a team's protection on its caller's
	// word.
	KeepRequiredChecks bool `json:"keepRequiredChecks,omitempty"`

	// Webhooks every repository carries; none by default (CircleCI installs
	// its own on follow).
	Webhooks []Webhook `json:"webhooks,omitempty"`

	// RenovateInstallationID is the Renovate GitHub App installation whose
	// repository list the renovate step reads as detail when the token can
	// (an organization owner's; a GitHub App token cannot); 0 reads none.
	// The step's verdict comes from the repository's own evidence.
	RenovateInstallationID int64 `json:"renovateInstallationID"`

	// CatalogRepository holds the catalog and the two workflows, as
	// owner/name.
	CatalogRepository string `json:"catalogRepository"`
	// CatalogPath is the catalog file listing the components.
	CatalogPath string `json:"catalogPath"`
	// CatalogWorkflow regenerates the catalog (input force).
	CatalogWorkflow string `json:"catalogWorkflow"`
	// MappingRepository holds the apps-to-teams mapping, as owner/name.
	MappingRepository string `json:"mappingRepository"`
	// MappingPath is the mapping ConfigMap file.
	MappingPath string `json:"mappingPath"`
	// MappingWorkflow regenerates the mapping (input repository), in
	// CatalogRepository.
	MappingWorkflow string `json:"mappingWorkflow"`
}

Baseline is the repository set-up every declared repository gets on top of its declaration: the settings, team permissions and branch protection of giantswarm/giantswarm#36733 as `devctl repo setup` applies them, the required-check rule, the webhooks, and where the catalog and the mapping live. DefaultBaseline is the company's; a caller adjusts a copy.

func DefaultBaseline

func DefaultBaseline() Baseline

DefaultBaseline is the company baseline: what `devctl repo setup` applies by default, the required-check rule of `devctl repo checks`, the catalog and mapping of giantswarm/github and management-cluster-bases.

type Counter added in v8.82.1

type Counter struct {
	// Base sends the requests; nil means http.DefaultTransport.
	Base http.RoundTripper
	// contains filtered or unexported fields
}

Counter is an http.RoundTripper that counts the requests it sends. The caller builds a client's transport over one and hands it to the Runner, which reads the run's cost from it (Result.Requests). A request counts when it is sent, whatever it answers; one Counter may sit under several clients of the same system, so their requests add up.

func (*Counter) Count added in v8.82.1

func (c *Counter) Count() int

Count is how many requests were sent through the Counter so far.

func (*Counter) RoundTrip added in v8.82.1

func (c *Counter) RoundTrip(req *http.Request) (*http.Response, error)

RoundTrip implements http.RoundTripper.

type CreateRequest added in v8.65.0

type CreateRequest struct {
	// Owner is the GitHub organization; empty means reposetup.DefaultOwner.
	Owner string
	// Team is the slug of the team file the entry is declared in.
	Team string
	// Entry is an accepted entry of the dry run; its Rendered declaration
	// gives the name, the description and the visibility.
	Entry reposetup.Entry
	// Mode is check or repair; empty means check. [ModeCheck] is the dry
	// run: the caller's organization role is read and the two steps plan
	// what they would do, nothing is written.
	Mode Mode
	// RenderOptions selects among the template's options for the scaffold.
	RenderOptions map[string]string
}

CreateRequest is one creation of one declared repository as the caller: the create and scaffold steps of Runner.Run, standalone, with whatever GitHub client the Runner holds — the person's, so the repository is created as them and they are its admin.

type CreateResult added in v8.65.0

type CreateResult struct {
	// Repository is owner/name.
	Repository string `json:"repository"`
	// URL is the repository on GitHub; empty in a dry run of a repository
	// that does not exist yet.
	URL string `json:"url,omitempty"`
	// Created says this run created the repository; false when it existed
	// (a resumed creation) or in a dry run.
	Created bool `json:"created"`
	// ScaffoldCommit is the commit holding the scaffold at the head of the
	// default branch — the one this run pushed, or the one it found.
	ScaffoldCommit string    `json:"scaffoldCommit,omitempty"`
	Mode           Mode      `json:"mode"`
	StartedAt      time.Time `json:"startedAt"`
	FinishedAt     time.Time `json:"finishedAt"`
	// Steps are the create and the scaffold step, in that order.
	Steps []StepResult `json:"steps"`
}

CreateResult is the outcome of Runner.Create.

func (*CreateResult) Failed added in v8.65.0

func (r *CreateResult) Failed() []StepResult

Failed returns the steps that could not run to their end.

func (*CreateResult) Step added in v8.65.0

func (r *CreateResult) Step(step Step) *StepResult

Step returns the result of step, or nil when the run did not execute it.

type Finding

type Finding struct {
	Kind    FindingKind `json:"kind"`
	Message string      `json:"message"`
	Fix     string      `json:"fix"`
	// Advisory says this finding does not prevent convergence. It defaults
	// to [FindingKind.Advisory]; a skipped classic protection read in check
	// mode is advisory too. The artifact carries the weight for its readers.
	Advisory bool `json:"advisory,omitempty"`
}

Finding is something a step reports for a person, with the fix.

type FindingKind

type FindingKind string

FindingKind classifies a finding.

const (
	// FindingRepositoryMissing: the declared repository does not exist and
	// the entry was not added by this change.
	FindingRepositoryMissing FindingKind = "repository-missing"
	// FindingRenamed: the declared name redirects to a renamed repository.
	FindingRenamed FindingKind = "renamed"
	// FindingGenCircleCIRefused: the CircleCI generator would produce no
	// jobs for the declaration.
	FindingGenCircleCIRefused FindingKind = "gen-circleci-refused"
	// FindingABSPrerequisite: the chart lacks something app-build-suite
	// validates on the first build.
	FindingABSPrerequisite FindingKind = "abs-prerequisite"
	// FindingDefaultIcon: the chart still carries the default icon.
	FindingDefaultIcon FindingKind = "default-icon"
	// FindingRedRelease: the latest release's tag pipeline failed in a job
	// that leaves something unpublished (UnpublishedJobs).
	FindingRedRelease FindingKind = "red-release"
	// FindingMissedTagBuild: the latest release's tag carries no CircleCI
	// status of its own build. The reconciler rebuilds no tag; the fix is
	// `devctl repo follow-circleci`, which runs it as the person, or the
	// next tag.
	FindingMissedTagBuild FindingKind = "missed-tag-build"
	// FindingCircleCIPipelineMissing: the repository has a CircleCI
	// configuration but no CircleCI pipeline builds it (an OAuth follow is
	// FindingCircleCIOAuthFollow). CircleCI has no service accounts, so a
	// person creates the GitHub App pipeline with their own CircleCI login:
	// `devctl repo follow-circleci`.
	FindingCircleCIPipelineMissing FindingKind = "circleci-pipeline-missing"
	// FindingCircleCIOAuthFollow: the repository builds through an OAuth
	// follow, alone or beside a GitHub App pipeline. It builds today, through
	// the follower's GitHub grant, and stops when they leave; beside the App
	// pipeline every push builds twice. Advisory: the repository is built.
	// The fix is the person's `devctl repo follow-circleci --migrate`.
	FindingCircleCIOAuthFollow FindingKind = "circleci-oauth-follow"
	// FindingCircleCIWebhookMissing was the finding of an OAuth follow
	// without CircleCI's webhook.
	//
	// Deprecated: the engine no longer follows on CircleCI and never reports
	// it; it stays for readers of older run artifacts, such as
	// giantswarm-repo-manager. Use FindingCircleCIPipelineMissing.
	FindingCircleCIWebhookMissing FindingKind = "circleci-webhook-missing"
	// FindingRenovateNotScanned: the repository shows no sign that Renovate
	// scans it — no configuration, or a configuration without a trace of a
	// run (the Dependency Dashboard issue, a pull request, a commit).
	FindingRenovateNotScanned FindingKind = "renovate-not-scanned"
	// FindingRenovateDeclaredOff: the entry declares renovate: false and
	// the repository carries a Renovate configuration that keeps Renovate
	// on. The engine deletes no file; the fix names the configuration.
	FindingRenovateDeclaredOff FindingKind = "renovate-declared-off"
	// FindingArchivedUndeclared: archived on GitHub without lifecycle:
	// archived.
	FindingArchivedUndeclared FindingKind = "archived-undeclared"
	// FindingPendingPullRequest: a repair landed as a pull request that
	// awaits its merge.
	FindingPendingPullRequest FindingKind = "pending-pull-request"
	// FindingUnchecked: a check could not run with the caller's access.
	FindingUnchecked FindingKind = "unchecked"
	// FindingEntryRefused: the validator refused the entry for a reason
	// other than the CircleCI generator; the fix names the field.
	FindingEntryRefused FindingKind = "entry-refused"
	// FindingForeignRuleset: the repository carries a ruleset the engine did
	// not create and the entry does not declare. It is left alone; a person
	// decides whether it stays, and declares it to keep it.
	FindingForeignRuleset FindingKind = "foreign-ruleset"
	// FindingDeclaredRulesetMissing: the entry declares a ruleset the
	// repository does not carry — one deleted on GitHub, or a name that
	// never matched one. The engine creates no ruleset but its own.
	FindingDeclaredRulesetMissing FindingKind = "declared-ruleset-missing"
	// FindingRulesetsNotEnabled: the repository has no ruleset yet and the
	// run has no devctl App id to write one, so the protection step kept
	// classic branch protection as declared; the fix names the run that
	// writes the ruleset (the reconciler's, with the id) and its bypass
	// actors: the owning team and the repository admins for people, the
	// devctl App for the reconciler.
	FindingRulesetsNotEnabled FindingKind = "rulesets-not-enabled"
	// FindingRulesetPending: the ruleset needs a write this run cannot make
	// without the devctl App id, which its bypass list takes: its rules
	// differ from the declared protection, or classic protection still
	// stands beside it. The run that has the id, the reconciler's, makes it.
	FindingRulesetPending FindingKind = "ruleset-pending"
	// FindingTeamBypassRefused: the owning team cannot be a bypass actor of
	// the ruleset (a secret team, or one GitHub refused), so the App stands
	// alone and a member's own pull request does not merge through the API
	// without a second review; the fix names the team's privacy.
	FindingTeamBypassRefused FindingKind = "team-bypass-refused" //nolint:gosec // G101: a finding kind, not a credential
	// FindingOpenPullRequests: pull requests were open when the lifecycle
	// step archived the repository; they stay open and read-only in the
	// archive. The fix is the way back: the entry without lifecycle:
	// archived, which unarchives the repository.
	FindingOpenPullRequests FindingKind = "open-pull-requests"
)

The kinds of finding.

func (FindingKind) Advisory added in v8.70.0

func (k FindingKind) Advisory() bool

Advisory says whether findings of the kind are for a person only and do not keep the run from converging: the repository is set up as declared, the finding stays in the artifact with its fix. Every other kind names something a person must fix before the repository counts as in sync.

type Label added in v8.129.0

type Label struct {
	// Name is the label's name; GitHub compares it case-insensitively.
	Name string `json:"name"`
	// Color is the six hex digits GitHub takes, without '#'; empty means
	// [DefaultLabelColor].
	Color string `json:"color,omitempty"`
	// Description is the label's description; empty leaves it without one.
	Description string `json:"description,omitempty"`
}

Label is one issue label the labels step keeps on a repository: created when missing, never changed or removed once there.

type Mode

type Mode string

Mode is what a run does with the drift it finds.

const (
	// ModeCheck reports the drift as the changes a repair would make.
	ModeCheck Mode = "check"
	// ModeRepair makes the changes.
	ModeRepair Mode = "repair"
)

type ReportedChecker

type ReportedChecker interface {
	ReportedChecks(ctx context.Context, repository *github.Repository, branch string) ([]string, error)
}

ReportedChecker returns the check contexts that have reported on the heads of the recently merged pull requests — the reported-only rule of `devctl repo checks`. *githubclient.Client satisfies it.

type Request

type Request struct {
	// Owner is the GitHub organization; empty means reposetup.DefaultOwner.
	Owner string
	// Team is the slug of the team file the entry lives in (team-bumblebee).
	Team string
	// Entry is an accepted entry of the front half's dry run; its Rendered
	// declaration is the desired state.
	Entry reposetup.Entry
	// Added says the triggering change added the entry: the one condition
	// under which a missing repository is created.
	Added bool
	// Unarchived says the triggering change took lifecycle: archived from the
	// entry: the one condition under which the lifecycle step unarchives a
	// repository archived on GitHub. An archive no declaration change undoes
	// stays the finding archived-undeclared.
	Unarchived bool
	// Mode is check or repair; empty means check.
	Mode Mode
	// Steps restricts the run to the named steps; nil runs every step. The
	// repository is looked up in any case.
	Steps []Step
	// RenderOptions selects among the template's options for the scaffold.
	RenderOptions map[string]string
	// Pipeline holds the documents of the generated CircleCI pipeline
	// (workflows.yml, custom.yml) the protection step reads the branch-side
	// jobs from instead of the repository's .circleci — the files a caller
	// has just generated and not pushed yet. Nil reads the repository.
	Pipeline [][]byte
	// CodeownersOverride is the repository's CODEOWNERS override, the file
	// align-files writes in place of the generated one
	// ([reposetup.CodeownersOverridePath] in giantswarm/github), resolved by
	// the caller with the entry: [reposetup.ReadCodeownersOverride] from a
	// checkout, [reposetup.Overrides] from the remote. The codeowners step
	// wants it verbatim; nil means the repository has none and the step
	// wants the generated file naming Team.
	CodeownersOverride []byte
}

Request is one run over one declared repository.

type Requests added in v8.82.1

type Requests struct {
	GitHub   int `json:"github"`
	CircleCI int `json:"circleci"`
}

Requests is what a run cost in requests to each system: every request the run's clients sent, counted at the transport (Counter), whatever it answered. The result carries it so that the cost of a repository is known against the reconciler's budget, the App's requests per hour.

func (Requests) String added in v8.82.1

func (q Requests) String() string

String renders the cost for a person; "" when nothing was counted.

type Result

type Result struct {
	// Repository is owner/name as the run addressed it — the renamed name
	// when the declared one redirected.
	Repository string `json:"repository"`
	// Declared is owner/name as the team file declares it.
	Declared string `json:"declared"`
	Team     string `json:"team"`
	Mode     Mode   `json:"mode"`
	// Added says whether the entry was passed as added by the triggering
	// change, which alone allows the create step to create.
	Added bool `json:"added"`
	// Unarchived says whether the entry was passed as one the triggering
	// change took lifecycle: archived from, which alone allows the
	// lifecycle step to unarchive.
	Unarchived bool         `json:"unarchived,omitempty"`
	StartedAt  time.Time    `json:"startedAt"`
	FinishedAt time.Time    `json:"finishedAt"`
	Steps      []StepResult `json:"steps"`
	// Converged says the repository is set up as declared: no step ended in
	// drift or failure and every finding is advisory ([StepResult.Converges]
	// for each step). A finding a person must fix clears it, and a refused
	// entry ([Refused]) never converges: nothing was checked.
	Converged bool `json:"converged"`
	// Requests is what the run cost in requests to GitHub and CircleCI,
	// when the Runner's clients count them ([Runner.GitHubRequests]);
	// omitted when nothing was counted.
	Requests Requests `json:"requests,omitzero"`
}

Result is the outcome of one run over one repository: the structured value the inventory stores and the callers render.

func Refused added in v8.64.7

func Refused(req Request, now time.Time) *Result

Refused is the result of an entry the validator refused: no step ran, the declaration is at fault, and the callers parse the refusal like any other finding instead of an exit without a result. The result carries one step, StepEntry, VerdictReported, with one finding per problem naming the field to fix — FindingGenCircleCIRefused for gen.ci.generate (the CircleCI generator would produce no job), FindingEntryRefused for the rest. Nothing was checked against the declaration, so the result is not converged (Result.Refused tells it from drift): there is nothing to repair, the entry is what a person fixes. No step failed, so the callers exit 0 as for any other finding.

func (*Result) Failed added in v8.63.0

func (r *Result) Failed() []StepResult

Failed returns the steps that could not run to their end.

func (*Result) Findings

func (r *Result) Findings() []Finding

Findings returns every finding of the run, in step order.

func (*Result) Refused added in v8.82.6

func (r *Result) Refused() bool

Refused says the entry was refused by the validator and no step ran: the result is the one Refused builds, StepEntry its only step. A refused result is not converged, and not drift either — the fix is in the declaration, not on GitHub.

func (*Result) Step

func (r *Result) Step(step Step) *StepResult

Step returns the result of step, or nil when the run did not execute it.

func (*Result) WriteTable added in v8.63.0

func (r *Result) WriteTable(w io.Writer) error

WriteTable renders the result for a person: a header naming the repository (and the declared name after a rename), the mode and whether the run converged (a refused entry says so: nothing was checked); one row per step with its verdict and detail; then every finding with its fix.

type Runner

type Runner struct {
	// GitHub is the client the steps read and write GitHub with, under the
	// App installation token or the person's.
	GitHub *github.Client
	// Dispatch is the client the catalog step lists and dispatches the
	// catalog and mapping workflow runs with; nil means GitHub. A caller
	// whose GitHub identity holds no Actions permission on the catalog
	// repository gives the token that does (a workflow run's own, with
	// actions: write) here and keeps every read — the repository lookup,
	// the catalog and the mapping — with GitHub, which sees the private
	// repositories that token does not.
	Dispatch *github.Client
	// Checks answers which check contexts have reported.
	Checks ReportedChecker
	// PreserveCheckSources keeps existing required-check integration IDs and
	// leaves new checks unpinned. Callers accepting generic context names,
	// such as repo checks, cannot infer a GitHub Actions source from a name.
	// False pins the baseline's generated Actions gates as before.
	PreserveCheckSources bool
	// WriteClassicChecks replaces the full classic-protection write with a
	// checks-only writer. When set, only required-check differences are
	// compared; all other classic settings, including strictness, stay as
	// read. The writer receives the latest protection snapshot and must
	// refuse nil protection rather than creating it. Nil uses the baseline's
	// full protection policy. repo checks supplies its narrower writer here.
	WriteClassicChecks func(ctx context.Context, owner, repo, branch string, protection *github.Protection, want []string) error
	// CircleCI is a person's CircleCI client the circleci step reads the
	// project's pipeline definitions with; nil reads GitHub's statuses. The
	// engine writes nothing on CircleCI.
	CircleCI *circleciclient.Client
	// Renderer renders the scaffold the scaffold step pushes.
	Renderer ScaffoldRenderer
	// Baseline is the set-up applied on top of the declaration; nil means
	// [DefaultBaseline].
	Baseline *Baseline
	// DevctlAppID is the numeric id of the devctl GitHub App (the App's
	// settings page; not the client id): what the protection step's bypass
	// list takes to be compared and written. With it the step writes the
	// default branch's ruleset with the App and, for people, the owning
	// team (the team file's team) and the repository admins as bypass
	// actors in pull_request mode (none on agentMerge: false) and removes
	// classic protection; 0 reads a ruleset the repository has and compares
	// its rules alone, and writes classic branch protection as before where
	// there is none yet. An identity without write access to the ruleset
	// gets it without its bypass actors (GitHub returns the field to write
	// access alone): the list is then not compared either, and the summary
	// says so. The reconciler's wiring passes the id.
	DevctlAppID int64
	// AlignFilesAppID is the numeric id of the align-files GitHub App, whose
	// installation opens the generated-file pull requests and arms their
	// auto-merge. With it the protection step moves the review rule into the
	// ruleset [ReviewRulesetName], which the App and the Renovate App bypass
	// for pull requests beside the actors of [RulesetName]; the required
	// checks stay in [RulesetName], which they do not bypass. An entry with
	// agentMerge: false keeps the review in [RulesetName] without bypass. 0
	// keeps the review rule in [RulesetName] and deletes the review ruleset.
	AlignFilesAppID int64
	// ReleaseAppID is the numeric id of the giantswarm-release GitHub App,
	// which the generated release workflows authenticate as. It bypasses
	// [ReviewRulesetName] for pull requests beside the align-files and
	// Renovate Apps, so the version bump pull request the release workflow
	// opens and arms auto-merge on lands once the required checks of
	// [RulesetName], which it does not bypass, are green. 0 adds no release
	// App to the review ruleset.
	ReleaseAppID int64
	// GitHubRequests and CircleCIRequests count the requests the clients
	// send, when the caller built the clients' transports over them (one
	// [Counter] under GitHub, Checks and Dispatch, one under CircleCI). The
	// run's cost is what they counted over the run, in [Result.Requests]
	// and, per step, in the log. Nil counts nothing.
	GitHubRequests, CircleCIRequests *Counter
	// Log receives one line per step and change; nil discards.
	Log io.Writer
	// Now is the clock; nil means time.Now.
	Now func() time.Time
}

Runner runs the set-up steps. GitHub is required; the others are optional and their absence skips what needs them: without CircleCI the CircleCI step reads the default branch's CircleCI statuses instead of the project's pipeline definitions, without Checks no check is required on the reported-only rule (none is removed on a guess either), without Renderer the scaffold cannot be repaired.

func (*Runner) Create added in v8.65.0

func (r *Runner) Create(ctx context.Context, req CreateRequest) (*CreateResult, error)

Create creates the declared repository and pushes its scaffold as the caller, and nothing else: the create step (skipped when the repository exists — a creation resumed after a failure) and the scaffold step (one commit on the default branch; skipped when it is there). The same steps [Run] executes for the reconciler, so the two paths cannot drift. The set-up that follows — settings, protection, CircleCI, the catalog — is the reconciler's, from the merged declaration.

The caller's role in the organization is read first, in every mode: the organization does not let members create repositories, so anyone but an owner is refused with NotOwnerRefusal before a write (IsNotOwner), and a 403 on the creation itself ends the create step with the same text. A step that fails is a VerdictFailed in the result; the scaffold step then does not run and the caller reruns once the cause is fixed.

func (*Runner) Run

func (r *Runner) Run(ctx context.Context, req Request) (*Result, error)

Run executes the steps for req and returns the result. An error means the run could not start (an unaccepted entry, no GitHub client); a step that fails is a VerdictFailed in the result and the run continues.

type ScaffoldRenderer

type ScaffoldRenderer interface {
	Render(ctx context.Context, req reposetup.RenderRequest) (*reposetup.Scaffold, error)
}

ScaffoldRenderer renders the scaffold of an accepted entry into a directory. reposetup.Renderer satisfies it.

type Step

type Step string

Step names one set-up step.

const (
	StepCreate      Step = "create"
	StepScaffold    Step = "scaffold"
	StepSettings    Step = "settings"
	StepPermissions Step = "permissions"
	StepProtection  Step = "protection"
	StepCircleCI    Step = "circleci"
	StepWebhooks    Step = "webhooks"
	StepRenovate    Step = "renovate"
	StepCodeowners  Step = "codeowners"
	StepMetadata    Step = "metadata"
	StepLabels      Step = "labels"
	StepLifecycle   Step = "lifecycle"
	StepCatalog     Step = "catalog"
	StepRelease     Step = "release"

	// StepEntry is the declaration itself: the one step of a [Refused]
	// result, never run by [Runner.Run].
	StepEntry Step = "entry"
)

The steps, in the order Runner.Run executes them.

type StepResult

type StepResult struct {
	Step    Step    `json:"step"`
	Verdict Verdict `json:"verdict"`
	// Summary is one line on the state found.
	Summary string `json:"summary,omitempty"`
	// Changes are the repairs made ([ModeRepair]) or the repairs a run
	// would make ([ModeCheck]).
	Changes []string `json:"changes,omitempty"`
	// Findings are reported, not repaired; each carries its fix.
	Findings []Finding `json:"findings,omitempty"`
}

StepResult is the outcome of one step.

func (*StepResult) Converges added in v8.70.0

func (s *StepResult) Converges() bool

Converges says whether the step lets the run converge: it did not drift or fail, and every finding it carries is advisory.

type Verdict

type Verdict string

Verdict is the outcome of one step.

const (
	// VerdictOK: no drift, nothing to report.
	VerdictOK Verdict = "ok"
	// VerdictDrift: drift found and not repaired — the run was a check, or
	// the repair is a pull request that awaits its merge.
	VerdictDrift Verdict = "drift"
	// VerdictRepaired: drift found and repaired in this run.
	VerdictRepaired Verdict = "repaired"
	// VerdictReported: no drift the engine repairs, but findings with a fix
	// for a person.
	VerdictReported Verdict = "reported"
	// VerdictSkipped: the step did not apply (repository missing or empty,
	// archived or deleted, no client for the system, no CircleCI pipeline
	// for the circleci and release steps).
	VerdictSkipped Verdict = "skipped"
	// VerdictFailed: the step could not run to its end; Summary says why.
	VerdictFailed Verdict = "failed"
)

type Webhook

type Webhook struct {
	URL         string   `json:"url"`
	Events      []string `json:"events"`
	ContentType string   `json:"contentType"`
	// Secret is the shared secret; it is written, never read back.
	Secret string `json:"-"`
}

Webhook is one webhook of the baseline.

Jump to

Keyboard shortcuts

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