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
17 changes: 15 additions & 2 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
* @jaspermayone
# Every change here reaches production DNS, so every change needs an approving
# review from the DNS Managers team.
#
# https://github.com/orgs/WITCodingClub/teams/dns-managers
#
# The team must keep write access to this repository, otherwise GitHub ignores
# these rules.

.github/** @jaspermayone
* @WITCodingClub/dns-managers

# These decide what gets applied and how. A mistake here is worse than a
# mistake in a single record, so they are listed again to make that clear.
/.github/ @WITCodingClub/dns-managers
/bin/ @WITCodingClub/dns-managers
/config/ @WITCodingClub/dns-managers
/tools/ @WITCodingClub/dns-managers
5 changes: 5 additions & 0 deletions .github/ISSUE_TEMPLATE/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
blank_issues_enabled: true
contact_links:
- name: Ask in Discord
url: https://discord.gg/witcodingclub
about: Quicker than an issue for anything that is not a DNS change.
31 changes: 31 additions & 0 deletions .github/ISSUE_TEMPLATE/dns-problem.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: DNS problem
description: Report a subdomain that does not resolve or points somewhere wrong.
title: "[problem] "
labels: ["bug"]
body:
- type: input
id: name
attributes:
label: Which name is wrong?
placeholder: docs.witcc.dev
validations:
required: true

- type: textarea
id: expected
attributes:
label: What did you expect, and what happened instead?
validations:
required: true

- type: textarea
id: dig
attributes:
label: Output of dig
description: |
Run this and paste the result.

dig +short docs.witcc.dev
render: shell
validations:
required: false
46 changes: 46 additions & 0 deletions .github/ISSUE_TEMPLATE/subdomain-request.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: Subdomain request
description: Ask for a subdomain when you cannot open a pull request yourself.
title: "[subdomain] "
labels: ["enhancement"]
body:
- type: markdown
attributes:
value: |
A pull request is faster than an issue. See the README for how to open
one. Use this form if you are stuck, or if you are not sure what the
record should be.

- type: input
id: subdomain
attributes:
label: Subdomain
description: The full name you want.
placeholder: docs.witcc.dev
validations:
required: true

- type: input
id: target
attributes:
label: Where should it point?
description: A domain name or an IP address.
placeholder: docs-site.netlify.app
validations:
required: true

- type: input
id: owner
attributes:
label: Who owns it?
description: The WIT email of whoever we should ask when it breaks.
placeholder: yourname@wit.edu
validations:
required: true

- type: textarea
id: purpose
attributes:
label: What is it for?
description: Which club project, event, or service does this serve?
validations:
required: true
28 changes: 28 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
## What does this change?

<!-- One line. For example: add docs.witcc.dev for the club handbook. -->

## Who owns the records?

<!--
Every record needs an owner in a comment on the same line as its name, so we
know who to ask when it breaks. For example:

docs: # mayonej@wit.edu
- ttl: 600
type: CNAME
value: docs-site.netlify.app.
-->

## Checklist

- [ ] The record is for a club project, event, or service.
- [ ] Every record I added or changed has an owner in a comment.
- [ ] `./bin/validate` passes, so the records are in the order octoDNS wants.
- [ ] CNAME values end with a dot. A and AAAA values do not.
- [ ] I have read the `octoDNS plan` comment on this pull request and it
changes only what I expected.

## Anything else a reviewer should know?

<!-- Delete this section if there is nothing. -->
13 changes: 13 additions & 0 deletions .github/dns-reviewers.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Round robin review rotation for DNS pull requests.
#
# One GitHub username per line. Lines that start with # are ignored.
#
# Everybody here must be a member of the @WITCodingClub/dns-managers team,
# because CODEOWNERS requires an approval from that team. This file only
# decides whose turn it is to be asked. Anybody on the team can still approve.
#
# To hand the rotation to somebody new, add their username and open a pull
# request. To pause somebody, comment their line out.

jaspermayone
Cattn
96 changes: 96 additions & 0 deletions .github/workflows/assign-reviewer.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
name: assign-reviewer

# Picks the next person in the review rotation and asks them to review.
#
# GitHub can do this by itself with team review assignment, but that feature
# needs a paid organisation plan. This is the same idea in a workflow: the
# rotation is .github/dns-reviewers.txt and the turn is decided by the pull
# request number, so it moves on by one with every pull request.
#
# This workflow never checks out or runs code from the pull request, which is
# why `pull_request_target` is safe here.

on:
pull_request_target:
types: [opened, ready_for_review]

permissions:
contents: read

jobs:
assign:
name: pick the next reviewer
runs-on: ubuntu-latest
if: github.event.pull_request.draft == false
permissions:
contents: read
pull-requests: write
env:
# CODEOWNERS asks the whole team for a review. Set this to false if you
# would rather leave that request in place and only add the individual.
REMOVE_TEAM_REQUEST: 'true'
TEAM_SLUG: dns-managers
steps:
- name: Check out main (for the rotation file)
uses: actions/checkout@v7
with:
ref: ${{ github.event.pull_request.base.ref }}
sparse-checkout: .github/dns-reviewers.txt
sparse-checkout-cone-mode: false

- name: Request a review
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
PR: ${{ github.event.pull_request.number }}
AUTHOR: ${{ github.event.pull_request.user.login }}
run: |
set -eu

rotation=$(grep -vE '^\s*(#|$)' .github/dns-reviewers.txt | tr -d ' \r')
count=$(printf '%s\n' "$rotation" | grep -c . || true)
if [ "$count" -eq 0 ]; then
echo "::warning title=No reviewers::.github/dns-reviewers.txt is empty."
exit 0
fi

# Already has somebody on it, so leave it alone. This keeps a reopen
# or a "ready for review" from piling on more reviewers.
existing=$(gh pr view "$PR" --repo "$REPO" \
--json reviewRequests --jq '.reviewRequests | map(.login // empty) | length')
if [ "$existing" -gt 0 ]; then
echo "A reviewer is already requested. Nothing to do."
exit 0
fi

# Start at the pull request number and walk forward, so consecutive
# pull requests go to consecutive people, and skip the author.
reviewer=''
i=0
while [ "$i" -lt "$count" ]; do
pick=$(( (PR + i) % count + 1 ))
candidate=$(printf '%s\n' "$rotation" | sed -n "${pick}p")
if [ "$candidate" != "$AUTHOR" ]; then
reviewer="$candidate"
break
fi
i=$(( i + 1 ))
done

if [ -z "$reviewer" ]; then
echo "::notice title=No reviewer::$AUTHOR is the only person in the rotation."
exit 0
fi

echo "Asking $reviewer to review #$PR."
gh pr edit "$PR" --repo "$REPO" \
--add-reviewer "$reviewer" \
--add-assignee "$reviewer"

if [ "$REMOVE_TEAM_REQUEST" = 'true' ]; then
# CODEOWNERS still requires an approval from the team. Dropping the
# team's review request only stops everybody being notified.
gh api -X DELETE "repos/$REPO/pulls/$PR/requested_reviewers" \
-f "team_reviewers[]=$TEAM_SLUG" >/dev/null 2>&1 \
|| echo "The team was not requested, or could not be removed."
fi
22 changes: 0 additions & 22 deletions .github/workflows/deploy.yaml

This file was deleted.

100 changes: 100 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
name: deploy

# Applies the zone files to Cloudflare. This is the only workflow that holds a
# Cloudflare token with write access.

on:
push:
branches: [main]
paths:
- '*.yaml'
- 'config/**'
- 'bin/**'
- 'requirements.txt'
- '.github/workflows/deploy.yml'
workflow_dispatch:
inputs:
allow_mass_delete:
description: 'Apply even when the plan deletes more records than the limit'
type: boolean
default: false

permissions:
contents: read

env:
# octoDNS refuses a plan that deletes more than 30% of a zone, but only for a
# zone that already has at least 10 records. MIN_EXISTING_RECORDS is a
# constant in octoDNS and cannot be configured. hackwit.org has fewer records
# than that, so octoDNS would delete every one of them without complaining.
# This is the guard for that case.
MAX_DELETES: '3'

# Never let two deploys apply to Cloudflare at the same time, and never cancel
# one halfway through.
concurrency:
group: deploy-cloudflare
cancel-in-progress: false

jobs:
deploy:
name: apply to cloudflare
runs-on: ubuntu-latest
# Add reviewers or a wait timer to this environment in the repository
# settings if you ever want a second pair of eyes between merge and apply.
environment: production
steps:
- uses: actions/checkout@v7

- uses: actions/setup-python@v7
with:
python-version: '3.12'
cache: pip

- name: Install octoDNS
run: pip install -r requirements.txt

- name: Show what will be applied
env:
CLOUDFLARE_TOKEN: ${{ secrets.CLOUDFLARE_TOKEN }}
run: |
set -eu
./bin/plan > plan.md
cat plan.md >> "$GITHUB_STEP_SUMMARY"
cat plan.md

- name: Refuse a mass delete
if: inputs.allow_mass_delete != true
run: |
set -eu
deletes=$(grep -c '^| Delete |' plan.md || true)
echo "The plan deletes $deletes records. The limit is $MAX_DELETES."

if [ "$deletes" -gt "$MAX_DELETES" ]; then
echo "::error title=Too many deletes::The plan deletes $deletes records, and the limit is $MAX_DELETES. Nothing has been applied."
echo
echo "Read the plan in the job summary. If every delete is correct,"
echo "run this workflow by hand with 'allow_mass_delete' turned on:"
echo
echo " gh workflow run deploy.yml -f allow_mass_delete=true"
exit 1
fi

- name: Apply to Cloudflare
env:
CLOUDFLARE_TOKEN: ${{ secrets.CLOUDFLARE_TOKEN }}
run: ./bin/sync

- name: Confirm Cloudflare now matches main
env:
CLOUDFLARE_TOKEN: ${{ secrets.CLOUDFLARE_TOKEN }}
run: |
set -eu
./bin/plan > after.md
if grep -q 'No changes were planned' after.md; then
echo "Cloudflare matches main."
exit 0
fi
echo "::error title=Deploy did not finish::Cloudflare still does not match main after the apply."
cat after.md
exit 1
Loading
Loading