github.com/letsencrypt/boulder@v0.20251208.0/docs/CONTRIBUTING.md (about) 1 Thanks for helping us build Boulder! This page contains requirements and 2 guidelines for Boulder contributions. 3 4 # Patch Requirements 5 6 * All new functionality and fixed bugs must be accompanied by tests. 7 * All patches must meet the deployability requirements listed below. 8 * We prefer pull requests from external forks be created with the ["Allow edits 9 from 10 maintainers"](https://github.com/blog/2247-improving-collaboration-with-forks) 11 checkbox selected. 12 13 # Review Requirements 14 15 * All pull requests must receive at least one approval by a [CODEOWNER](../CODEOWNERS) other than the author. This is enforced by GitHub itself. 16 * All pull requests should receive at least two approvals by [Trusted Contributors](https://github.com/letsencrypt/cp-cps/blob/main/CP-CPS.md#161-definitions). 17 This requirement may be waived when: 18 * the change only modifies documentation; 19 * the change only modifies tests; 20 * in exceptional circumstances, such as when no second reviewer is available at all. 21 22 This requirement should not be waived when: 23 * the change is not written by a Trusted Contributor, to ensure that at least two TCs have eyes on it. 24 * New commits pushed to a branch invalidate previous reviews. In other words, a 25 reviewer must give positive reviews of a branch after its most recent pushed 26 commit. 27 * If a branch contains commits from multiple authors, it needs a reviewer who 28 is not an author of commits on that branch. 29 * Review changes to or addition of tests just as rigorously as you review code 30 changes. Consider: Do tests actually test what they mean to test? Is this the 31 best way to test the functionality in question? Do the tests cover all the 32 functionality in the patch, including error cases? 33 * Are there new RPCs or config fields? Make sure the patch meets the 34 Deployability rules below. 35 36 # Merge Requirements 37 38 We have a bot that will comment on some PRs indicating there are: 39 40 1. configuration changes 41 2. SQL schema changes 42 3. feature flag changes 43 44 These may require either a CP/CPS review or filing of a ticket to make matching changes 45 in production. It is the responsibility of the person merging the PR to make sure 46 the required action has been performed before merging. Usually this will be confirmed 47 in a comment or in the PR description. 48 49 # Patch Guidelines 50 51 * Please include helpful comments. No need to gratuitously comment clear code, 52 but make sure it's clear why things are being done. Include information in 53 your pull request about what you're trying to accomplish with your patch. 54 * Avoid named return values. See 55 [#3017](https://github.com/letsencrypt/boulder/pull/3017) for an example of a 56 subtle problem they can cause. 57 * Do not include `XXX`s or naked `TODO`s. Use 58 the formats: 59 60 ```go 61 // TODO(<email-address>): Hoverboard + Time-machine unsupported until upstream patch. 62 // TODO(#<num>): Pending hoverboard/time-machine interface. 63 // TODO(@githubusername): Enable hoverboard kickflips once interface is stable. 64 ``` 65 66 # Squash merging 67 68 Once a pull request is approved and the tests are passing, the author or any 69 other committer can merge it. We always use [squash 70 merges](https://github.com/blog/2141-squash-your-commits) via GitHub's web 71 interface. That means that during the course of your review you should 72 generally not squash or amend commits, or force push. Even if the changes in 73 each commit are small, keeping them separate makes it easier for us to review 74 incremental changes to a pull request. Rest assured that those tiny changes 75 will get squashed into a nice meaningful-size commit when we merge. 76 77 If the CI tests are failing on your branch, you should look at the logs 78 to figure out why. Sometimes (though rarely) they fail spuriously, in which 79 case you can post a comment requesting that a project owner kick the build. 80 81 # Error handling 82 83 All errors must be addressed in some way: That may be simply by returning an 84 error up the stack, or by handling it in some intelligent way where it is 85 generated, or by explicitly ignoring it and assigning to `_`. We use the 86 `errcheck` tool in our integration tests to make sure all errors are 87 addressed. Note that ignoring errors, even in tests, should be rare, since 88 they may generate hard-to-debug problems. 89 90 When handling errors, always do the operation which creates the error (usually 91 a function call) and the error checking on separate lines: 92 ``` 93 err := someOperation(args) 94 if err != nil { 95 return nil, fmt.Errorf("some operation failed: %w", err) 96 } 97 ``` 98 We avoid the `if err := someOperation(args); err != nil {...}` style as we find 99 it to be less readable and it can give rise to surprising scoping behavior. 100 101 We define two special types of error. `BoulderError`, defined in 102 errors/errors.go, is used specifically when an typed error needs to be passed 103 across an RPC boundary. For instance, if the SA returns "not found", callers 104 need to be able to distinguish that from a network error. Not every error that 105 may pass across an RPC boundary needs to be a BoulderError, only those errors 106 that need to be handled by type elsewhere. Handling by type may be as simple as 107 turning a BoulderError into a specific type of ProblemDetail. 108 109 The other special type of error is `ProblemDetails`. We try to treat these as a 110 presentation-layer detail, and use them only in parts of the system that are 111 responsible for rendering errors to end-users, i.e. WFE2. Note 112 one exception: The VA RPC layer defines its own `ProblemDetails` type, which is 113 returned to the RA and stored as part of a challenge (to eventually be rendered 114 to the user). 115 116 Within WFE2, ProblemDetails are sent to the client by calling 117 `sendError()`, which also logs the error. For internal errors like timeout, 118 or any error type that we haven't specifically turned into a ProblemDetail, we 119 return a ServerInternal error. This avoids unnecessarily exposing internals. 120 It's possible to add additional errors to a logEvent using `.AddError()`, but 121 this should only be done when there is is internal-only information to log 122 that isn't redundant with the ProblemDetails sent to the user. Note that the 123 final argument to `sendError()`, `ierr`, will automatically get added to the 124 logEvent for ServerInternal errors, so when sending a ServerInternal error it's 125 not necessary to separately call `.AddError`. 126 127 # Deployability 128 129 We want to ensure that a new Boulder revision can be deployed to the 130 currently running Boulder production instance without requiring config 131 changes first. We also want to ensure that during a deploy, services can be 132 restarted in any order. That means two things: 133 134 ## Good zero values for config fields 135 136 Any newly added config field must have a usable [zero 137 value](https://tour.golang.org/basics/12). That is to say, if a config field 138 is absent, Boulder shouldn't crash or misbehave. If that config file names a 139 file to be read, Boulder should be able to proceed without that file being 140 read. 141 142 Note that there are some config fields that we want to be a hard requirement. 143 To handle such a field, first add it as optional, then file an issue to make 144 it required after the next deploy is complete. 145 146 In general, we would like our deploy process to be: deploy new code + old 147 config; then immediately after deploy the same code + new config. This makes 148 deploys cheaper so we can do them more often, and allows us to more readily 149 separate deploy-triggered problems from config-triggered problems. 150 151 ## Flag-gating features 152 153 When adding significant new features or replacing existing RPCs the 154 `boulder/features` package should be used to gate its usage. To add a flag, a 155 new field of the `features.Config` struct should be added. All flags default 156 to false. 157 158 In order to test if the flag is enabled elsewhere in the codebase you can use 159 `features.Get().ExampleFeatureName` which gets the `bool` value from a global 160 config. 161 162 Each service should include a `map[string]bool` named `Features` in its 163 configuration object at the top level and call `features.Set` with that map 164 immediately after parsing the configuration. For example to enable 165 `UseNewMetrics` and disable `AccountRevocation` you would add this object: 166 167 ```json 168 { 169 ... 170 "features": { 171 "UseNewMetrics": true, 172 "AccountRevocation": false, 173 } 174 } 175 ``` 176 177 Feature flags are meant to be used temporarily and should not be used for 178 permanent boolean configuration options. 179 180 ### Deprecating a feature flag 181 182 Once a feature has been enabled in both staging and production, someone on the 183 team should deprecate it: 184 185 - Remove any instances of `features.Get().ExampleFeatureName`, adjusting code 186 as needed. 187 - Move the field to the top of the `features.Config` struct, under a comment 188 saying it's deprecated. 189 - Remove all references to the feature flag from `test/config-next`. 190 - Add the feature flag to `test/config`. This serves to check that we still 191 tolerate parsing the flag at startup, even though it is ineffective. 192 - File a ticket to remove the feature flag in staging and production. 193 - Once the feature flag is removed in staging and production, delete it from 194 `test/config` and `features.Config`. 195 196 ### Gating RPCs 197 198 When you add a new RPC to a Boulder service (e.g. `SA.GetFoo()`), all 199 components that call that RPC should gate those calls using a feature flag. 200 Since the feature's zero value is false, a deploy with the existing config 201 will not call `SA.GetFoo()`. Then, once the deploy is complete and we know 202 that all SA instances support the `GetFoo()` RPC, we do a followup config 203 deploy that sets the default value to true, and finally remove the flag 204 entirely once we are confident the functionality it gates behaves correctly. 205 206 ### Gating migrations 207 208 We use [database migrations](https://en.wikipedia.org/wiki/Schema_migration) 209 to modify the existing schema. These migrations will be run on live data 210 while Boulder is still running, so we need Boulder code at any given commit 211 to be capable of running without depending on any changes in schemas that 212 have not yet been applied. 213 214 For instance, if we're adding a new column to an existing table, Boulder should 215 run correctly in three states: 216 217 1. Migration not yet applied. 218 2. Migration applied, flag not yet flipped. 219 3. Migration applied, flag flipped. 220 221 Specifically, that means that all of our `SELECT` statements should enumerate 222 columns to select, and not use `*`. Also, generally speaking, we will need a 223 separate model `struct` for serializing and deserializing data before and 224 after the migration. This is because the ORM package we use, 225 [`borp`](https://github.com/letsencrypt/borp), expects every field in a struct to 226 map to a column in the table. If we add a new field to a model struct and 227 Boulder attempts to write that struct to a table that doesn't yet have the 228 corresponding column (case 1), borp will fail with `Insert failed table posts 229 has no column named Foo`. There are examples of such models in sa/model.go, 230 along with code to turn a model into a `struct` used internally. 231 232 An example of a flag-gated migration, adding a new `IsWizard` field to Person 233 controlled by a `AllowWizards` feature flag: 234 235 ```go 236 # features/features.go: 237 238 const ( 239 unused FeatureFlag = iota // unused is used for testing 240 AllowWizards // Added! 241 ) 242 243 ... 244 245 var features = map[FeatureFlag]bool{ 246 unused: false, 247 AllowWizards: false, // Added! 248 } 249 ``` 250 251 ```go 252 # sa/sa.go: 253 254 struct Person { 255 HatSize int 256 IsWizard bool // Added! 257 } 258 259 struct personModelv1 { 260 HatSize int 261 } 262 263 // Added! 264 struct personModelv2 { 265 personModelv1 266 IsWizard bool 267 } 268 269 func (ssa *SQLStorageAuthority) GetPerson() (Person, error) { 270 if features.Enabled(features.AllowWizards) { // Added! 271 var model personModelv2 272 ssa.dbMap.SelectOne(&model, "SELECT hatSize, isWizard FROM people") 273 return Person{ 274 HatSize: model.HatSize, 275 IsWizard: model.IsWizard, 276 } 277 } else { 278 var model personModelv1 279 ssa.dbMap.SelectOne(&model, "SELECT hatSize FROM people") 280 return Person{ 281 HatSize: model.HatSize, 282 } 283 } 284 } 285 286 func (ssa *SQLStorageAuthority) AddPerson(p Person) (error) { 287 if features.Enabled(features.AllowWizards) { // Added! 288 return ssa.dbMap.Insert(context.Background(), personModelv2{ 289 personModelv1: { 290 HatSize: p.HatSize, 291 }, 292 IsWizard: p.IsWizard, 293 }) 294 } else { 295 return ssa.dbMap.Insert(context.Background(), personModelv1{ 296 HatSize: p.HatSize, 297 // p.IsWizard ignored 298 }) 299 } 300 } 301 ``` 302 303 You will also need to update the `initTables` function from `sa/database.go` to 304 tell borp which table to use for your versioned model structs. Make sure to 305 consult the flag you defined so that only **one** of the table maps is added at 306 any given time, otherwise borp will error. Depending on your table you may also 307 need to add `SetKeys` and `SetVersionCol` entries for your versioned models. 308 Example: 309 310 ```go 311 func initTables(dbMap *borp.DbMap) { 312 // < unrelated lines snipped for brevity > 313 314 if features.Enabled(features.AllowWizards) { 315 dbMap.AddTableWithName(personModelv2, "person") 316 } else { 317 dbMap.AddTableWithName(personModelv1, "person") 318 } 319 } 320 ``` 321 322 New migrations should be added at `./sa/db-next`: 323 324 ```shell 325 $ cd sa/db 326 $ sql-migrate new -env="boulder_sa_test" AddWizards 327 Created migration boulder_sa/20220906165519-AddWizards.sql 328 ``` 329 330 Finally, edit the resulting file 331 (`sa/db-next/boulder_sa/20220906165519-AddWizards.sql`) to define your migration: 332 333 ```mysql 334 -- +migrate Up 335 ALTER TABLE people ADD isWizard BOOLEAN SET DEFAULT false; 336 337 -- +migrate Down 338 ALTER TABLE people DROP isWizard BOOLEAN SET DEFAULT false; 339 ``` 340 341 # Expressing "optional" Timestamps 342 Timestamps in protocol buffers must always be expressed as 343 [timestamppb.Timestamp](https://pkg.go.dev/google.golang.org/protobuf/types/known/timestamppb). 344 Timestamps must never contain their zero value, in the sense of 345 `timestamp.AsTime().IsZero()`. When a timestamp field is optional, absence must 346 be expressed through the absence of the field, rather than present with a zero 347 value. The `core.IsAnyNilOrZero` function can check these cases. 348 349 Senders must check that timestamps are non-zero before sending them. Receivers 350 must check that timestamps are non-zero before accepting them. 351 352 # Rounding time in DB 353 354 All times that we send to the database are truncated to one second's worth of 355 precision. This reduces the size of indexes that include timestamps, and makes 356 querying them more efficient. The Storage Authority (SA) is responsible for this 357 truncation, and performs it for SELECT queries as well as INSERT and UPDATE. 358 359 # Release Process 360 361 The current Boulder release process is described in 362 [release.md](https://github.com/letsencrypt/boulder/blob/main/docs/release.md). New 363 releases are tagged weekly, and artifacts are automatically produced for each 364 release by GitHub Actions. 365 366 # Dependencies 367 368 We use [go modules](https://github.com/golang/go/wiki/Modules) and vendor our dependencies. 369 370 To add a dependency, add the import statement to your .go file, then run 371 `go build` on it. This will automatically add the dependency to go.mod. Next, 372 run `go mod vendor && git add vendor/` to save a copy in the vendor folder. 373 374 When vendorizing dependencies, it's important to make sure tests pass on the 375 version you are vendorizing. Currently we enforce this by requiring that pull 376 requests containing a dependency update to any version other than a tagged 377 release include a comment indicating that you ran the tests and that they 378 succeeded, preferably with the command line you run them with. Note that you 379 may have to get a separate checkout of the dependency (using `go get` outside 380 of the boulder repository) in order to run its tests, as some vendored 381 modules do not bring their tests with them. 382 383 ## Updating Dependencies 384 385 To upgrade a dependency, [see the Go 386 docs](https://github.com/golang/go/wiki/Modules#how-to-upgrade-and-downgrade-dependencies). 387 Typically you want `go get <dependency>` rather than `go get -u 388 <dependency>`, which can introduce a lot of unexpected updates. After running 389 `go get`, make sure to run `go mod vendor && git add vendor/` to update the 390 vendor directory. If you forget, CI tests will catch this. 391 392 If you are updating a dependency to a version which is not a tagged release, 393 see the note above about how to run all of a dependency's tests and note that 394 you have done so in the PR. 395 396 Note that updating dependencies can introduce new, transitive dependencies. In 397 general we try to keep our dependencies as narrow as possible in order to 398 minimize the number of people and organizations whose code we need to trust. 399 As a rule of thumb: If an update introduces new packages or modules that are 400 inside a repository where we already depend on other packages or modules, it's 401 not a big deal. If it introduces a new dependency in a different repository, 402 please try to figure out where that dependency came from and why (for instance: 403 "package X, which we depend on, started supporting XML config files, so now we 404 depend on an XML parser") and include that in the PR description. When there are 405 a large number of new dependencies introduced, and we don't need the 406 functionality they provide, we should consider asking the relevant upstream 407 repository for a refactoring to reduce the number of transitive dependencies. 408 409 # Go Version 410 411 The [Boulder development 412 environment](https://github.com/letsencrypt/boulder/blob/main/README.md#setting-up-boulder) 413 does not use the Go version installed on the host machine, and instead uses a 414 Go environment baked into a "boulder-tools" Docker image. We build a separate 415 boulder-tools container for each supported Go version. Please see [the 416 Boulder-tools 417 README](https://github.com/letsencrypt/boulder/blob/main/test/boulder-tools/README.md) 418 for more information on upgrading Go versions. 419 420 # ACME Protocol Divergences 421 422 While Boulder attempts to implement the ACME specification as strictly as 423 possible there are places at which we will diverge from the letter of the 424 specification for various reasons. We detail these divergences (for both the 425 V1 and V2 API) in the [ACME divergences 426 doc](https://github.com/letsencrypt/boulder/blob/main/docs/acme-divergences.md). 427 428 # ACME Protocol Implementation Details 429 430 The ACME specification allows developers to make certain decisions as to how 431 various elements in the RFC are implemented. Some of these fully conformant 432 decisions are listed in [ACME implementation details 433 doc](https://github.com/letsencrypt/boulder/blob/main/docs/acme-implementation_details.md). 434 435 ## Code of Conduct 436 437 The code of conduct for everyone participating in this community in any capacity 438 is available for reference 439 [on the community forum](https://community.letsencrypt.org/guidelines). 440 441 ## Problems or questions? 442 443 The best place to ask dev related questions is on the [Community 444 Forums](https://community.letsencrypt.org/).