Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
73db4e2
Open sync pull requests when a consumer's automation.yml drifts
akolson Sep 21, 2026
a4ebeda
Request a pre-review from rtibblesbot on sync pull requests
akolson Sep 21, 2026
06b3ee1
Fix the sync state machine and cover it with tests
akolson Sep 21, 2026
3a7e81d
Fail toward drift when the template history cannot be read
akolson Sep 21, 2026
4cc6deb
Say what the problem-state test guards against
akolson Sep 21, 2026
ecf5a25
Let a consumer supply the whole sync pull request body
akolson Sep 21, 2026
f25c460
Drop the bot pre-review from sync pull requests
akolson Sep 21, 2026
91ad2c1
Correct what the sync states mean and what onboarding needs
akolson Sep 23, 2026
d4714c7
Name the bot app and tidy the sync section
akolson Sep 23, 2026
042cb6d
Drop the redundant comment on the kolibri-design-system entry
akolson Sep 23, 2026
1df6628
Discover the consumers instead of listing them
akolson Sep 23, 2026
82b1c72
Build the pull request body from the consumer's own template
akolson Sep 23, 2026
2a700c6
Treat kolibri-design-system as the one template exception
akolson Sep 23, 2026
2638860
Move the marker comment to the filter that uses it
akolson Sep 23, 2026
c2f51ea
Contain a thrown read during discovery, and trim the kolibri-design-s…
akolson Sep 23, 2026
cfc4adc
Keep the explanation when a kept heading is renamed
akolson Sep 23, 2026
8c5d4e9
Let SYNC_ORG point the sync at another org
akolson Sep 24, 2026
92e2dce
Classify drift by content rather than by commit dates
akolson Sep 24, 2026
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: 46 additions & 0 deletions .github/workflows/sync-automation-template.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: Sync automation template
run-name: Open a pull request in every consumer repo whose automation.yml has drifted
on:
schedule:
- cron: "17 6 * * 1"
workflow_dispatch:
inputs:
dry_run:
description: 'Report drift without opening pull requests'
type: boolean
default: false
push:
branches:
- main
paths:
- automation-template.yml
permissions:
contents: read
concurrency:
group: sync-automation-template
cancel-in-progress: false
jobs:

@rtibblesbot rtibblesbot Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Resolved — addressed in the current code.

nitpick: Two overlapping runs can both try to open the sync PR, and the loser reports a 422. A push to main touching automation-template.yml can start while the weekly cron is still running. Both see no open PR and both POST /pulls.

Please add concurrency: { group: sync-automation-template, cancel-in-progress: false }.

sync:
name: Sync consumers
runs-on: ubuntu-latest
steps:
- name: Generate App Token
id: generate-token
uses: actions/create-github-app-token@v3
with:
app-id: ${{ secrets.LE_BOT_APP_ID }}
private-key: ${{ secrets.LE_BOT_PRIVATE_KEY }}
owner: learningequality
- name: Checkout
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '20'
cache: 'yarn'
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Sync consumers
env:
GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }}
run: node scripts/sync-automation-template.js ${{ inputs.dry_run && '--dry-run' || '' }}
59 changes: 57 additions & 2 deletions docs/automation.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,9 @@ No edits required. Then set the secrets:
| `CONTRIBUTIONS_SHEET_NAME` | no | Sheet name within the spreadsheet |
| `GH_UPLOADER_GCP_SA_CREDENTIALS` | no | GCP service account credentials for Sheets access |

Every automation except `resolve-bot-pr-threads` authenticates as the bot, so the two required
secrets must be set. `resolve-bot-pr-threads` uses the default `GITHUB_TOKEN` instead.
Every automation except `resolve-bot-pr-threads` authenticates as `learning-equality-bot[bot]`, the
GitHub App behind `LE_BOT_APP_ID`, so the two required secrets must be set. `resolve-bot-pr-threads`
uses the default `GITHUB_TOKEN` instead.

Optional means that you accept losing the automations that use the secret. It does not mean that
they degrade gracefully. The generated caller forwards every key, so an absent secret reaches the
Expand All @@ -68,3 +69,57 @@ leaf workflow as an empty string, and every automation that needs it fails at ru
picks up the wider `on:` block the next time they re-copy `automation-template.yml` - existing
copies keep running on their current `on:` block until then, since GitHub workflow triggers are
evaluated from the file checked into the consumer repo itself, not from this repo.

## Keeping the copies in sync

Most registry changes reach consumers automatically because their copied file only says:
`uses: learningequality/.github/.github/workflows/automation.yml@main`. A consumer needs its file
updated whenever its copy no longer matches the template, which happens in two ways.

The template changes here, such as when a new event or activity type is added to the `on:` union,
permissions are widened, or the secret list changes. Or the consumer's own tooling rewrites its
copy, which is reported as a `toolchain-conflict` below.

`.github/workflows/sync-automation-template.yml` handles this. It runs weekly, on manual dispatch,
and whenever `automation-template.yml` changes on `main`.

It discovers the consumers by walking the org's repos, skipping archived ones and forks, and keeping
every repo whose `.github/workflows/automation.yml` calls this repo's `automation.yml`. Each pull
request targets that repo's default branch, which is the only branch GitHub evaluates workflow
triggers from.

For each consumer it compares the copy with the template and opens a pull request when they differ.
A repo that is already in sync gets nothing, while a repo with an existing sync pull request has
that pull request updated rather than a second one opened.

The workflow only proposes changes. It opens pull requests on a branch, never commits to a default
branch, and never merges, approves, or enables auto-merge. A core maintainer in each consumer repo
gives the final review and merges under that repo's own rules.

Run it with `dry_run` to see which repos have drifted without opening any pull requests.

Two results turn the run red and require action:

- `error` means the repo could not be read or written. The app already has the permissions required
for syncing, so this is usually a transient API failure or the app not being installed on that
repo. Check the installation first for a recently added repo.
- `toolchain-conflict` means a sync pull request was merged, the template has not changed since, and
the file has drifted again. The repo's own tooling is rewriting the copy, so the template is not
stable under that toolchain. Fix the template rather than reopening the pull request.

A third result, `declined`, keeps the run green and requires no action. It means a core maintainer
closed the last sync pull request without merging it, so the workflow leaves that repo alone until
the template changes again. The repo remains drifted in the meantime. To restore it sooner, copy the
template in by hand.

To onboard a repo, copy the template in and make sure the `learning-equality-bot[bot]` app is
installed on it. The next run picks it up. A repo on which the app is not installed stays invisible
to the sync, so the installation is what enrols it.

The pull request body is a short explanation of what changed and why the file is generated.

`kolibri-design-system` is the exception. Its `check-description` job fails unless the body carries
a Changelog section whose Description is not the placeholder its own template ships with, and a
plain explanation has no such section. For that repo the sync reads its template and fills each
field, so the body is not a half-filled form. Nothing about that template is stored here, so it
stays current as they change it.
Loading
Loading