Description

Updatecli exists because the alternative was rewriting almost the same update script over and over. It is open to contributions, and plenty of them involve no Go at all.

Note
This page is the canonical contributor guide, verified against Updatecli v0.119.0. The CONTRIBUTING.adoc file in the repository covers building and testing locally and points here for everything else. If the two disagree, the Go source decides, see The source is the source of truth.

Please read the Code of Conduct before taking part.

Ways to contribute

Feedback

Building something several different people can use is genuinely hard, and it is easy to get stuck in a local optimum. Saying what does not work for you is useful on its own. Open an issue, or use one of the channels on the support page.

Documentation

Phrasing, gaps, examples that no longer match reality. See Contributing documentation.

Triage

Confirming bugs, spotting duplicates, and reproducing reports.

Code

Plugins are the easiest place to start, because a plugin can be added without touching the core. See Writing a resource plugin.

Financially

GitHub Sponsors and OpenCollective.

Building and testing

Building needs Go, the version in go.mod is authoritative, plus GNU Make and goreleaser.

make build

Three tiers of tests, all driven by make:

make test-short

Unit tests. Run with -short, so they must have no external dependencies, avoid network requests, and finish in a couple of seconds. Use mocks.

make test

Unit and integration tests. Integration tests are ordinary Go tests that skip themselves when testing.Short() is true.

make test-e2e

End-to-end tests, driven by OVH’s Venom. These need Venom installed, plus GITHUB_TOKEN (a personal access token with read access to public repositories) and GITHUB_ACTOR set to the matching username. The make target tells you which variable is missing. Venom writes its log to ./e2e/venom.log.

make lint
Tip
make help lists every target.

Commits and pull requests

Commits carry a Signed-off-by: trailer, git commit --signoff. Nothing in CI rejects a commit without one, but every recent commit has it, so please follow suit.

The repository has a pull request template; filling it in is the fastest route to a review.

Writing a resource plugin

A resource is a kind usable in a manifest’s sources, conditions, or targets block. Adding one touches four places, none of them in the core’s logic.

The source is the source of truth

Before copying anything from this page into an editor, read an existing plugin. The interfaces here are current as of v0.119.0, but a plugin in the tree cannot drift (this page can).

pkg/plugins/resources/gitcommit is a good model: it is recent, small, implements a source and a condition, and explicitly refuses a target.

1. Create the package

Resource plugins live under pkg/plugins/resources/<name>/, note resources, plural:

pkg
└── plugins
    └── resources
        └── <name>
            ├── main.go        # Spec, the resource struct, New(), Changelog(), ReportConfig()
            ├── main_test.go
            ├── source.go
            ├── source_test.go
            ├── condition.go
            ├── condition_test.go
            ├── target.go
            └── target_test.go

Splitting Spec into its own spec.go, with Validate() beside it, is common once the spec grows.

2. Define the Spec

Spec holds every parameter the manifest can set. Exported fields are populated when the manifest is decoded:

// Spec defines a specification for a "<name>" resource.
type Spec struct {
	// Path specifies a local Git repository path.
	//
	// compatible:
	//   * source
	//   * condition
	//
	// remarks:
	//   * Path overrides the working directory provided by an SCM configuration.
	Path string `yaml:",omitempty"`
}

Tags come from two modules: go-yaml for yaml: and invopop/jsonschema for jsonschema:. Use yaml:",omitempty" on optional fields, jsonschema:"required" on mandatory ones, and jsonschema:"-" to hide a field (which is how deprecated aliases stay out of the schema).

Important

The field comments are not decoration. They are extracted into content/en/schema/latest/policy/manifest/config.json, which is what generates both the parameter tables on this website and the completions your editor offers. A parameter documented wrongly is fixed in the Go comment, not on the website.

The recognised sections are:

A short one-line description of the parameter

  compatible:
    * source
    * condition
    * target

  default:
    What happens when the field is unset, including whether the value is inherited
    from the source output.

  remarks:
    * Things to be aware of when using the parameter

  example:
    * A short, realistic value

3. Implement the interface

The resource struct must satisfy Resource, defined in pkg/core/pipeline/resource/main.go:

type Resource interface {
	// Source returns the resource value
	Source(ctx context.Context, workingDir string, sourceResult *result.Source) error
	// Condition checks if the resource is in the expected state
	Condition(ctx context.Context, version string, scm scm.ScmHandler) (pass bool, message string, err error)
	// Target updates the resource with the given value
	Target(ctx context.Context, source string, scm scm.ScmHandler, dryRun bool, targetResult *result.Target) (err error)
	// Changelog returns the changelog for this resource, or an empty string if not supported
	Changelog(from, to string) *result.Changelogs
	// ReportConfig returns a new resource configuration
	// with only the necessary configuration fields without any sensitive information
	// or context specific data.
	ReportConfig() interface{}
}

