# GitHub Actions

The composite Action, its three modes, and the two things a hosted runner does not keep.

Source: https://jmrplens.github.io/ghchronicle/install/actions/

The repository ships a composite Action, so a workflow needs no Go toolchain:
it downloads a release binary and calls it.

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

## Inputs

| Input             | Default              | What it does                                                                                                      |
| ----------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------- |
| `token`           | required             | A personal access token. The automatic `GITHUB_TOKEN` is not enough                                               |
| `config`          | `""`                 | Path to a configuration file. Omit to run with defaults built from `user`                                         |
| `user`            | the repository owner | The account to collect when no config file is given                                                               |
| `mode`            | `once`               | `once`, `backfill` or `card`                                                                                      |
| `backfill-since`  | `""`                 | Bound for `backfill`: a date, `90d`, `2y` or a Go duration. Empty means no bound                                  |
| `card`            | `""`                 | Path of the SVG to write. Empty means no card                                                                     |
| `card-layout`     | `summary`            | One of the thirteen registered layouts                                                                                 |
| `card-theme`      | `auto`               | `dark`, `light`, `auto`, or `both` for a light card and its `_dark` twin                                          |
| `card-fields`     | `""`                 | Comma-separated fields. Empty means the layout's default                                                          |
| `card-motion`     | `once`               | `once`, `loop` or `off`; `loop` changes only `terminal` and `ticker`                                                                 |
| `card-width`      | `""`                 | Card width in pixels. Empty draws the layout at its own width; each one draws between two ends of its own, stated in its [section](/ghchronicle/card/layouts/). Only `activity-heatmap` spends the room on data, a whole year of the calendar at its far end |
| `card-speed`      | `""`                 | How fast an animated layout plays, as a decimal from 0 to 1. Empty means 0.5, the pace every card has always been drawn at; below it the card is slower, above it faster, and every animated layout scales together. 0 is the slowest animation and not a still card, `card-motion: off` is |
| `include-private` | `false`              | `true` counts private repositories when no config file is given (the default up to v1.0.0). See the warning below |
| `version`         | `latest`             | The release to install                                                                                            |

## The three modes

- **once**

  One sweep against a configuration file you supply, which is how a workflow
  feeds a database.

  ```yaml
  name: Collect
  on:
    schedule:
      - cron: "*/30 * * * *"
    workflow_dispatch:

  jobs:
    collect:
      runs-on: ubuntu-latest
      steps:
        - uses: actions/checkout@v7
        - uses: jmrplens/ghchronicle@v1
          with:
            token: ${{ secrets.GHCHRONICLE_TOKEN }}
            mode: once
            config: .github/ghchronicle.yaml
          env:
            INFLUX_TOKEN: ${{ secrets.INFLUX_TOKEN }}
  ```

  The config file refers to the database credentials as `${VAR}`, so they go
  in as secrets and never into the repository.

- **card**

  One sweep and one SVG, and nothing written to any database. This is the
  only mode that needs no store configured at all.

  ```yaml
  name: Profile card
  on:
    schedule:
      - cron: "17 6 * * *"
    workflow_dispatch:

  permissions:
    contents: write

  jobs:
    card:
      runs-on: ubuntu-latest
      steps:
        - uses: actions/checkout@v7
        - uses: jmrplens/ghchronicle@v1
          with:
            token: ${{ secrets.GHCHRONICLE_TOKEN }}
            mode: card
            card: generated/github-stats.svg
            card-layout: github-stats
            card-theme: both
        - name: Commit if it changed
          run: |
            git config user.name "github-actions[bot]"
            git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
            git add generated/
            git diff --staged --quiet || git commit -m "Update the profile card"
            git push
  ```

  The SVG is byte-identical for the same input, so a day with no change
  produces no commit.

- **backfill**

  Reaches as far back as GitHub allows and waits for the rate limit rather
  than stopping. Run it once, by hand.

  ```yaml
  name: Backfill
  on:
    workflow_dispatch:
      inputs:
        since:
          description: "A date, 90d, 2y, or empty for no bound"
          default: "2y"

  jobs:
    backfill:
      runs-on: ubuntu-latest
      timeout-minutes: 360
      steps:
        - uses: actions/checkout@v7
        - uses: jmrplens/ghchronicle@v1
          with:
            token: ${{ secrets.GHCHRONICLE_TOKEN }}
            mode: backfill
            backfill-since: ${{ inputs.since }}
            config: .github/ghchronicle.yaml
  ```

  Give it a generous `timeout-minutes`: an unbounded backfill parks at every
  rate limit reset, and a job that is killed half way has spent the quota and
  kept part of the benefit.

## A card in your profile README

