README
ΒΆ
Corto
Incident tooling.
Building & testing
$ make
$ make unit-test
$ make CORTO_CONFIG_PATH=/path/to/config.yaml functional-test
Configuring Visual Studio Code (IDE)
Corto's tests use build tags to separate functional and unit tests. If you use Visual Studio Code you'll need to configure it for these tags. Warning: If you don't do this, you will see warnings of unused objects.
Add/edit the following in settings.json
:
...
"go.buildTags": "functional,unit",
...
Releasing
To create a new release of Corto:
$ VERSION=x.y.z
$ DEBEMAIL=username@wikimedia.org dch -v "$VERSION"
$ git add debian/changelog
$ git commit -m "v$VERSION release"
$ git tag -a -m "v$VERSION release" "v$VERSION" HEAD
$ git push origin main
$ git push origin --tags
Creating a Debian package
Corto builds what is referred to as a Debian native package, which is a fancy way of saying that the packaging files
(the contents of debian/
) are in the main source tree; The software, and the packaging for the software are
effectively treated as one in the same. This also means that Corto package versions do not contain a Debian packaging
release number (-1, -2, etc). Any documentation that describes the use of branches to maintain the packaging can
safely be ignored here.
Once a release has been prepared (see Releasing above), all that is required is to push the release sha
to an appropriately named branch ({name}-wikimedia
), and one of Gitlab's CI runners will create a package for you.
For example:
$ git checkout -b bookworm-wikimedia origin/bookworm-wikimedia
$ git merge "v$VERSION"
$ git push origin bookworm-wikimedia
Now you can visit pipelines and find the one that corresponds to your bookworm-wikimedia push. Assuming all stages completed successfully, click the final one (build_ci_deb), and then use Job artifacts on the right hand side of the page to download the Debian package artifacts.
NOTE: The branch that packages are built from ({name}-wikimedia
) only needs to be a copy of the branch you release
from (with its HEAD at the point you want to a release a package from). Do not make changes to this branch that would
prevent a merge from being a fast-forward.
Using
Running an IRC bot
- Create a
config.yaml
as appropriate (see sample) - Start the bot:
./cortobot -config config.yaml
See also: cortobot/README.markdown
Code conventions
Named return parameters
Go allows you to supply named return parameters in the function signature or definition. When present, these variables do not need to be declared within the function body, and a simple return statement (a naked return) automatically returns the named return parameters. Use of named return parameters has the potential to be confusing though, so as a general rule: If you use them it's probably best to standardize and use them everywhere. And if you do use them, it's probably best to adopt the convention of always returning the named parameters (naked or otherwise).
For Corto, we agreed to standardize on NOT using named return parameters.
Don't do this βΉοΈ:
func divide(x, y int) (result int, err error) {
if y == 0 {
err = fmt.Errorf("Can't divide by zero!")
return
}
result = x / y
return
}
Do this instead π:
func divide(x, y int) (int, error) {
var err error
var result int
if y == 0 {
err = fmt.Errorf("Can't divide by zero!")
return -1, err
}
result = x / y
return result, nil
}
Error handling
A very common pattern in Go is to invoke a function that returns
multiple results, one of which is of type error
that is expected to be
non-nil when there are failures. For example:
res, err := divide(4, 0)
if err != nil {
log.Fatal("Oh no:", err)
}
fmt.Println("Result is:", res)
An alternative is to combine the assignment with the expression (an initialization statement):
var err error
var res int
if res, err = divide(4, 0); err != nil {
log.Fatal("Oh no:", err)
}
fmt.Println("Result is:", res)
This can read a bit better (it looks closer to the exception-handling seen in other languages). One caveat with this however, is that using type inference here would limit the scope of the variables to the enclosing block. To make use of a result outside the block, it needs to be pre-declared (in the example above, they both must be). This usually contributes to readability anyway by making scope (and types) more obvious (explicit v implicit).
For Corto: Initialization statements can improve readability, so consider them preferred, and use them whenever it makes sense.
Architecture
Disclaimer: What follows is more or less correct at the time of writing (correct in that it reflects the current implementation), but it is not the path forward. The idea that Corto could be a stateless application (that Phabricator could be the state) hasn't survived contact with reality.
See also: https://groups.google.com/a/wikimedia.org/g/sre-onfire/c/ZtBHtJCJDdk
β
β
βββββββββββββββββ βββββββββββββββββ β
β β β β β
β IRC β β Web(?) β β
β β β β β View(?)
β β β β β
βββββββββ²ββββββββ βββββββββ²ββββββββ β
β β β
β β β
βββββββββββββ βββββββββββββ ββββββββββββββββββββββββ
β β β
β β β
ββββββ΄ββββ΄βββββ β
β β β
β Corto β β
β β β
β β β Controller(?)
β β β
β β β
ββββββ¬ββββ¬βββββ β
β β β
β β β
β β β
βββββββββββββ βββββββββββββ ββββββββββββββββββββββββ
β β β
β β β
βββββββββΌββββββββ βββββββββΌββββββββ β
β β β β β
β Phabricator β β Google Docs β β Model(?)
β β β β β
β β β β β
βββββββββββββββββ βββββββββββββββββ β
β
β
β
ManiphestTaskManager
implements the TaskManager
interface, and completely encapsulates all
interaction with Phabricator. So βfor exampleβ while Gonduit is used to interface
Phabricator directly, ManiphestTaskManager
doesn't expose any its API or structures.
Similarly, GoogleDocument
implements the Document
interface, and encapsulates interaction
with Google's APIs.
Instances of both of these are created in main.go
and passed in during creation of the
Corto
instance.
The objective is to have a clear separation of concerns, and loose coupling. A change in one component becomes less likely to break others, and less coordination is needed amongst developers (think: fewer merge conflicts when merging long-lived branches). Mocking is possible (examples provided), and the code is more testable.
Corto
here manages the high-level logic. It uses its ManiphestTaskManager
to create a new
templated issue, its GoogleDocument
to create a new collab document, and then (using its
ManiphestTaskManager
) adds a reference to that in the metadata comment of the incident.
If you stand back and squint, it's almost an MVC architecture, where Phabricator (and Google) are state (model), Corto provides the business logic (controller), and IRC is presentation (view).