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
6 changes: 0 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,3 @@ jobs:
contents: read
pull-requests: write
secrets: inherit

deploy-prod:
needs: qa
if: github.event_name == 'push'
uses: ./.github/workflows/deploy-prod.yml
secrets: inherit
22 changes: 22 additions & 0 deletions .github/workflows/deploy-production.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Deploy production

on:
release:
types:
- published

permissions:
contents: read

concurrency:
group: deploy-production
cancel-in-progress: false

jobs:
qa:
uses: ./.github/workflows/qa.yml

deploy-prod:
needs: qa
uses: ./.github/workflows/deploy-prod.yml
secrets: inherit
102 changes: 102 additions & 0 deletions .github/workflows/github-infra.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
name: GitHub infra

on:
pull_request:
branches:
- main
paths:
- infra/github/**
- scripts/import-github-infra.sh
- .github/workflows/github-infra.yml
push:
branches:
- main
paths:
- infra/github/**
- scripts/import-github-infra.sh
- .github/workflows/github-infra.yml

permissions:
contents: read

concurrency:
group: github-infra-${{ github.ref }}
cancel-in-progress: true

env:
GH_TOKEN: ${{ secrets.TERRAFORM_GITHUB_TOKEN }}
TF_VAR_github_token: ${{ secrets.TERRAFORM_GITHUB_TOKEN }}
TF_VAR_netlify_auth_token: ${{ secrets.NETLIFY_AUTH_TOKEN }}
TF_VAR_netlify_site_id: ${{ vars.NETLIFY_SITE_ID }}
TF_VAR_release_please_token: ${{ secrets.RELEASE_PLEASE_TOKEN }}

jobs:
plan:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7

- name: Check Terraform GitHub token
shell: bash
run: |
if [ -z "${TERRAFORM_GITHUB_TOKEN:-}" ]; then
echo "TERRAFORM_GITHUB_TOKEN repository secret is required." >&2
echo "It cannot be created by Terraform because Terraform needs it to manage GitHub." >&2
exit 1
fi
env:
TERRAFORM_GITHUB_TOKEN: ${{ secrets.TERRAFORM_GITHUB_TOKEN }}

- name: Setup Terraform
uses: hashicorp/setup-terraform@v4
with:
terraform_version: 1.15.8

- name: Init
run: terraform -chdir=infra/github init -backend=false -input=false

- name: Import existing GitHub resources
run: ./scripts/import-github-infra.sh

- name: Format check
run: terraform -chdir=infra/github fmt -check -recursive

- name: Validate
run: terraform -chdir=infra/github validate

- name: Plan
run: terraform -chdir=infra/github plan -input=false -no-color

apply:
needs: plan
if: github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7

- name: Check Terraform GitHub token
shell: bash
run: |
if [ -z "${TERRAFORM_GITHUB_TOKEN:-}" ]; then
echo "TERRAFORM_GITHUB_TOKEN repository secret is required." >&2
echo "It cannot be created by Terraform because Terraform needs it to manage GitHub." >&2
exit 1
fi
env:
TERRAFORM_GITHUB_TOKEN: ${{ secrets.TERRAFORM_GITHUB_TOKEN }}

- name: Setup Terraform
uses: hashicorp/setup-terraform@v4
with:
terraform_version: 1.15.8

- name: Init
run: terraform -chdir=infra/github init -backend=false -input=false

- name: Import existing GitHub resources
run: ./scripts/import-github-infra.sh

- name: Apply
run: terraform -chdir=infra/github apply -input=false -auto-approve
5 changes: 2 additions & 3 deletions .github/workflows/release-please.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ jobs:
steps:
- uses: googleapis/release-please-action@v5
with:
# If RELEASE_PLEASE_TOKEN is set, Release PRs trigger CI like a human-made PR.
# Fallback keeps the workflow usable, but GitHub suppresses CI for PRs made with GITHUB_TOKEN.
token: ${{ secrets.RELEASE_PLEASE_TOKEN || secrets.GITHUB_TOKEN }}
# PAT managed by infra/github. Required so Release Please PRs and Releases trigger workflows.
token: ${{ secrets.RELEASE_PLEASE_TOKEN }}
config-file: release-please-config.json
manifest-file: .release-please-manifest.json
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,14 @@ Two branches are kept as primary sources and are deliberately not merged:

## Deployment

Static build deployed by GitHub Actions to Netlify on push to `main`, served at
[timer.marcosgilf.com](https://timer.marcosgilf.com).
Pull requests deploy preview builds to Netlify. Production deploys only when a GitHub Release is
published, served at [timer.marcosgilf.com](https://timer.marcosgilf.com).

## Releases

Release Please opens Release PRs from Conventional Commits. Merging a Release PR bumps
`package.json`, updates `CHANGELOG.md`, creates a `vX.Y.Z` Git tag and a GitHub Release. The app shows
that version in the bottom-right corner.
`package.json`, updates `CHANGELOG.md`, creates a `vX.Y.Z` Git tag and a GitHub Release. Publishing
that release triggers the production deploy. The app shows that version in the bottom-right corner.

Use commit prefixes intentionally:

Expand All @@ -85,8 +85,8 @@ Use commit prefixes intentionally:
- `feat!:` or `BREAKING CHANGE:` → major version
- `docs:` / `chore:` → no release unless paired with releasable changes

Set repository secret `RELEASE_PLEASE_TOKEN` if you want Release PRs to trigger CI like normal PRs;
otherwise GitHub suppresses workflows created by `GITHUB_TOKEN`.
`RELEASE_PLEASE_TOKEN` is managed by `infra/github` and is required; Release Please PRs and releases
must trigger CI/deploy like human-created ones.

## Contributing

Expand Down
4 changes: 4 additions & 0 deletions infra/github/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.terraform/
terraform.tfstate*
terraform.tfvars
*.tfplan
24 changes: 24 additions & 0 deletions infra/github/.terraform.lock.hcl

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

100 changes: 100 additions & 0 deletions infra/github/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# GitHub repository IaC

Terraform-managed settings for `marcosgilf/timer`.

## What it manages

- Repository settings: description, visibility, merge strategy, delete branch on merge
- Feature toggles: Issues enabled, Projects disabled, Wiki disabled
- Vulnerability alerts
- Repository ruleset for `main`
- GitHub Actions credentials:
- secret `NETLIFY_AUTH_TOKEN`
- variable `NETLIFY_SITE_ID`
- secret `RELEASE_PLEASE_TOKEN`

## Why `RELEASE_PLEASE_TOKEN` exists

Release Please failed with `GITHUB_TOKEN` because GitHub Actions was not allowed to create PRs. A
fine-grained PAT stored as `RELEASE_PLEASE_TOKEN` fixes two problems at once:

1. Release Please can open Release PRs.
2. Release PRs and GitHub Releases created by Release Please trigger workflows like a human-created PR
or release. GitHub suppresses follow-up workflows when using `GITHUB_TOKEN`.

Required permissions for that PAT, scoped to `marcosgilf/timer`:

- Contents: Read and write
- Pull requests: Read and write
- Metadata: Read

The Terraform `github_token` (used to apply this module) needs broader repo administration:

- Administration: Read and write
- Secrets: Read and write
- Variables: Read and write
- Metadata: Read

## First-time setup

Token creation is the only manual part; GitHub does not provide a safe API for scripts to mint user
PATs. Use the bootstrap script for everything else:

```sh
./scripts/setup-github-iac.sh
```

It reuses valid values from env vars or `infra/github/terraform.tfvars`, prompts only for missing or
expired values, writes local `terraform.tfvars` with `0600` permissions, runs `terraform init`, calls
`./scripts/import-github-infra.sh`, then runs `terraform plan`. Existing GitHub Actions secrets are
write-only, so their values cannot be reused unless they are already in env vars or local tfvars. See
[scripts/README.md](../../scripts/README.md).

Equivalent manual setup:

```hcl
github_token = "github_pat_..."

netlify_auth_token = "nfp_..."
netlify_site_id = "00000000-0000-0000-0000-000000000000"
release_please_token = "github_pat_..."
```

```sh
terraform -chdir=infra/github init
terraform -chdir=infra/github import github_repository.this timer
terraform -chdir=infra/github import github_repository_ruleset.main timer:20583655
terraform -chdir=infra/github plan
terraform -chdir=infra/github apply
```

Secrets and variables are imported when they already exist. Missing ones are skipped during import and
created on apply from `terraform.tfvars` / `TF_VAR_*` values.

## GitHub Actions

`.github/workflows/github-infra.yml` runs on changes to this folder or the import script:

- Pull requests: `./scripts/import-github-infra.sh`, then `terraform plan`
- Pushes to `main`: same import, then `terraform apply`

Required repository secret for the workflow itself:

- `TERRAFORM_GITHUB_TOKEN` — fine-grained PAT with Administration R/W, Secrets R/W, Variables R/W,
Metadata Read, scoped to `marcosgilf/timer`

The workflow imports current resources on every run instead of relying on remote state. That keeps this
small personal repo simple; if the repo grows, move state to Terraform Cloud or another remote backend.

## Day to day

```sh
terraform -chdir=infra/github fmt -recursive
terraform -chdir=infra/github validate
terraform -chdir=infra/github plan
terraform -chdir=infra/github apply
```

## State

State is local and gitignored. Do not commit `terraform.tfstate`, `.terraform/` or `terraform.tfvars`.
69 changes: 69 additions & 0 deletions infra/github/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
resource "github_repository" "this" {
name = var.repository_name
description = "Track tasks, workouts or run pomodoros in a Progressive Web Application"
visibility = "public"

# PR merge strategy: squash + rebase only, no merge commits.
allow_squash_merge = true
allow_rebase_merge = true
allow_merge_commit = false
squash_merge_commit_title = "COMMIT_OR_PR_TITLE"
squash_merge_commit_message = "COMMIT_MESSAGES"

# Cleanup
delete_branch_on_merge = true

# Features
has_issues = true
has_projects = false
has_wiki = false
}

resource "github_repository_vulnerability_alerts" "this" {
repository = github_repository.this.name
}

resource "github_repository_ruleset" "main" {
name = "main"
repository = github_repository.this.name
target = "branch"
enforcement = "active"

conditions {
ref_name {
include = ["~DEFAULT_BRANCH"]
exclude = []
}
}

rules {
deletion = true
non_fast_forward = true
required_linear_history = true

pull_request {
allowed_merge_methods = ["squash", "rebase"]
dismiss_stale_reviews_on_push = false
require_code_owner_review = false
require_last_push_approval = false
required_approving_review_count = 0
required_review_thread_resolution = true
}

required_status_checks {
strict_required_status_checks_policy = true

required_check {
# GitHub Actions app id. Pinning avoids Terraform replacing provider-read checks with
# integration_id = 0 on every plan.
context = "qa / qa"
integration_id = 15368
}

required_check {
context = "deploy-dev / deploy-dev"
integration_id = 15368
}
}
}
}
9 changes: 9 additions & 0 deletions infra/github/outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
output "repository_full_name" {
value = github_repository.this.full_name
description = "Full repository name"
}

output "repository_html_url" {
value = github_repository.this.html_url
description = "Repository URL"
}
4 changes: 4 additions & 0 deletions infra/github/providers.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
provider "github" {
token = var.github_token
owner = var.github_owner
}
Loading