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
129 changes: 129 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
name: CI

# Validation runs on everything. Rendering, committing and publishing are kept
# to main, where the code is already merged.
#
# The `if` on publish is load-bearing, not a formality: a pull request from a
# branch in this repository gets whatever permissions the workflow asks for, so
# without it every such branch would deploy the site and hold a write token
# while still unmerged.
# Fork pull requests are stopped by something else, GitHub forcing their token
# read-only, which no setting on a public repository can undo.

on:
# Pull requests cover branch work, so pushes are only watched on main, where
# a change can land without one. Listing both events unfiltered would run
# everything twice for a pull request opened from a branch in this repo.
push:
branches: [main]
pull_request:
workflow_dispatch:

permissions: {}

defaults:
run:
shell: bash # for -o pipefail, so a failure piped through tee still fails

jobs:
verify:
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
concurrency:
group: verify-${{ github.ref }}
cancel-in-progress: true
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with: {python-version: '3.12'}

- name: Validate the solution
run: python solution/validate_solution.py | tee -a "$GITHUB_STEP_SUMMARY"

publish:
needs: verify
# An allowlist rather than "not a pull request": a trigger added later has
# to be named here before it can reach any of the write permissions below.
if: >-
(github.event_name == 'push' || github.event_name == 'workflow_dispatch')
&& github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
timeout-minutes: 90
permissions:
contents: write
pages: write
id-token: write
# No cancel-in-progress: a deployment should finish rather than be killed
# part way. A later run waits, though only one waits at a time, since a
# third run replaces the pending second.
concurrency:
group: publish
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with: {python-version: '3.12'}
- run: pip install --quiet protobuf

- name: Render the representations
id: render
run: |
python representations/webclient/render.py
python representations/protobuf/render.py

# Deploying here rather than from a separate workflow publishes the files
# this job just rendered. A push made with GITHUB_TOKEN starts no further
# run, so a deploy triggered by the commit below would never happen.
- uses: actions/configure-pages@v5
- name: Assemble the site
run: |
# index.html links to "explanation" and the explanation pages link
# back with "../", so the two are served one level apart.
mkdir _site
cp sample.png _site/
cp -r representations/webclient/. _site/
cp -r explanation _site/explanation
# The browser never fetches the scripts that build the page, or the
# layout file they read.
rm -rf _site/render.py _site/spread_graph.py _site/__pycache__ \
_site/positions.txt
- uses: actions/upload-pages-artifact@v3
with:
path: _site
- id: deployment
uses: actions/deploy-pages@v4

# Last, so publishing never waits on the push. Conditioned on the render
# rather than on the steps above, so a Pages failure does not throw the
# rendered files away, and a half-written render is never committed.
- name: Commit whatever changed
if: ${{ !cancelled() && steps.render.outcome == 'success' }}
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
# Staged before the check, so a newly generated file counts too.
git add representations/
if git diff --cached --quiet; then
echo "artifacts already up to date"
exit 0
fi
git commit -m "Rebuild representations"
# A commit landing on main while this ran rejects the push. That
# commit's own run renders the same files, so the next run fixes it.
git push

# Deliberately not conditioned on the step above: if that step stops
# running, through a renamed id or an edited condition, it skips in
# silence and the render is thrown away on a green run.
- name: Fail if the render was left uncommitted
if: ${{ !cancelled() }}
run: |
if [ -n "$(git status --porcelain -- representations/)" ]; then
git status --short -- representations/
echo "::error::the render changed files that were not committed"
exit 1
fi
52 changes: 0 additions & 52 deletions .github/workflows/pages.yml

This file was deleted.

72 changes: 0 additions & 72 deletions .github/workflows/rebuild.yml

This file was deleted.

35 changes: 0 additions & 35 deletions .github/workflows/verify.yml

This file was deleted.

8 changes: 4 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,10 @@ python solution/print_statistics.py

## What CI does

Every push and pull request runs the whole-solution check, and the result
appears in the **Summary** panel of the run, linked from the Checks tab. After
a change lands on `main`, a second workflow re-renders the representations and
commits whatever changed, and a third publishes the site.
Every pull request runs the whole-solution check, and the result appears in the
**Summary** panel of the run, linked from the Checks tab. Once a change is on
`main`, the same workflow re-renders the representations, publishes the site,
and commits whatever the render changed.

A first-time contribution needs a maintainer to approve the run, so an empty
Checks tab at first is normal.