Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 133 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# Contributing

A 5-minute orientation for anyone — human or AI agent — picking up work on this repo for the first time.

## The mental model

This repo manages SaaS-shaped infrastructure for [`perish.dev`](https://perish.dev) — Cloudflare (DNS, Email Routing, etc.) and GitHub (org repos, branch protection, labels), all through Terraform with HCP Terraform as the state backend.

There are no servers, no containers, no Kubernetes manifests. Every resource maps to a managed service.

## Read these first, in this order

1. **[`CLAUDE.md`](./CLAUDE.md)** — design decisions table. Authoritative. Don't re-derive any decision listed there; if you want to change one, update the doc first.
2. **[`README.md`](./README.md)** — what's where.
3. **[`docs/state.md`](./docs/state.md)** — how Terraform state is structured and how to read HCP via API.
4. **[`docs/ci.md`](./docs/ci.md)** — the required checks for any PR to land.
5. **[`docs/setup.md`](./docs/setup.md)** — bootstrap (skip this if HCP + GitHub App are already wired; you'd know).

## The PR workflow

Every change is a PR. Even one-line typos. Even your own. Branch protection on `main` enforces it.

### Branch naming

[Conventional Branch](https://conventional-branch.github.io/): `feat/<topic>`, `fix/<topic>`, `chore/<topic>`, `docs/<topic>`.

### Commit messages

[Conventional Commits](https://www.conventionalcommits.org/): `feat(scope): subject`, `fix(scope): subject`, `docs: subject`, etc. Scope is usually the affected directory (`cloudflare`, `github`, `hcp`).

### PR title

[Conventional PR action format](https://github.com/marketplace/actions/conventional-pull-request) — same prefix rules as commits. Squash-merge collapses the branch's commits into the PR title, so the PR title becomes the commit subject on `main`.

### Required checks

Four checks must pass before merge:

- `terraform fmt`
- `terraform validate (terraform/cloudflare)`
- `terraform validate (terraform/github)`
- `Terraform Cloud/perishdev/…` — HCP's aggregated commit status

The first three run on every PR (including from forks). The HCP check fires per-workspace when a workspace's path filter matches the diff; otherwise HCP rolls up to a single SUCCESS. Docs-only PRs pass cleanly.

### Fork PRs

A maintainer applies the `safe-to-plan` label to authorise HCP speculative plans on a fork PR. Without it, only the fork-safe GH Actions checks run. See [`docs/ci.md`](./docs/ci.md) for the threat model and what to scan for before labelling.

## Where things go

Match the change to the right leaf:

| Want to change | File |
|---|---|
| DNS records on `perish.dev` | [`terraform/cloudflare/dns.tf`](./terraform/cloudflare/dns.tf) |
| Cloudflare zone settings | [`terraform/cloudflare/main.tf`](./terraform/cloudflare/main.tf) |
| Repo settings or new repo | [`terraform/github/repos.tf`](./terraform/github/repos.tf) |
| Branch protection rules | [`terraform/github/branch_protection.tf`](./terraform/github/branch_protection.tf) |
| Issue / PR labels | [`terraform/github/labels.tf`](./terraform/github/labels.tf) |
| New HCP workspace, project, VCS link | not yet code; see [Issue #8](https://github.com/perishdev/infra/issues/8) |
| Anything new (GCP, AWS, etc.) | new leaf `terraform/<concern>/` — see [`terraform/README.md`](./terraform/README.md) |

## Local development

```sh
brew install terraform # ≥ 1.9
brew install gh # for PRs
terraform login # writes ~/.terraform.d/credentials.tfrc.json

cd terraform/cloudflare # or terraform/github
terraform init
terraform plan # speculative; runs in HCP, output streams back
```

`terraform apply` from CLI is blocked on VCS-connected workspaces. Apply happens via HCP when a PR merges to `main`.

## API tricks worth knowing

### Read HCP plan summary without opening the UI

`terraform login` stores a usable HCP API token. From there:

```sh
HCP_TOKEN=$(jq -r '.credentials["app.terraform.io"].token' ~/.terraform.d/credentials.tfrc.json)

# Find your PR's run
curl -s "https://app.terraform.io/api/v2/workspaces/<workspace-id>/runs?filter%5Bstatus%5D=planned_and_finished" \
-H "Authorization: Bearer $HCP_TOKEN" | jq '.data[0]'

# Read structured plan diff
PLAN_ID=...
curl -sL "https://app.terraform.io/api/v2/plans/$PLAN_ID/json-output-redacted" \
-H "Authorization: Bearer $HCP_TOKEN" \
| jq '.resource_changes[] | {address, actions: .change.actions}'
```

Workspace IDs are in [`docs/state.md`](./docs/state.md).

### Confirm apply via API

```sh
curl -s -X POST "https://app.terraform.io/api/v2/runs/<run-id>/actions/apply" \
-H "Authorization: Bearer $HCP_TOKEN" \
-H "Content-Type: application/vnd.api+json" \
-d '{"comment":"reviewed plan via API"}'
```

This counts as the manual confirm — same gate, scripted.

## Things to know that aren't in the design decisions

- **The HCP required-status-check name (`Terraform Cloud/perishdev/repo-id-CffUfWW6H1x6Bauq`) embeds a per-installation VCS ID.** If the GitHub-HCP OAuth connection is ever rebuilt, that string changes and every PR is silently blocked until [`terraform/github/branch_protection.tf`](./terraform/github/branch_protection.tf) is updated. Lives in three places (this file, [`docs/ci.md`](./docs/ci.md), inline in the resource) so future-you finds it from any angle.
- **GitHub Pages cert provisioning can wedge.** Fix: `gh api -X PUT repos/<owner>/<repo>/pages -f 'cname='` then re-set the CNAME. See troubleshooting in [`docs/setup.md`](./docs/setup.md).
- **cf-terraforming is for one-time onboarding**, not steady-state. New Cloudflare resources should be written in Terraform directly, not discovered after the fact. See [`docs/import.md`](./docs/import.md).
- **`tfe_workspace.*` doesn't exist yet.** HCP itself isn't Terraform-managed today; settings clicked into the UI. See [Issue #8](https://github.com/perishdev/infra/issues/8) for the planned arc.

## What not to do

- Don't put secrets in code, in `.tfvars` files, in commit messages, or in CI logs. Sensitive workspace variables live only in HCP.
- Don't bypass branch protection by force-pushing to `main`. It's blocked at the GitHub level, but if you're an admin and tempted: don't.
- Don't add `claude.ai/code` as a Co-Authored-By trailer or collaborator. Policy.
- Don't add features that don't have a use case today. YAGNI applies — the repo deliberately has no Salt, no Ansible, no module abstraction yet.

## When you're done

- Open the PR. Wait for the 4 checks. Read the HCP plan summary (UI or API). Merge.
- If the change was a Terraform apply, confirm in HCP (or via API). Watch for `applied` status.
- Update [`docs/`](./docs/) if any of the design decisions or operational details shifted.

## Questions

If something here doesn't match what you observe, the docs are wrong — open a `docs:` PR. The repo is small enough that "doc drift" is a real risk and worth fixing on sight.
22 changes: 22 additions & 0 deletions docs/import.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,28 @@ Use Cloudflare's own [`cf-terraforming`](https://github.com/cloudflare/cf-terraf

Why not a custom script: cf-terraforming is maintained by Cloudflare alongside the Terraform provider, so import ID formats and HCL schemas track provider changes automatically. A hand-rolled script would drift.

## Discovery token

Before you run cf-terraforming, generate a separate, short-lived Cloudflare token with **Read** scopes on every resource type you're discovering. Don't reuse the HCP token (which has Edit scopes — broader than discovery needs).

1. <https://dash.cloudflare.com/profile/api-tokens> → **Create Token** → **Custom token**.
2. Permissions: pick **Read** on each resource type you're about to discover. Examples:
- Zone — DNS — Read
- Zone — Email Routing Rules — Read
- Account — Email Routing Addresses — Read
3. **Account Resources**: Include — your account. **Zone Resources**: Include — Specific zone — `perish.dev`.
4. **TTL**: set to a day or a few hours. This is throwaway.
5. **Continue to summary** → **Create token** → copy the value (shown once).
6. Park it on your laptop until the run is done:
```sh
read -s "?Paste Cloudflare discovery token: " CF_TOK
echo
echo "$CF_TOK" > /tmp/cf_token
chmod 600 /tmp/cf_token
unset CF_TOK
```
7. After cf-terraforming finishes: `rm /tmp/cf_token`, and revoke the token in the dashboard.

## Install

```sh
Expand Down
18 changes: 15 additions & 3 deletions docs/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,21 +34,33 @@ In Settings → General:

## 3. Cloudflare API token

1. Cloudflare dashboard → My Profile → API Tokens → Create Token.
1. Go to <https://dash.cloudflare.com/profile/api-tokens> → **Create Token**.
2. **Custom token** with these permissions for the `perish.dev` zone and the account it belongs to:
- Zone — DNS — Edit
- Zone — Zone Settings — Edit
3. Account Resources: **Include — your account only**. Zone Resources: **Include — Specific zone — `perish.dev`**.
4. Copy the token (shown once).
5. In HCP → workspace **`cloudflare`** → Variables → add `cloudflare_api_token` as a **Terraform variable**, mark **Sensitive**, paste the token.

If you want to onboard *more* Cloudflare resource types via [`cf-terraforming`](./import.md) later, the discovery token will need Read scopes matching those resource types (e.g. `Account.Rulesets:Read` for redirect rules). That's separate from the HCP token; generate a short-lived token, use it locally, delete it.
## Adding scopes to the Cloudflare token later

When you start managing a new Cloudflare resource type (e.g. Email Routing rules, Rulesets), the existing HCP token needs more permissions. **Edit, don't regenerate** — the value stays the same and HCP keeps working without re-pasting.

1. <https://dash.cloudflare.com/profile/api-tokens> → find the existing token → **⋯ → Edit**.
2. Under **Permissions**, click **+ Add more** and add the new rows. Examples:
- Zone — Email Routing Rules — Edit
- Account — Email Routing Addresses — Edit
- Account — Rulesets — Edit (for redirect rules)
3. Confirm **Account Resources** and **Zone Resources** still match what they were.
4. **Continue to summary** → **Update token**. The token value does not change on a permission edit.

If the dashboard *does* regenerate the value (which happens if you click "Roll" or recreate instead of edit), copy the new value into HCP → workspace `cloudflare` → Variables → `cloudflare_api_token`.

## 4. GitHub App

The `github-org` workspace authenticates as a GitHub App, not a PAT.

1. Org settings → Developer settings → GitHub Apps → New GitHub App.
1. Go to `https://github.com/organizations/perishdev/settings/apps/new` (replace `perishdev` if your org slug differs).
2. Permissions (start narrow, widen on demand):
- Repository: Administration (R/W), Contents (R), Metadata (R), Pull requests (R/W).
- Organization: Members (R), Administration (R/W).
Expand Down
Loading