Skip to content
Open
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
17 changes: 0 additions & 17 deletions .claude/infra-copilot.local.md

This file was deleted.

9 changes: 4 additions & 5 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
{
"enabledPlugins": {
"infra-copilot@infra-copilot": true
},
"extraKnownMarketplaces": {
"infra-copilot": {
"source": {
"source": "github",
"repo": "hasansezertasan/infra-copilot"
},
"autoUpdate": false
}
}
},
"enabledPlugins": {
"infra-copilot@infra-copilot": true
}
}
14 changes: 7 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,19 @@ permissions:
contents: read

env:
TERRAFORM_VERSION: "1.9.8"
# Terraform comes from mise.toml + mise.lock, the single pin source for
# local machines and CI. Locked mode fails instead of resolving off-lock.
MISE_LOCKED: "1"

jobs:
fmt:
name: terraform fmt
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
- uses: jdx/mise-action@v4
with:
terraform_version: ${{ env.TERRAFORM_VERSION }}
terraform_wrapper: false
install_args: terraform
- run: terraform fmt -check -recursive terraform/

validate:
Expand All @@ -38,10 +39,9 @@ jobs:
- terraform/github
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
- uses: jdx/mise-action@v4
with:
terraform_version: ${{ env.TERRAFORM_VERSION }}
terraform_wrapper: false
install_args: terraform
- name: terraform init (no backend)
working-directory: ${{ matrix.dir }}
run: terraform init -backend=false -input=false
Expand Down
29 changes: 29 additions & 0 deletions .infra-copilot/config.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
# infra-copilot:customization start
backend: hcp
github_org: perishdev
apex_domain: perish.dev
cloudflare_account_id: d8a72309e747515805b614574ea7f323
cloudflare_zone_id: 78ff9bdc9f1a38c01a935d3d079b1e7b
# First entry is this repo — the checks read it as REPO.
managed_repos:
- perishdev/infra
- perishdev/perishdev.github.io
hcp_org: perishdev
hcp_status_check_id: repo-id-CffUfWW6H1x6Bauq
additional_providers: []
# infra-copilot:customization end
---

# infra-copilot config — perishdev / perish.dev

