# Build the configuration

A form that writes the config.yaml a normal run takes and the workflow step the Action takes, from the binary's own list of settings.

Source: https://jmrplens.github.io/ghchronicle/configuration/builder/

Answer what your deployment is, and this writes the three things that run it:
the `config.yaml` a normal run reads, the command that runs it, and the step a
workflow gives the composite Action. All three come from one set of answers,
and none of them asks for a credential.

## Why it cannot drift

The controls are not a copy of the settings. `cmd/gen_config` exports
`internal/config`'s own types into a file the page reads, down to the default
each key resolves to, which it measures by validating a probe rather than by
restating a number. `make check-config-options` fails when that file is no
longer what the code produces, in the analysis suite and in CI, so the form
cannot offer a setting the binary does not have and cannot miss one it does.

The other direction is checked too, and its limit is worth saying out loud. The
configurations this form writes for the answer sets in
`internal/config/testdata/config-cases.json` go through the real parser in
`internal/config`'s own tests, including the shapes that have caught people out:
a cadence per family under `every.families`, an `include_private` left off, a
sink whose credentials are `${VAR}` references, a card-only run with no
destination at all, and answers the parser is expected to refuse. Those cases
are generated by the module this page runs, so they cannot be a copy of it, and
every branch of that module that can change what it writes has one. What the
corpus does not prove is behaviour no answer set produces, which is why the
file that owns it lists what it cannot cover and why.

## What the form allows and the parser refuses

A form is a form: a destination is a checkbox and the keys it cannot resolve
without are not, so a few clicks are enough to write a file that dies at
start-up. The page warns about the ones its own generated list of settings can
see, above the outputs, and names the rest here. Refusing the rest in the form
would mean keeping a second copy of a parser rule on this page, which is the
drift the whole arrangement exists to avoid.

- A destination switched on with a required key left empty is refused by name,
  with an example: `sinks.loki: url is required, for example
  http://loki:3100`. The form warns about this one.
- A credential field answered with the credential instead of the name of an
  environment variable is left out of the file. The form warns about this one.
- A file with nothing in it is refused: `the file is empty`.
- No account at all is refused: `targets: set at least one of user, orgs or
  repos`.
- `heartbeat: 0` is refused: it must be positive, and leaving the key out is
  how the loop's tick is derived instead.
- A cadence that is not a duration is refused where it is used:
  `every.families.repo: time: invalid duration "soon"`.
- `sinks.elasticsearch.api_key` filled in beside `sinks.elasticsearch.username`
  is refused: `sinks.elasticsearch: set either api_key or username and
  password, not both`.
- Writing `-` as `sinks.sql.path` while `sinks.stdout` is on is accepted and
  gives two writers one stream; pick one of them.

> **What the form does with a credential**
>
> Where a setting is a credential, the form asks for the NAME of an environment
> variable and writes `${NAME}` into the file. That is the expansion the binary
> performs at start-up, and it is what lets the file be committed. An answer
> that is not a variable name is not written at all: the form says so and
> leaves the key out, rather than folding a pasted token into a reference to a
> variable that will never exist. One field is not covered by that,
> `sinks.otlp.headers`, which is free text because a header is not always a
> credential: what you type there is written as it stands, so put a `${VAR}`
> reference in it. The Action's token is a repository secret, which is what the
> step references.

## The form

This page is a form that writes a configuration. What follows is the inventory it offers, which is generated from the binary's own types, and the three outputs it starts from.

- **The account and its API**

  - `github`: a block of settings
  - `github.token`: string, required, a credential, like `${GITHUB_TOKEN}`
  - `github.base_url`: string, like `https://github.example.com/api/v3`
  - `github.web_url`: string, like `https://github.example.com`
  - `github.timeout`: string, defaults to `30s`, like `30s`
  - `github.reserve_rate`: int, defaults to `500`, like `500`

