Contributing
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
Building and testing
Building needs Go, the version in
go.mod is authoritative, plus GNU Make
and goreleaser.
make buildThree tiers of tests, all driven by make:
make test-shortUnit tests. Run with
-short, so they must have no external dependencies, avoid network requests, and finish in a couple of seconds. Use mocks.make testUnit and integration tests. Integration tests are ordinary Go tests that skip themselves when
testing.Short()is true.make test-e2eEnd-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) andGITHUB_ACTORset to the matching username. Themaketarget tells you which variable is missing. Venom writes its log to./e2e/venom.log.
make lintTip | 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.goSplitting 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
The recognised sections are: |
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.jsonBot-owned, generated from the Go
Speccomments. 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 anincludeshortcode inside the listing block. A missing path fails the build.Shared prose fragments (version filtering, GitHub authentication) are included from
content/en/docs/plugins/_*.adocrather than repeated.Front matter descriptions are checked at build time and must be between 50 and 160 characters.
Go Further
updatecli/updatecli - the CLI, plugins, and e2e tests.
updatecli/website - this documentation.
Troubleshooting - error messages and their causes.
Support - where to ask, and how to report a bug.