---
name: update-dbt-docs-hub
description: Refresh a client's static_index.html in Gemma's dbt-docs-hub and open the quarterly update PR with paulineroehn as reviewer. Use when performing the quarterly dbt docs refresh for a client repo.
disable-model-invocation: true
argument-hint: "<client_slug> <client_repo_path>"
---

# Update dbt-docs-hub for a client

Automates the quarterly refresh workflow documented in
[Gemma-Analytics/dbt-docs-hub README](https://github.com/Gemma-Analytics/dbt-docs-hub#readme):
regenerate the client's `static_index.html`, drop it into
`raw/<client_slug>/`, optionally update the `<li>...</ul>` summary block
in the root `index.html`, and open a PR with `@paulineroehn` set as
reviewer.

## Arguments

`$ARGUMENTS` is two whitespace-separated values:

1. `<client_slug>` — the client's folder name in `raw/`, lowercase with
   underscores (e.g. `maniko`, `fond_of_baesiq_dadada`).
2. `<client_repo_path>` — absolute path to the client's dbt project
   (the folder containing `dbt_project.yml`).

Example invocation from inside the cloned `dbt-docs-hub`:

```
/update-dbt-docs-hub maniko /home/jan/dev/client/maniko/data-transformations
```

## Prerequisites

- The skill must be invoked from a checked-out clone of
  `Gemma-Analytics/dbt-docs-hub`. Refuse to run if the current
  directory's `origin` remote does not match.
- The client repo must already be cloned at `<client_repo_path>` and
  on `main` (the skill will checkout/pull main itself).
- The client project must be on dbt ≥ 1.7.1 (older projects produce a
  3-file artifact set; this skill handles the modern single-file
  `--static` flow only).
- `gh` CLI authenticated, `dbt` on `$PATH`, `paulineroehn` exists as a
  GitHub user in the org (no validation — `gh pr create` will error
  if not).

## Procedure

Follow these steps **in order**. Do not skip steps. After each block,
verify the stated assertion before moving on.

### Cross-step state — values you must carry in your context

Each ```bash``` block in this skill runs in a **fresh shell**. Shell
variables set in one block (e.g. `client_repo_sha=$(git rev-parse …)`)
are **gone** by the next block. Treat the values below as ones *you*
hold across steps in your conversational context, and substitute them
as `<placeholder>` text when you construct subsequent shell commands:

| Placeholder | Set in | Used in |
|---|---|---|
| `<hub_dir>` | step 1 (capture `pwd`) | step 1 dirty-check, step 3 |
| `<client_repo_sha>` | step 2 (`git rev-parse --short HEAD` output) | step 6 PR body |
| `<is_new_client>` | step 3 (1 if `raw/<slug>/` was created, else 0) | step 4 routing |
| `<index_changed>` | step 4 (1 if `index.html` was edited, else 0) | step 5 conditional `git add`, step 6 PR body |
| `<branch>` | step 3 (`update_<client_slug>_<YYYYMM>`) | step 5 push |
| `<DBT>` | step 2 (resolved dbt binary path) | step 2 only — single shell, OK as `$DBT` |

Always wrap `<client_repo_path>` (and any path you receive from the
user) in **double-quotes** in every shell command. Paths with spaces
will otherwise silently break `cd`, `cp`, and `test`.

### 1. Parse and validate `$ARGUMENTS`

- Split `$ARGUMENTS` into `client_slug` and `client_repo_path`.
- Refuse and exit if either is empty.
- Assert `client_slug` matches `^[a-z0-9_]+$`. Reject capitals, hyphens,
  spaces. The 29 existing client folders all conform to this.
- Assert `client_repo_path` exists and contains `dbt_project.yml`:
  ```bash
  test -d "<client_repo_path>" && test -f "<client_repo_path>/dbt_project.yml"
  ```
- Assert the current working directory's origin remote is the hub repo,
  capture `<hub_dir>`, and check both repos for uncommitted changes —
  all in one shell so the captured `pwd` and the second `cd` cooperate:
  ```bash
  git remote get-url origin | grep -q 'Gemma-Analytics/dbt-docs-hub'
  hub_dir=$(pwd)
  ( cd "<client_repo_path>" && git diff --quiet && git diff --cached --quiet ) \
    && ( cd "$hub_dir"      && git diff --quiet && git diff --cached --quiet )
  ```
  Record the value of `$hub_dir` as `<hub_dir>` in your context for
  step 3.

### 2. Refresh client dbt docs

This produces only HTML/metadata artifacts — no row data is returned to
the terminal or written outside `target/`.

**Resolve which `dbt` binary to use.** Almost every Gemma client repo
ships its own Python virtualenv (typically `dbt-env/`) pinned to a
specific classic-dbt version. The global `dbt` on a developer machine
is usually `dbt Fusion`, which does **not** have a `docs` subcommand.
Resolve in this order and use the first hit:

1. `<client_repo_path>/dbt-env/bin/dbt`
2. `<client_repo_path>/.venv/bin/dbt`
3. `<client_repo_path>/venv/bin/dbt`
4. `which dbt-classic` (if the user has a deliberate alias)
5. `dbt` from `$PATH` — only as a last resort, and bail with a clear
   message if it turns out to be Fusion (i.e. `dbt --version` does not
   include `dbt-core`).

```bash
cd "<client_repo_path>"
git checkout main
git pull --ff-only

# Pick the dbt binary
for candidate in ./dbt-env/bin/dbt ./.venv/bin/dbt ./venv/bin/dbt; do
  if [ -x "$candidate" ]; then DBT="$candidate"; break; fi
done
: "${DBT:=dbt}"

"$DBT" docs generate --static
test -f target/static_index.html
git rev-parse --short HEAD
```

**Record the short SHA printed by the final command** as
`<client_repo_sha>` in your context. You will substitute it into the
PR body in step 6.

If `target/static_index.html` is missing after the run:

- Common cause: project on dbt < 1.7.1. Tell the user and abort — do
  not attempt the legacy 3-file workflow from this skill.
- Less common: a non-default `target-path:` in `dbt_project.yml`.
  Inspect it and copy from the configured target dir.

### 3. Prepare the dbt-docs-hub branch

Substitute `<hub_dir>` (captured in step 1) and `<client_slug>` /
`<client_repo_path>` (from arguments) when constructing this block.
Compute `<YYYYMM>` from today's date and form
`<branch>` = `update_<client_slug>_<YYYYMM>`; record both in your
context.

```bash
cd "<hub_dir>"
git checkout main
git pull --ff-only
git checkout -b "<branch>"

if [ -d "raw/<client_slug>" ]; then
  echo "is_new_client=0"
else
  echo "is_new_client=1"
  mkdir -p "raw/<client_slug>"
fi

cp "<client_repo_path>/target/static_index.html" "raw/<client_slug>/static_index.html"
```

**Record the `is_new_client=…` line printed above** as
`<is_new_client>` in your context. Step 4 routes off this value, not
off a separate grep.

### 4. `index.html` summary review

The root `index.html` carries one `<li>...</ul>` block per client. The
block template (taken from the existing Maniko entry) is:

```html
        <li><a href="/<client_slug>"><b><Display Name></b></a></li>
            <ul>
                <li><b>Owner</b>: <Owner></li>
                <li><b>Keywords</b>: <Keywords></li>
                <li><b>Sources</b>: <Sources></li>
                <li><b>Data warehouse</b>: <Data warehouse></li>
                <li><b>Data loading</b>: <Data loading></li>
                <li><b>Data visualization</b>: <Data visualization></li>
            </ul>
```

**Indentation matters** — 8 spaces before the outer `<li>`, 12 before
`<ul>`, 16 before each inner `<li>`. Match it exactly.

**Branch selection.** Route off `<is_new_client>` from step 3:

- `<is_new_client> = 0` → Branch A
- `<is_new_client> = 1` → Branch B

Do not re-derive the branch with a separate grep — the two can
disagree (folder exists but `index.html` entry missing, or vice
versa) and the wrong branch would execute silently.

Initialise `<index_changed>` to `0`. Both branches below tell you when
to flip it to `1`; if neither tells you to, leave it at `0`.

#### Branch A — existing client

1. Locate the block (line number only — do not read the whole file):
   ```bash
   grep -n "href=\"/<client_slug>\"" index.html
   ```
2. Read those 9 lines (outer `<li>` plus the `<ul>` block) with the
   `Read` tool, using the line number from step 1.
3. **Display the block to the user verbatim** so they can review.
4. Ask via `AskUserQuestion` (header `"index.html"`):
   - `"No changes"` — leave `index.html` untouched. Leave
     `<index_changed>` at `0`. Skip to step 5.
   - `"Update fields"` — proceed to field prompts.
5. Field prompts. Six fields to cover (Owner, Keywords, Sources, Data
   warehouse, Data loading, Data visualization). `AskUserQuestion`
   accepts up to **4 questions per call**, so batch as 4 + 2 to
   minimise round-trips. For each field, give exactly two options:
   - `"Keep: <current value>"` — accept as-is
   - `"Change"` — user supplies the new value via the implicit
     `Other` input

   Do **not** describe the answer convention as "empty = keep current"
   — `AskUserQuestion` does not have empty answers. The explicit
   "Keep" / "Change" option pair is what makes this work cleanly.

   Build the new block by substituting the changed fields into the
   template; keep the others verbatim.
6. Replace the old block with the new block using the `Edit` tool —
   pass the full 9-line old block as `old_string` and the new 9-line
   block as `new_string`. **Never** use `sed`/`awk` for this; HTML
   quoting and escape interactions are too fragile. **Set
   `<index_changed>` to `1` in your context after the Edit succeeds.**

#### Branch B — new client

1. Ask via `AskUserQuestion` for all seven values: display name (e.g.
   `"Maniko"`), Owner, Keywords, Sources, Data warehouse, Data
   loading, Data visualization.
2. Build the 9-line block from the template.
3. Insert it just before the closing `</ul>` that wraps all client
   entries (find the line `</ul>` immediately followed by `</body>`).
   Use the `Edit` tool with that closing-`</ul>` line as the
   `old_string` anchor. **Set `<index_changed>` to `1` in your
   context after the Edit succeeds.**

### 5. Commit and push

Display name comes from either the existing block's `<b>...</b>` text
or the value the user provided. Substitute `<client_slug>`, `<branch>`,
and `<Display Name>` into the commands below. If `<index_changed>` is
`1`, include `index.html` in the `git add`; otherwise omit it.

```bash
git add "raw/<client_slug>/static_index.html"
# include the next line only if <index_changed> = 1:
git add index.html
git commit -m "Update <Display Name>"
git push -u origin "<branch>"
```

If `git push` reports the remote branch already exists (skill was run
this month already), stop and surface the error. Do **not** force-push.

### 6. Open the pull request

Before constructing the `gh pr create` command, **verify that
`<client_repo_sha>` (captured in step 2) is non-empty**. If it is
empty, you skipped or aborted step 2's `git rev-parse` — go back and
re-run it. A PR body with a blank source SHA is a silent failure mode
reviewers will not catch.

Substitute `<Display Name>`, `<YYYY-MM>` (e.g. `2026-05`),
`<client_slug>`, `<client_repo_path>`, `<client_repo_sha>`, and the
literal string `updated` or `unchanged` (based on `<index_changed>`)
into the command:

```bash
gh pr create \
  --title "Update <Display Name> (<YYYY-MM>)" \
  --assignee @me \
  --reviewer paulineroehn \
  --body "$(cat <<'EOF'
## Summary
- Quarterly refresh of `raw/<client_slug>/static_index.html`
- Source: `<client_repo_path>` @ <client_repo_sha>
- `index.html` summary: <updated-or-unchanged>

## Test plan
- [ ] CI `deploy-artifacts` workflow green
- [ ] After merge, https://docs.gemmaanalytics.com/<client_slug> renders the new docs

🤖 Generated with [Claude Code](https://claude.com/claude-code)
EOF
)"
```

The single-quoted heredoc (`<<'EOF'`) intentionally **disables** shell
expansion — every placeholder must already have been substituted by
you before the command runs. This protects against partial expansion
bugs (e.g. an unset `$client_repo_sha` silently producing a blank).

Print the resulting PR URL.

## Guardrails

- **Never `Read` / `cat` / `head` / `tail` `static_index.html`.** These
  files are 8–15 MB of minified HTML; reading them balloons context.
  Only `cp` it. Same for the legacy `catalog.json` / `manifest.json`
  if a future version of this skill ever handles them.
- **Never `sed`/`awk` edit `index.html`.** Use the `Edit` tool with the
  exact old block as `old_string`.
- **Never force-push.** A branch-name collision on re-run is the user's
  signal to delete the stale branch manually.
- **Refuse uncommitted-changes states.** Don't auto-stash or auto-commit
  on behalf of the user in either repo.
- **Do not generate or run `dbt show` / `dbtf show`** as part of this
  workflow. `dbt docs generate --static` is the only dbt command this
  skill executes.

## Failure modes to surface clearly

| Symptom | Likely cause | Action |
|---|---|---|
| `target/static_index.html` missing after `dbt docs generate --static` | dbt < 1.7.1 in client repo | Abort, tell user this skill does not support legacy artifacts |
| `gh pr create` fails on reviewer not found | `paulineroehn` username changed or org access lost | Abort, surface the error verbatim — do not try a different reviewer |
| `git push` rejected — branch exists | Skill already run this month | Abort, tell user to delete `update_<slug>_<YYYYMM>` branch on origin or pick a new month suffix |
| `cp` fails — source missing | `dbt docs generate` ran but wrote to a non-default `target-path` | Inspect `<client_repo_path>/dbt_project.yml` for `target-path:`; copy from there |

## Related references

- `dbt-docs-hub/README.md` — manual workflow this skill automates
- `dbt-docs-hub/index.html` — the root summary file edited in step 4
- `dbt-docs-hub/.github/workflows/deploy-artifacts.yml` — what runs on
  merge to deploy `raw/**` to S3
- `dbt-docs-hub/.github/workflows/deploy-website-html.yml` — deploys
  `index.html` to S3