GitHub shows the README of the repository named after your account, `<you>/<you>`,
at the top of your profile. The card lives in that repository as a file, the
README points at it once, and a scheduled workflow replaces the file. The README
itself is never rewritten.

1. Create a personal access token (see [the token](/ghchronicle/start/token/)) and
   store it in `<you>/<you>` as the secret `GHCHRONICLE_TOKEN`. The automatic
   `GITHUB_TOKEN` will not do, even for public numbers: it is not a user, so the
   Action cannot list your repositories with it and the card comes out empty.
2. Add `.github/workflows/card.yml`:

   ```yaml
   name: Profile card
   on:
     schedule:
       - cron: "17 6 * * *"
     workflow_dispatch:

   permissions:
     contents: write

   # One run at a time: two that overlap would race each other to push.
   concurrency:
     group: profile-card
     cancel-in-progress: false

   jobs:
     card:
       runs-on: ubuntu-latest
       steps:
         - uses: actions/checkout@v7
         - uses: jmrplens/ghchronicle@v1
           with:
             token: ${{ secrets.GHCHRONICLE_TOKEN }}
             mode: card
             card: generated/card.svg
             card-layout: animated-counters
             card-theme: both
             card-motion: once
         - name: Commit if it changed
           run: |
             git config user.name "github-actions[bot]"
             git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
             git add generated/
             git diff --staged --quiet || git commit -m "Update the profile card"
             git push
   ```

3. Paste this line into `README.md` wherever the card should appear, once:

   ```html
   <picture><source media="(prefers-color-scheme: dark)" srcset="generated/card_dark.svg"><img src="generated/card.svg" alt="My GitHub statistics"></picture>
   ```

4. Run the workflow by hand from the Actions tab the first time, so the files
   exist before the first scheduled run.

**More than one card.** Repeat the `uses: jmrplens/ghchronicle@v1` step with
another `card` path and layout, and paste one `<picture>` per card. Each step is
a sweep of its own, and each draws every number again: a run that writes a card
collects every family whatever its cadence says, and one in `card` mode writes
nothing back to the state file. Several card steps can therefore share one
`config`, and so one `state_file`, and each card is the whole account. That is
also what each one costs: a card is a cold sweep of every family, so N cards
are N sweeps of [the per-family price](/ghchronicle/api/cost/).

**A README that is not at the root.** This is for other repositories, since
GitHub shows a profile's README only from the root of `<you>/<you>`. The paths
in the `<picture>` are relative to the README, so a `docs/README.md` points at
`../generated/card.svg`.

> **Private repositories**
>
> Without a config file, stars, forks, languages, traffic and repository names
> come from public repositories only, while contribution, commit and pull
> request counts are account totals, as GitHub reports them on your profile.
> Up to v1.0.0, the Action's default configuration counted private
> repositories. With `include-private: true` the card adds the stars, forks,
> languages and traffic of your private repositories, and the layouts that list repositories
> **publish their names**: `repo-list` and `summary` (the Action's default
> layout) list them by default, and so does any layout given `top_repos` in
> `card-fields`. To count them without naming them, pick the fields yourself and leave
> `top_repos` out, for example `card-fields: stars,forks,followers,contributions`.

## Two things a hosted runner does not keep

> **The token must be a personal access token**
>
> Traffic needs push access to *every* repository being collected, and the
> automatic `GITHUB_TOKEN` has it only for the repository the workflow is
> running in. It also has no `security_events`, no `read:packages`, and is not a
> user, so nothing account-wide works with it. See [the
> token](/ghchronicle/start/token/).

**The state file does not survive between runs.** Each run therefore does the
full stargazer walk again. On a small account that is a handful of calls; on an
account with many stars, cache it:

```yaml
- uses: actions/cache@v4
  with:
    path: ~/.ghchronicle
    key: ghchronicle-state-${{ github.run_id }}
    restore-keys: ghchronicle-state-
```

and point `state_file` at `~/.ghchronicle/state.json` in the config. A `card`
mode run reads a restored state file, which is what lets it skip the walk, and
never writes one back: it delivers its points to the card and to no store, so
nothing it learned may tell the next collection that a family is already done.
What fills the cache is a `once` or `backfill` step.

## Without the Action

The same thing by hand, if you would rather not depend on it:

```yaml
- uses: actions/setup-go@v7
  with:
    go-version: stable
- run: go install github.com/jmrplens/ghchronicle/cmd/ghchronicle@latest
- run: ghchronicle -config .github/ghchronicle.yaml -card profile.svg -card-only
  env:
    GITHUB_TOKEN: ${{ secrets.GHCHRONICLE_TOKEN }}
```