- **What to collect**

  - `targets`: a block of settings
  - `targets.user`: string, like `your-github-login`
  - `targets.orgs`: list, like `some-org, another-org`
  - `targets.repos`: list, like `someone/one-repo`
  - `targets.exclude`: list, like `someone/experiment-*`
  - `targets.include_forks`: bool, defaults to `false`, like `false`
  - `targets.include_archived`: bool, defaults to `false`, like `false`
  - `targets.include_private`: bool, defaults to `true`, like `true`

- **Where the points go**

  - `sinks`: a block of settings
  - `sinks.influxdb`: a block of settings
  - `sinks.influxdb.url`: string, required, like `http://localhost:8181`
  - `sinks.influxdb.token`: string, a credential, like `${INFLUX_TOKEN}`
  - `sinks.influxdb.org`: string, defaults to `default`, like `default`
  - `sinks.influxdb.bucket`: string, required, like `github`
  - `sinks.influxdb.batch`: int, like `5000`
  - `sinks.influxdb.exclude`: list, defaults to `gh_job_log`, like `gh_job_log`
  - `sinks.influxdb.dedupe`: bool, defaults to `true`, like `true`
  - `sinks.prometheus`: a block of settings
  - `sinks.prometheus.listen`: string, defaults to `:9605`, like `127.0.0.1:9605`
  - `sinks.prometheus.path`: string, defaults to `/metrics`, like `/metrics`
  - `sinks.prometheus.no_prime`: bool, defaults to `false`, like `false`
  - `sinks.otlp`: a block of settings
  - `sinks.otlp.endpoint`: string, required, like `http://collector:4318/v1/metrics`
  - `sinks.otlp.headers`: map, a credential, like `Authorization: Bearer ${OTLP_TOKEN}`
  - `sinks.otlp.service`: string, like `ghchronicle`
  - `sinks.otlp.raw`: bool, defaults to `false`, like `false`
  - `sinks.otlp.batch`: int, like `2000`
  - `sinks.otlp.repeat`: string, like `1m`
  - `sinks.loki`: a block of settings
  - `sinks.loki.url`: string, required, like `http://loki:3100/loki/api/v1/push`
  - `sinks.loki.tenant_id`: string, like `tenant-one`
  - `sinks.loki.labels`: map, like `job: ghchronicle`
  - `sinks.loki.batch`: int, like `1000`
  - `sinks.loki.max_age`: string, like `1h`
  - `sinks.file`: a block of settings
  - `sinks.file.path`: string, required, like `/var/log/ghchronicle/points.lp`
  - `sinks.file.format`: string, one of `influx`, `json`
  - `sinks.file.max_bytes`: int, like `67108864`
  - `sinks.file.keep`: int, like `5`
  - `sinks.stdout`: bool, defaults to `false`, like `false`
  - `sinks.stdout_format`: string, one of `influx`, `json`
  - `sinks.telegraf`: a block of settings
  - `sinks.telegraf.url`: string, required, like `http://telegraf:8186/telegraf`
  - `sinks.telegraf.username`: string, like `telegraf`
  - `sinks.telegraf.password`: string, a credential, like `${TELEGRAF_PASSWORD}`
  - `sinks.telegraf.batch`: int, like `5000`
  - `sinks.telegraf.dedupe`: bool, defaults to `true`, like `true`
  - `sinks.graphite`: a block of settings
  - `sinks.graphite.addr`: string, required, like `graphite:2003`
  - `sinks.graphite.prefix`: string, defaults to `github`, like `github`
  - `sinks.graphite.batch`: int, like `1000`
  - `sinks.graphite.dedupe`: bool, defaults to `true`, like `true`
  - `sinks.sql`: a block of settings
  - `sinks.sql.dialect`: string, defaults to `postgres`, one of `postgres`
  - `sinks.sql.path`: string, required, like `/var/lib/ghchronicle/points.sql`
  - `sinks.sql.max_bytes`: int, like `67108864`
  - `sinks.sql.keep`: int, like `5`
  - `sinks.sql.dedupe`: bool, defaults to `true`, like `true`
  - `sinks.elasticsearch`: a block of settings
  - `sinks.elasticsearch.url`: string, required, like `http://elasticsearch:9200`
  - `sinks.elasticsearch.prefix`: string, defaults to `ghchronicle`, like `ghchronicle`
  - `sinks.elasticsearch.username`: string, like `elastic`
  - `sinks.elasticsearch.password`: string, a credential, like `${ES_PASSWORD}`
  - `sinks.elasticsearch.api_key`: string, a credential, like `${ES_API_KEY}`
  - `sinks.elasticsearch.batch`: int, like `1000`
  - `sinks.elasticsearch.dedupe`: bool, defaults to `true`, like `true`
  - `sinks.dedupe_file`: string, defaults to `ghchronicle-state-written.bin`, like `/var/lib/ghchronicle/state-written.bin`
  - `sinks.dedupe_horizon`: string, defaults to `720h`, like `720h`

