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/).