Public identifiers for this repo, read by the
[infra-copilot plugin](https://github.com/hasansezertasan/infra-copilot) at startup.
Sensitive data never lives here.

`backend: hcp` holds until each leaf cuts over to R2 state and GitHub Actions; see
**Terraform CI migration** in [`decisions.md`](./decisions.md). The cutover switches this
to `backend: object-storage` and drops the `hcp_*` fields.

Keep the two `infra-copilot:customization` comments: a re-scaffold preserves everything
between them verbatim, and stops rather than guessing if they are missing or unbalanced.
42 changes: 42 additions & 0 deletions .infra-copilot/decisions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# infra-copilot decisions

Durable provider, authentication, state, and safety choices for this repository.
Migrated from the locked-decisions table that used to live in [`CLAUDE.md`](../CLAUDE.md).
Change a row deliberately, in the same PR as the code it governs.

Keep the two `infra-copilot:customization` comments around the table: a re-scaffold
preserves everything between them verbatim, and stops rather than guessing if they are
missing or unbalanced.

<!-- infra-copilot:customization start -->

| Decision | Choice | Status | Rationale |
|---|---|---|---|
| Terraform backend | HCP Terraform, org `perishdev`, project `infra`, one workspace per leaf (`cloudflare`, `github-org`) | locked | Managed remote state and locking; no local `.tfstate`. To be superseded per leaf by **Terraform CI migration**. |
| Terraform-time secrets | HCP workspace variables (sensitive) | locked | Provider credentials never touch the repo. To be superseded per leaf by **Terraform CI credentials**. |
| CI-time secrets | None. The old table listed `TF_API_TOKEN`, but no repo secret exists and no workflow reads one. | locked | The fork-safe `fmt` / `validate` gates need no provider access. To be superseded by **Terraform CI credentials**. |
| GitHub auth | GitHub App, not a PAT | locked | Org-scoped, revocable, not tied to a person. |
| CI plan/apply policy | Plan on PR (collaborators automatically; fork PRs only after a maintainer adds `safe-to-plan`); apply gated to `main` plus manual confirmation in HCP | locked | Every apply is confirmed against a reviewed plan. To be superseded by **Terraform CI apply** and **Fork PRs**. |
| PR status checks | `fmt`, both `validate` jobs, and HCP's aggregated `Terraform Cloud/perishdev/repo-id-CffUfWW6H1x6Bauq` | locked | The `repo-id` changes whenever the GitHub↔HCP connection is rebuilt, silently blocking every PR until `terraform/github/branch_protection.tf` is updated. To be superseded by **Plan status check**. |
| At-rest encryption in repo | None: nothing encrypted is committed | locked | Anything sensitive lives in an external store, so there is no key to manage or leak. |
| Toolchain | Exact pins in `mise.toml` (`terraform`, `gh`, `jq`) plus a committed `mise.lock` with per-platform checksums | locked | Same binaries on every machine and in CI; `MISE_LOCKED=1` refuses anything off-lock. CI installs Terraform through `mise`, so `mise.toml` is the only CI pin. |
| Terraform version | `1.15.9`, from `mise.toml` | locked | While HCP runs plans, both workspaces carry the same version and are bumped together with `mise.toml`. After **Terraform CI migration**, `mise.toml` is the only pin. |
| Terraform CI migration | HCP → GitHub Actions, with state in the R2 bucket `perishdev-tfstate`, key `<leaf>/terraform.tfstate`, through Terraform's `s3` backend. Order: bootstrap (bucket and credentials), then one cutover PR per leaf, `github` then `cloudflare`, then HCP retirement. | proposed | Keeps everything on providers already in use (Cloudflare, GitHub) and drops HCP as a third vendor and login. A cutover PR may merge only after that leaf's state has been pushed to R2 and its HCP workspace locked; otherwise the first apply on `main` runs against empty state. Each cutover PR also rewrites what still assumes HCP: `required_status_checks` in `terraform/github/branch_protection.tf`, the leaf's `versions.tf`, and the docs. The R2 bucket is created by hand, not by the `cloudflare` leaf, which cannot safely own the bucket that holds its own state. Supersedes **Terraform backend**, **Terraform-time secrets**, **CI-time secrets**, **CI plan/apply policy**, and **PR status checks** as each leaf cuts over. |
| State locking | `use_lockfile = true`: a `.tflock` object written with a conditional `PutObject` (`If-None-Match`), which R2 supports. PR plans run `-lock=false`. Applies are serialised by workflow `concurrency`, never cancelled midway. | proposed | No DynamoDB-style lock table exists on R2. The plan credential is read-only, so it cannot take a lock, and plans do not need one. One apply at a time keeps the lockfile a backstop rather than the only guard. |
| State backups | After every apply, copy each leaf's state to `backups/<leaf>/<UTC timestamp>.tfstate` in the same bucket; a bucket lifecycle rule expires backups after 90 days. | proposed | R2 implements neither bucket versioning nor object lock, so a bad write or a deleted state object cannot otherwise be undone. |
| Terraform CI credentials | Read-only plan credentials as repo-level Actions secrets: `CLOUDFLARE_PLAN_API_TOKEN`, a read-only GitHub App, an R2 key with Object Read on the state bucket. Write credentials only as `terraform-<leaf>` environment secrets, deployable from `main` only: `CLOUDFLARE_API_TOKEN`, a write GitHub App, an R2 key with Object Read & Write on the state bucket. | proposed | Any branch's workflow can read repo-level secrets, so nothing at repo level may be able to write. Environment secrets reach only jobs running in that environment, and the environment admits only `main`. A read-only GitHub App may not see every attribute a refresh reads (merge settings need Contents: write), in which case github-leaf PR plans run `-refresh=false` against the last-applied state; confirm during the `github` cutover. |
| Terraform CI apply | Auto-apply on merge. Every push to `main` plans and applies every leaf (no path filter), each in its `terraform-<leaf>` environment with no required reviewer. The apply job refuses empty state and refuses any commit that is not `main`'s current tip. | proposed | The PR's reviewed plan is the approval. Environment required reviewers are available on this public repo and were deliberately not chosen. Accepted risks: drift between review and merge (kept small by `strict` branch protection), and drift made outside Terraform being reverted by the next merge, whichever leaf it touched, without a reviewed plan showing it. Supersedes the "production applies are never auto-applied" rule in `CLAUDE.md`. |
| Destructive apply opt-in | Every apply inspects its saved plan's JSON and refuses any action containing `delete` (destroys and both replacement orders) unless the merged PR that produced the commit carries the `allow-destroy` label. PR plan comments list deletes and replacements above the collapsed plan. | proposed | With auto-apply, nothing else stands between a merged mistake and a deleted zone or repo. `removed { lifecycle { destroy = false } }` is a `forget`, not a `delete`, and stays allowed. The label is an intent marker, not authorization: any writer can add it. |
| Plan status check | One required context, `plan`: an aggregate job in `terraform-plan.yml` that always reports and fails when any changed leaf's plan did not succeed. | proposed | Per-leaf jobs are path-filtered, and a required check that a filter skips never reports and blocks the PR forever. Unlike HCP's `repo-id` context, the job name does not change when a connection is rebuilt. |
| Fork PRs | `fmt` and `validate` only, no plan. | proposed | GitHub gives a fork's `pull_request` run no secrets, so a fork cannot plan; the `plan` job reports that as a failure for any leaf the fork changed, and a maintainer re-opens the change from a branch. Supersedes the `safe-to-plan` label flow. |
| Plan visibility | Plans are posted as PR comments and run summaries, public on this public repository. | proposed | HCP kept plan output behind an HCP login. Terraform redacts values marked sensitive; everything else these leaves manage (DNS records, repo settings) is already public. |

<!-- infra-copilot:customization end -->

Statuses:

- `proposed` — under discussion; workflows must not assume it.
- `locked` — authoritative until explicitly superseded.
- `superseded` — retained for history; link or name the replacement decision.

No secrets, tokens, private keys, or credentials in this file.
9 changes: 5 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# AGENTS.md

Guidance for any AI coding agent (Claude Code, opencode, Codex, Cursor, …) working in
this repo. Claude Code also reads [`CLAUDE.md`](./CLAUDE.md); the design decisions and
conventions there are authoritative for every agent — read it first.
this repo. Claude Code also reads [`CLAUDE.md`](./CLAUDE.md); the conventions there and the design
decisions in [`.infra-copilot/decisions.md`](./.infra-copilot/decisions.md) are
authoritative for every agent — read both first.

## What this repo is

Expand Down Expand Up @@ -30,8 +31,8 @@ the first incomplete step, so it's safe to re-invoke.

## Working conventions

Conventions and the locked design decisions are **authoritative in
[`CLAUDE.md`](./CLAUDE.md)** — read it; this file deliberately does not restate them (one
Conventions are **authoritative in [`CLAUDE.md`](./CLAUDE.md)** and the design decisions
in [`.infra-copilot/decisions.md`](./.infra-copilot/decisions.md) — read both; this file deliberately does not restate them (one
source of truth, no drift). The essentials it covers: every change is a PR (branch
protection on `main` needs four green checks), secrets never touch the repo (they live in
HCP workspace variables), applies are never automatic (a human or authenticated API call
Expand Down
13 changes: 3 additions & 10 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,13 @@ The repo is inspired by [`pypi/infra`](https://github.com/pypi/infra) but does n

- **Secrets**: never commit plaintext secrets, and never commit encrypted secrets either — sensitive data lives outside the repo entirely. See the [infra-copilot plugin](https://github.com/hasansezertasan/infra-copilot)'s secrets reference.
- **Environments**: single Cloudflare account, single apex domain today. If/when a staging surface is added, keep it separated at the Terraform workspace level (not just the resource level) so a change to one environment can't silently apply to another.
- **Plan before apply**: every Terraform change goes through `terraform plan` (locally or as an HCP speculative run on a PR) and is reviewed before `apply`. Production applies are never auto-applied.
- **Plan before apply**: every Terraform change goes through `terraform plan` (locally or as an HCP speculative run on a PR) and is reviewed before `apply`. Production applies are never auto-applied (until **Terraform CI apply** in [`.infra-copilot/decisions.md`](./.infra-copilot/decisions.md) is locked, which moves to auto-apply on merge with the PR's plan as the approval).

## Locked design decisions

These are the contracts the repo is built on. Don't re-derive; if changing, update the linked docs first.
The contracts the repo is built on live in [`.infra-copilot/decisions.md`](./.infra-copilot/decisions.md), one row per decision with a status (`proposed`, `locked`, `superseded`). Don't re-derive them; to change one, update that file first, in the same PR as the code it governs. Public identifiers (org, zone, account IDs) live in [`.infra-copilot/config.md`](./.infra-copilot/config.md).

| Concern | Decision | Doc |
|---|---|---|
| Terraform-time secrets store | HCP Terraform workspace variables (sensitive) | [infra-copilot plugin](https://github.com/hasansezertasan/infra-copilot) secrets reference |
| CI-time secrets store | GitHub Actions encrypted secrets (only `TF_API_TOKEN`) | infra-copilot plugin secrets reference |
| Terraform state backend | HCP Terraform (managed) | infra-copilot plugin state reference |
| GitHub auth from Terraform | GitHub App (not PAT) | infra-copilot plugin secrets reference |
| CI plan/apply policy | Plan-on-PR (collaborator auto, fork PRs require `safe-to-plan` label); apply gated to `main` + manual confirmation in HCP | infra-copilot plugin CI reference |
| At-rest encryption in repo | None — nothing encrypted committed; anything sensitive lives in HCP workspace variables or external stores | infra-copilot plugin secrets reference |
The move from HCP to R2 state and GitHub Actions is recorded there as **Terraform CI migration** and is `proposed`: until a leaf cuts over, the HCP rows in that file still hold for that leaf.

## Inherited from the user's global CLAUDE.md (highlights)

Expand Down
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ terraform/
github/ one HCP workspace (github-org), repos + protection + labels
.claude/
settings.json enables the infra-copilot plugin (marketplace + plugin)
infra-copilot.local.md this repo's non-secret config the plugin reads at startup
.infra-copilot/
config.md this repo's non-secret config the plugin reads at startup
decisions.md locked / proposed design decisions, with status
.github/
workflows/ fork-safe terraform fmt + validate gates
docs/
Expand All @@ -46,7 +48,7 @@ docs/
AGENTS.md cross-harness pointer for non-Claude agents
```

For the contracts the repo is built on — secrets, state, CI — see [`CLAUDE.md`](./CLAUDE.md). For the live design decisions table, look there first. The generic setup/import/CI/HCP-API/secrets/state procedure lives in the [infra-copilot plugin](https://github.com/hasansezertasan/infra-copilot).
For the contracts the repo is built on — secrets, state, CI — see [`.infra-copilot/decisions.md`](./.infra-copilot/decisions.md), including the proposed move from HCP to R2 state and GitHub Actions. The generic setup/import/CI/HCP-API/secrets/state procedure lives in the [infra-copilot plugin](https://github.com/hasansezertasan/infra-copilot).

## Contributing

Expand All @@ -57,7 +59,7 @@ Branch protection requires four green checks before any merge to `main`:
- `terraform validate (terraform/github)`
- `Terraform Cloud/perishdev/...` (the HCP aggregated commit status)

Fork PRs only get GitHub Actions; HCP plans require a maintainer to apply the `safe-to-plan` label first. See the [infra-copilot plugin](https://github.com/hasansezertasan/infra-copilot)'s CI reference for the full policy, and [`CLAUDE.md`](./CLAUDE.md) for the locked policy decision.
Fork PRs only get GitHub Actions; HCP plans require a maintainer to apply the `safe-to-plan` label first. See the [infra-copilot plugin](https://github.com/hasansezertasan/infra-copilot)'s CI reference for the full policy, and [`.infra-copilot/decisions.md`](./.infra-copilot/decisions.md) for the policy decision.

Conventional Commits, Conventional Branches, Conventional PR titles.

Expand Down
7 changes: 4 additions & 3 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
# docs/

Operational documentation for the `perishdev/infra` repo. The [top-level `CLAUDE.md`](../CLAUDE.md) holds the locked design decisions; the docs below explain how those decisions play out in day-to-day use.
Operational documentation for the `perishdev/infra` repo. [`../.infra-copilot/decisions.md`](../.infra-copilot/decisions.md) holds the design decisions; the docs below explain how those decisions play out in day-to-day use.

Generic setup/import/CI/HCP-API/secrets/state **procedure** lives in the [infra-copilot plugin](https://github.com/hasansezertasan/infra-copilot). This directory holds perish.dev-specific operations and decisions.

## Reading order for first contact

1. [`../CLAUDE.md`](../CLAUDE.md) — design decisions table. Authoritative.
1. [`../.infra-copilot/decisions.md`](../.infra-copilot/decisions.md) — design decisions, with status. Authoritative.
[`../CLAUDE.md`](../CLAUDE.md) — repo conventions.
2. [`../README.md`](../README.md) — what this repo is.
3. [`../CONTRIBUTING.md`](../CONTRIBUTING.md) — five-minute orientation for contributors and AI agents.
4. [`decisions.md`](./decisions.md) — perish.dev-specific decisions and live resource facts.
Expand All @@ -33,7 +34,7 @@ Don't add a new file for:
- One-off tasks (PR description suffices).
- Things that duplicate provider documentation (link to the vendor instead).
- Generic procedure that applies to any infra-copilot-managed repo — that belongs in the [infra-copilot plugin](https://github.com/hasansezertasan/infra-copilot), not here.
- Things that contradict [`../CLAUDE.md`](../CLAUDE.md) — fix the design decisions table first, then write the doc.
- Things that contradict [`../.infra-copilot/decisions.md`](../.infra-copilot/decisions.md) — fix the decision first, then write the doc.

## When to delete a doc

Expand Down
Loading
Loading