- **How often**

  - `every`: a block of settings
  - `every.default`: string, like `15m`
  - `every.groups`: map, like `ci: 1m`
  - `every.families`: map, like `deps: 24h`

- **The run's own log**

  - `log`: a block of settings
  - `log.level`: string, defaults to `info`, one of `debug`, `info`, `warn`, `error`
  - `log.format`: string, defaults to `text`, one of `text`, `json`
  - `log.file`: string, like `/var/log/ghchronicle/ghchronicle.log`
  - `log.max_bytes`: int, like `67108864`
  - `log.keep`: int, like `5`

- **The run**

  - `heartbeat`: string, like `15s`
  - `groups`: list, like `audience, account, repos`
  - `state_file`: string, defaults to `ghchronicle-state.json`, like `/var/lib/ghchronicle/state.json`
  - `backfill`: a block of settings
  - `backfill.since`: string, like `2y`

- **config.yaml**

  Save this beside the binary, or at the path you give to -config, and run it with the command below.

  ```yaml
  github:
    token: ${GITHUB_TOKEN}
  targets:
    user: octocat
  sinks:
    stdout: true
  ```

- **The command that runs it**

  ```sh
  ghchronicle -config config.yaml -once
  ```

- **Workflow step**

  These answers need settings no input carries, so the step reads the file above. Commit it at that path.

  ```yaml
  - uses: jmrplens/ghchronicle@v1
    with:
      token: ${{ secrets.GHCHRONICLE_TOKEN }}
      config: .github/ghchronicle.yaml
  ```

## What to do with each output

There is not one command, which is why the form writes it. A configuration that
names a destination is run with `-once`. A configuration that names none is
refused by `-once`, with the parser asking for a sink you deliberately did not
want, so it is run with `-card-only` and a path for the card instead. The
command above changes with the answers, and so does the step.

The step goes in the `steps:` of a workflow job. Answers no input can carry make
the step read the file, and the file has to be committed at the path the step
names; answers the four inputs can carry are written into the step itself, and
then there is no file at all. A step with no file that names no destination is a
card run, `mode: card` with a `card:` path, because that is the one thing the
Action turns into `-card-only`.

The two outputs default the opposite way on one setting, and the step is written
so it says which one you are getting. `include-private` is off unless you ask
for it, because a card that counts private repositories names them in a public
README; `targets.include_private` is on unless you say otherwise, because the
token already reaches them. So a file-less step always writes the input out
rather than leaving it to a default that means the opposite of the file beside
it. See [GitHub Actions](/ghchronicle/install/actions/) for the workflow around
it, and [the token](/ghchronicle/start/token/) for what the secret needs to be
able to do.

## The rest

- [The file](/ghchronicle/configuration/): what each block is for, and what
  happens when a value is wrong.
- [Targets](/ghchronicle/configuration/targets/): which repositories.
- [Cadences](/ghchronicle/configuration/cadences/): every family, its group and
  its built-in interval.
- [Choosing a store](/ghchronicle/sinks/): one page per destination.
