PAT Rotation and Diagnostics#

The GH_PAT secret is a GitHub Personal Access Token that allows the CI/CD pipeline to read build state and trigger cross-repository workflows. It must be present in every repository that participates in the pipeline.

The ccat pat subcommands provide a single, safe interface for checking and rotating this secret across all ccatobs repositories.

Why a PAT is needed#

The orchestrate-deploy.yml workflow in system-integration dispatches builds to leaf repositories and reads their workflow status. Both operations require a token with permissions beyond the default GITHUB_TOKEN scope.

The canonical list of repositories that need GH_PAT is maintained in ci/dependency-graph.yml. Adding a new repo to that file is sufficient to include it in the next ccat pat rotate run.

Generating a PAT with minimum permissions#

Create a fine-grained Personal Access Token:

  1. Go to GitHub → Settings → Developer settings → Personal access tokens → Fine-grained tokens → Generate new token.

  2. Set Resource owner to ccatobs (the organisation).

  3. Set Repository access to All repositories (or select individual repos matching ci/dependency-graph.yml).

  4. Under Permissions → Repository permissions, grant:

    • Actions: Read-only (allows reading workflow run status)

    • Secrets: Read and write (allows ccat pat rotate to set the secret)

  5. Set an expiry date and note it somewhere so you know when to rotate.

Note

Classic PATs also work. The minimum scopes are repo and workflow. Fine-grained tokens are preferred because their permissions are narrower and auditable per repository.

Where the secret must be set#

GH_PAT is read by orchestrate-deploy.yml in ccatobs/system-integration. However, leaf repositories also need the secret so their own triggered workflows can authenticate back to the orchestrator.

The complete list of repositories is driven by ci/dependency-graph.yml. Run ccat pat status at any time to see the current state.

Checking PAT status#

$ ccat pat status

Prints a table for every repository in the dependency graph:

╭────────────────────────────────────┬────────────────┬──────────────────────────╮
│ Repository                         │ Secret present │ Last rotated             │
├────────────────────────────────────┼────────────────┼──────────────────────────┤
│ ccatobs/system-integration         │ ✓ present      │ 2026-02-21T22:17:45Z     │
│ ccatobs/ops-db                     │ ✓ present      │ 2026-02-21T22:17:46Z     │
│ ccatobs/ops-db-api                 │ ✗ missing      │ —                        │
│ ...                                │ ...            │ ...                      │
╰────────────────────────────────────┴────────────────┴──────────────────────────╯

Exit code is non-zero if any secret is missing, making it safe to call from CI.

Note

The Secrets API returns only the updated_at timestamp. The token value itself is never exposed. To verify that a token is still valid, generate a new one and run ccat pat rotate — the old one is overwritten atomically.

Rotating the PAT#

Preview what would be updated (no secrets written):

$ ccat pat rotate --dry-run

Perform the rotation:

$ ccat pat rotate
Paste GH_PAT token:

The command:

  1. Prompts for the token with hidden input (not echoed to the terminal and not stored in shell history).

  2. Iterates over every repository in ci/dependency-graph.yml plus system-integration, calling gh secret set GH_PAT for each.

  3. Stops on the first failure and reports which repositories were already updated. Re-running after fixing the error is safe — setting the same secret twice is idempotent.

Requires the gh CLI to be authenticated as an organisation member with Secrets: Read and write access to all ccatobs repositories (i.e. the same account used to generate the PAT above, or an admin account).

Recovery after partial failure#

If ccat pat rotate stops mid-run, the output clearly lists which repositories were already updated. The remaining repositories still hold the old token.

To recover:

$ ccat pat rotate          # re-run with the same new token

The repositories that were already updated are set again (no-op), and the run continues from where it left off.