All five methods are required, even when a stage makes no sense for the resource. Four points that are not obvious from the signatures:

Results are written, not returned. Source and Target report through *result.Source and *result.Target, set Result to one of the result constants, plus Information and Description:

resultSource.Result = result.SUCCESS
resultSource.Information = hash
resultSource.Description = fmt.Sprintf("Git commit %q found for branch %q", hash, branch)

An unsupported stage returns an error, and that error string is documentation. It is what a user sees and searches for, so make it name the resource:

// Target is not supported for the Git Commit resource.
func (gc *GitCommit) Target(_ context.Context, source string, scm scm.ScmHandler, dryRun bool, resultTarget *result.Target) error {
	return fmt.Errorf("target not supported for the Git Commit resource")
}

Target must honour dryRun. updatecli pipeline diff sets it, and users rely on a diff changing nothing on disk.

ReportConfig must redact. It is what ends up in reports, so return a copy of the Spec carrying only what is safe, no tokens, no passwords, no absolute paths from the runner. redact.URL() strips credentials out of a URL.

Return nil from Changelog when the resource has no changelog to offer.

4. Register the kind

Two entries, both in pkg/core/pipeline/resource/main.go, and both are needed:

// The registry. This map is the gate: a kind missing from it is rejected
// before the switch below is ever reached.
func GetResourceMapping() map[string]interface{} {
	return map[string]interface{}{
		...
		"<name>": &<name>.Spec{},
	}
}
// The constructor.
switch kind {
...
case "<name>":

	return <name>.New(rs.Spec)
}

A kind absent from GetResourceMapping() fails with ⚠ Don’t support resource kind: <name> no matter what the switch says, because the map is checked first. It is also what the JSON schema is generated from, so a kind registered only in the switch gets no parameter table and no editor completion.

The map is the authoritative list of kinds, and the place to look up the exact spelling of an existing one (note the aliases, such as terraform/file for hcl).

5. Test it

Unit tests belong beside the code. Beyond those, add an end-to-end manifest under e2e/updatecli.d/success.d/, named after your kind, e2e/updatecli.d/success.d/gitCommit.yaml, for instance.

The bar for success.d is strict, and it is asserted in e2e/venom.d/test_pipeline.yaml: a updatecli pipeline diff across those manifests must exit 0 and its output must contain neither WARNING: nor ERROR:. A manifest that works but warns will fail the suite.

Sibling directories cover the other outcomes, warning.d, deprecated.d, and skipped.targets.d, each with its own Venom suite in e2e/venom.d/.

6. Document it

A plugin with no page is invisible. Add content/en/docs/plugins/resource/<name>.adoc in the website repository, modelled on an existing page. The parameter table comes from a shortcode rather than from hand-written prose:

{{< resourceparameters "sources" "<name>" >}}
Note
That table stays empty until the JSON schema is regenerated by the chore: update Updatecli jsonschema bot, so a brand-new kind renders nothing at first. Say so on the page and add a temporary hand-written table if the gap matters.

Contributing to the core

The core provides the skeleton: manifest parsing, the pipeline stages, ordering, reporting, and the SCM and action abstractions. It changes less often than plugins do and has no equivalent step-by-step recipe, start from an issue so the design can be discussed before the code exists.

Contributing documentation

The website lives in its own repository, updatecli/website. npm run dev serves it locally.

Pages are AsciiDoc. Two conventions matter more than the rest:

  • Behaviour is verified against the Go source or a real run, never inferred from a plugin’s name. The support matrix at the top of a plugin page is what rots worst.

  • Exact error strings are quoted verbatim, because that is what users paste into a search box.

Three things are generated and must not be hand-edited:

content/en/docs/commands/

Produced by npm run docs, from the CLI’s own help output.

content/en/schema/latest/policy/manifest/config.json

Bot-owned, generated from the Go Spec comments. Fix a wrong parameter description in the Go source.

content/en/changelogs/

Synchronised from releases.

Warning
Hugo shells out to asciidoctor for every .adoc file, so the build fails wholesale if it is not on PATH. Because security.exec.osEnv strips most environment variables before spawning it, a asciidoctor installed through a Ruby version manager may need a small wrapper script on PATH.

A few conventions worth knowing before opening a documentation pull request:

  • A cross-page link is "Version Filtering" page - root-relative, so it survives deploy previews. xref: is for anchors within the same page.

  • Examples live under assets/code_example/ and are pulled in with an include shortcode inside the listing block. A missing path fails the build.

  • Shared prose fragments (version filtering, GitHub authentication) are included from content/en/docs/plugins/_*.adoc rather than repeated.

  • Front matter descriptions are checked at build time and must be between 50 and 160 characters.

Go Further