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
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