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
46 changes: 32 additions & 14 deletions .github/workflows/sync-from-cloudflare.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,18 @@ name: sync-from-cloudflare
# somebody merges a pull request. With it, the change shows up as a reviewable
# diff, keeps its history, and can be given an owner.
#
# The pull request needs DNS_BOT_TOKEN to be checked. GitHub does not start
# workflows for commits pushed with the built-in GITHUB_TOKEN, so without that
# secret the pull request gets no checks and cannot satisfy a required check.
# See docs/runbook.md for how to create it.
# The pull request is opened by a GitHub App, not by a person and not by the
# built-in GITHUB_TOKEN. Both of those matter:
#
# * GitHub does not start workflows for commits pushed with GITHUB_TOKEN, so
# a pull request opened that way gets no checks and can never satisfy a
# required check. An App installation token does start them.
# * Nobody can approve their own pull request. If a person's token opened
# this, that person could never review it, which on a two person team
# leaves exactly one possible reviewer. The App is not a person, so anybody
# on the team can approve.
#
# See docs/runbook.md for how to create the App.

on:
schedule:
Expand All @@ -35,9 +43,20 @@ jobs:
pull-requests: write
issues: write
steps:
- name: Get a token for the bot
id: bot
uses: actions/create-github-app-token@v3
with:
# The client id is an identifier, not a credential, so it lives in a
# repository variable rather than a secret. Keeping it out of the
# secret store also keeps it out of the log masker, which makes a
# failure here readable.
client-id: ${{ vars.DNS_BOT_CLIENT_ID }}
private-key: ${{ secrets.DNS_BOT_PRIVATE_KEY }}

- uses: actions/checkout@v7
with:
token: ${{ secrets.DNS_BOT_TOKEN || secrets.GITHUB_TOKEN }}
token: ${{ steps.bot.outputs.token }}

- uses: actions/setup-python@v7
with:
Expand Down Expand Up @@ -67,8 +86,8 @@ jobs:

- name: Open or update the pull request
env:
GH_TOKEN: ${{ secrets.DNS_BOT_TOKEN || secrets.GITHUB_TOKEN }}
HAS_BOT_TOKEN: ${{ secrets.DNS_BOT_TOKEN != '' }}
GH_TOKEN: ${{ steps.bot.outputs.token }}
APP_SLUG: ${{ steps.bot.outputs.app-slug }}
run: |
set -eu

Expand All @@ -83,8 +102,12 @@ jobs:
--description "Records changed in the Cloudflare dashboard" \
--force

git config user.name 'github-actions[bot]'
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
# Commit as the App. The ruleset has
# require_extra_approval_for_unattributed_changes turned on, so the
# commit author has to be the identity that pushed it.
bot_id=$(gh api "users/$APP_SLUG[bot]" --jq .id)
git config user.name "$APP_SLUG[bot]"
git config user.email "$bot_id+$APP_SLUG[bot]@users.noreply.github.com"
git switch -C cloudflare-sync
git add -- ./*.yaml
git commit -m 'chore: pull manual Cloudflare changes into the zone files'
Expand All @@ -102,11 +125,6 @@ jobs:
printf -- '- If a change should not have been made, revert it in '
printf 'Cloudflare and close this pull request. It reopens tomorrow '
printf 'if the change is still there.\n'
if [ "$HAS_BOT_TOKEN" != 'true' ]; then
printf '\n> **No checks will run on this pull request.** '
printf 'The `DNS_BOT_TOKEN` secret is not set, so GitHub will not '
printf 'start workflows for these commits. See `docs/runbook.md`.\n'
fi
} > body.md

number=$(gh pr list --head cloudflare-sync --state open \
Expand Down
63 changes: 50 additions & 13 deletions docs/runbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,13 @@ lands.
|---|---|---|
| `CLOUDFLARE_TOKEN` | Cloudflare API token, **edit** DNS for both zones | `deploy` |
| `CLOUDFLARE_TOKEN_READ_ONLY` | Cloudflare API token, **read** DNS for both zones | `plan`, `sync-from-cloudflare` |
| `DNS_BOT_TOKEN` | Fine grained personal access token | `sync-from-cloudflare` |
| `DNS_BOT_PRIVATE_KEY` | Private key of the WITCC DNS Bot GitHub App, the whole `.pem` | `sync-from-cloudflare` |

There is also one repository **variable**, not a secret:

| Variable | What it is |
|---|---|
| `DNS_BOT_CLIENT_ID` | Client ID of the same App. An identifier, not a credential |

The two Cloudflare tokens already exist. Create them at
**Cloudflare > My Profile > API Tokens** with the `Edit zone DNS` template, and
Expand All @@ -31,18 +37,49 @@ scope each one to `witcc.dev` and `hackwit.org` only.
> scripts from a pull request while holding it, so anybody who opened a pull
> request could have read it. See [`SECURITY.md`](../SECURITY.md).

`DNS_BOT_TOKEN` is new and you have to create it. GitHub does not start
workflows for commits pushed with the built in `GITHUB_TOKEN`. Without this
token, the nightly sync pull request gets no checks, so it can never satisfy a
required check and can never merge.

1. Go to **Settings > Developer settings > Personal access tokens > Fine
grained tokens** on an account that is a DNS Manager.
2. Resource owner: `WITCodingClub`. Repository access: only `WITCodingClub/dns`.
3. Repository permissions: `Contents: Read and write`,
`Pull requests: Read and write`, `Issues: Read and write`.
4. Set an expiry you will remember. Put a reminder in the club calendar.
5. Save it as the `DNS_BOT_TOKEN` repository secret.
### 1b. The bot App

The nightly sync opens its pull request as a GitHub App. Two reasons, and both
of them rule out the simpler options:

- **Not `GITHUB_TOKEN`.** GitHub does not start workflows for commits pushed
with the built in token, so that pull request would get no checks and could
never satisfy a required check.
- **Not a person's token.** Nobody can approve their own pull request. If the
sync ran on a DNS Manager's personal access token, every sync pull request
would be authored by that person and they could never review it. On a two
person team that leaves exactly one possible reviewer.

An App is neither. It triggers checks, and it is not a person, so anybody on
the team can approve its pull requests. It also has no expiry to forget.

1. Go to
<https://github.com/organizations/WITCodingClub/settings/apps/new>.
2. **Name**: `WITCC DNS Bot`. **Homepage URL**: this repository is fine.
3. Turn **Webhook > Active** off. The App never receives events.
4. **Repository permissions**: `Contents: Read and write`,
`Pull requests: Read and write`, `Issues: Read and write`. Nothing else.
5. **Where can this App be installed**: only this account.
6. Create it, then **Generate a private key**. A `.pem` downloads.
7. **Install App** on the left, install it on `WITCodingClub/dns` only. The App
can do nothing until this step.
8. Record the client id and the key:

```console
$ gh variable set DNS_BOT_CLIENT_ID --repo WITCodingClub/dns --body 'Iv23...'
$ gh secret set DNS_BOT_PRIVATE_KEY --repo WITCodingClub/dns < ~/Downloads/witcc-dns-bot.*.pem
```

The client id is on the App's settings page. The private key is the whole
`.pem` file, `BEGIN` and `END` lines included.
9. Delete the `.pem` from your Downloads folder.

To check that it worked:

```console
$ gh api orgs/WITCodingClub/installations --jq '.installations[].app_slug'
$ gh workflow run sync-from-cloudflare.yml && gh run watch
```

### 2. Team access

Expand Down
Loading