This repository contains the code for the Automations using GitHub Actions used by the The Events Calendar team.
Our goal here is to create Actions that will allow us to deprecate the use of our Internal Jenkins for most of the automation tasks.
Before the creation of this repository most of the automation were done with individual GitHub Actions that were scattered across the different repositories. This made it hard to maintain and to have a clear overview of what was happening.
For more global integrations we used Jenkins, but we want to move away from it and use GitHub Actions for everything.
We are using the BetaHuhn/repo-file-sync-action action to enable us to synchronize files across different repositories.
So any files that need to be identical across different repositories can be synchronized using this action, and the original stored in this repository under the templates folder.
Note
If you are using this action, please make sure to include @tec-bot user as a collaborator with write permissions in the repository you want to sync the files to.
The groups in .github/sync.yml exist because our repositories are not all the
same kind of thing. A WordPress plugin, a shared library and a Laravel service
have genuinely different needs, and syncing a plugin's release machinery into a
service would only give its developers instructions that do not apply.
| Group | Goes to | Holds |
|---|---|---|
| Active Plugin Repositories | the 8 plugin repos and tribe-common |
base config, the PR template, AGENTS.md (each repo keeps a CLAUDE.md symlink to it, seeded by hand and not synced), changelog tooling, release workflows |
| Promoter | promoter |
the PR template and project linking |
| Stack neutral | every active product, 16 repos | the OpenSpec plan check and the actionlint config declaring our Blacksmith runner labels |
.github/actions/wp-test-matrix returns the most recent WordPress X.Y releases
from the wordpress.org version-check API, newest first, as a JSON array (for
example ["7.1","7.0.4","6.9.7"]). Feature pull requests test the latest release
only. The release sanity check pull request (release/* into main) tests the
latest three, so it is the last gate before main. Test workflows feed it into
their matrix:
on: [ pull_request ]
jobs:
wp-versions:
runs-on: blacksmith-4vcpu-ubuntu-2404
outputs:
versions: ${{ steps.matrix.outputs.versions }}
steps:
- id: matrix
uses: the-events-calendar/actions/.github/actions/wp-test-matrix@main
with:
count: ${{ startsWith(github.head_ref, 'release/') && 3 || 1 }}
test:
needs: wp-versions
runs-on: blacksmith-4vcpu-ubuntu-2404
name: ${{ matrix.suite }} (WP ${{ matrix.wp }})
strategy:
fail-fast: false
matrix:
suite: [ unit, wpunit ]
wp: ${{ fromJSON(needs.wp-versions.outputs.versions) }}
steps:
- run: ${SLIC_BIN} wp core update --force --version=${{ matrix.wp }}Because the version under test comes from the matrix, release-update-wp-version.yml
no longer rewrites any wp core update line in the test workflows.
Work on TEC products is planned before it is written, and the plan lives in one
shared store — the-events-calendar/plans —
rather than in the product repositories. A feature routinely spans several repos,
so a spec kept in any one of them is invisible from the others.
templates/workflows/openspec-plan.yml requires a complete, valid active plan for
the ticket a pull request belongs to. It reads the ticket id from the branch name
({type}/{task-id}/{short-desc}), falls back to the PR title, and looks the change
up through .github/actions/verify-openspec-plan. The store is read from a branch
named like the PR branch when one exists there, so a plan still being written can
be checked before it merges. Failing that, an open pull request in the store whose
branch carries the ticket as its {task-id} segment is used, so stacked PRs can share
one plan branch; otherwise the store's default branch is used.
What it reports:
| Situation | Result |
|---|---|
| Active plan has nonempty artifacts and passes strict validation | passes, with a reminder to archive once every repo has merged |
| Plan exists but is already archived | fails — a change must remain active until the last repo merges |
| No plan for that ticket | fails, and the summary shows the command to create one |
| No valid ticket id | fails with guidance to add an ID to the branch, title, or action's ticket-id input |
| Required artifacts are missing, blank, or invalid | fails with artifact or validation diagnostics |
| Store or artifacts cannot be read | fails because the plan could not be verified |
Both the action and the synced workflow default to enforcement: 'block'. An explicit
ticket-id takes precedence over the branch, which takes precedence over the title.
IDs are normalized to lower case. Callers can explicitly select warn for advisory
results; invalid enforcement values always fail. Outputs distinguish validated
(true), missing (false), archived, invalid, and unknown results.
The action itself has no exemptions: given a pull request it always wants a ticket and a plan. The synced workflow decides whether to ask it at all, through a job-level condition, and skips three cases:
| Case | Why |
|---|---|
Author is tec-bot |
Version bumps, changelog moves, POT files, TBD replacement and merge-forwards come off task/* branches that carry no ticket, because there is no change being proposed |
Head branch starts with release/ |
The release pull request into main is the release, not a change to plan |
[skip-openspec] in the pull request body |
The escape hatch for anything else, matching [skip-changelog]. edited is in the workflow's trigger types, so adding or removing it re-runs the check |
A skipped job reports as successful to branch protection, so OpenSpec Plan stays a required check for the pull requests that do run it. Nothing here weakens the requirement for work somebody chose to do: a human pull request off a feature branch still needs its plan.
Check changelog carries the same three clauses, for the same reasons: a release/*
branch's entries were processed into the changelog list before the pull request opened,
and the bot's task/* pull requests add no entry. Three of those — version bump, TBD
replacement and POT generation — never carried a [skip-changelog] marker in their
body, so the author clause is what clears them.
Both bot exemptions are keyed on the login that GHA_BOT_TOKEN_MANAGER authenticates
as; changing that token means changing the condition in both workflows with it.
Validation requires nonempty proposal.md, design.md, tasks.md, and at least
one specs/<capability>/spec.md (nested capability paths are supported). The action
downloads those files at one resolved store commit and runs OpenSpec 1.12.0 with
validate --type change --strict --no-interactive. It uses the built-in
spec-driven schema; plan metadata, custom schemas, and skip_specs cannot bypass
the required artifacts. Task checkboxes can remain unchecked while implementation
is in progress. The CLI validates spec structure; nonempty prose alone does not
prove quality or agreement with code. Reviewers must still confirm the plan
describes the PR.
The action uses Node 24, Python 3, gh, and a pinned OpenSpec installation on the
Ubuntu runner. Plan content is read as data, and telemetry is disabled. Package
installation failures also fail the job.
Changing the action default does not override existing callers that explicitly
select warn. After publishing the shared change, the workflow template must land
in each of the 16 repositories through the existing sync process. To prevent merges
with a failing check, the repository's merge rules must require OpenSpec Plan.
Editing the generated product workflows directly will be overwritten by sync.
The check needs GHA_BOT_TOKEN_MANAGER to have read access to the plans
repository, which is internal.
Install the same validator used in CI, then run the regression suite:
npm install --prefix /tmp/tec-openspec-cli --ignore-scripts --no-audit --no-fund @fission-ai/openspec@1.12.0
PATH="/tmp/tec-openspec-cli/node_modules/.bin:$PATH" python3 tests/test-openspec-plan.pyThe tests execute the actual action scripts and real OpenSpec validator. GitHub
responses are simulated to cover missing plans and API failures without changing
remote repositories. .github/workflows/check-templates.yml runs these tests and
the existing workflow lint and WordPress version arithmetic checks.
The openspec-workflow skill covers
the workflow itself — writing a proposal worth reviewing and keeping it current —
and its TEC extension, tec-openspec, covers the shared store, the ticket-ID
naming, and archiving once (after the last repository merges, not per repo).