Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
4afe4a3
docs(slides): document ownership model and operator commands
yasinmiran Jul 15, 2026
74b9631
feat(slides): add constants and article date parser
yasinmiran Jul 15, 2026
18f8ad1
feat(slides): load article frontmatter across collections
yasinmiran Jul 15, 2026
b54556f
feat(slides): add dependency-free image dimension probe
yasinmiran Jul 15, 2026
8f5696c
feat(slides): rank candidates by recency, lifecycle and editorial weight
yasinmiran Jul 15, 2026
adeb64d
feat(slides): add collect CLI emitting current state and candidate pool
yasinmiran Jul 15, 2026
8b476a6
feat(slides): select slides with pinning, hysteresis and swap cap
yasinmiran Jul 15, 2026
8199430
test(slides): cover hysteresis and swap-cap backfill in selection
yasinmiran Jul 15, 2026
22678d1
feat(slides): add optional caption agent with summary fallback
yasinmiran Jul 15, 2026
cc48e95
feat(slides): apply selection by copying images and pruning stale files
yasinmiran Jul 15, 2026
5c1e507
feat(slides): add hard validation gate and diff-scope guard
yasinmiran Jul 15, 2026
95bc26c
fix(slides): apply image quality gates to bot-created images only
yasinmiran Jul 15, 2026
77617c5
test(slides): assert quality gates still fire on bot-named images
yasinmiran Jul 15, 2026
02f9844
feat(slides): add pipeline orchestrator
yasinmiran Jul 15, 2026
6faa6e0
fix(slides): skip candidates whose cover fails the bot quality gates
yasinmiran Jul 15, 2026
836a2c7
chore(slides): tag existing slides with ownership metadata
yasinmiran Jul 15, 2026
fad6999
ci(slides): add scheduled highlights refresh workflow
yasinmiran Jul 15, 2026
0883280
ci(slides): merge deterministically and report merge failures loudly
yasinmiran Jul 15, 2026
dbc89dc
fix(slides): retain untracked slides and require candidate summaries
yasinmiran Jul 15, 2026
2440049
fix(slides): persist the evergreen stamp and reject dual ownership tags
yasinmiran Jul 28, 2026
eab27be
refactor(slides): share acceptance rules between the gate and its pro…
yasinmiran Jul 28, 2026
01480ff
fix(slides): close the collision, overflow and duplication paths
yasinmiran Jul 28, 2026
88c524b
fix(slides): stop guessing at ambiguous ownership and duplicated refs
yasinmiran Jul 28, 2026
3a87e71
fix(slides): pin the curated composite instead of handing it to the bot
yasinmiran Jul 28, 2026
d67f746
test(slides): cover the incumbent rescue path and halt on a malformed…
yasinmiran Jul 28, 2026
90d3ce3
docs(slides): state the halt-on-ambiguity rule the pipeline now follows
yasinmiran Jul 28, 2026
5301dd0
fix(slides): make the validator actually cover every state the bot ha…
yasinmiran Jul 28, 2026
a955b2a
feat(slides): leave the refresh PR for a human instead of merging it
yasinmiran Jul 29, 2026
b5b6ccf
fix(slides): load the caption agent's rules into the caption agent
yasinmiran Jul 29, 2026
f633755
refactor(slides): collapse the pipeline into one module
yasinmiran Jul 29, 2026
db8dff2
fix(slides): tell the caption model what to write
yasinmiran Jul 29, 2026
4d0424e
docs(slides): fix two comments pointing at deleted files
yasinmiran Aug 5, 2026
6f7e945
fix(slides): name the offending slides on halt and catch untracked st…
yasinmiran Aug 5, 2026
7d4d8ea
fix(slides): stop a transient API failure orphaning the refresh branch
yasinmiran Aug 5, 2026
1b84dd1
fix(slides): stop the caption agent describing images it never sees
yasinmiran Aug 5, 2026
ccd290d
docs(slides): correct why nothing in funding-and-projects can surface
yasinmiran Aug 5, 2026
3ed2d94
fix(cms): pin slides created in the admin editor
yasinmiran Aug 5, 2026
a06d639
fix(slides): five defects found auditing the pipeline
yasinmiran Aug 5, 2026
924537f
fix(slides): parse git status without quoting surprises
yasinmiran Aug 5, 2026
8122e91
fix(slides): reject alt text that claims to describe the picture
yasinmiran Aug 5, 2026
1377725
feat(slides): report why a candidate was refused
yasinmiran Aug 5, 2026
f3a3528
chore(slides): unpin the two time-bound highlights
yasinmiran Aug 5, 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
6 changes: 6 additions & 0 deletions .github/workflows/pr-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ jobs:
- name: Check slug naming conventions
run: node scripts/check-slugs.mjs

- name: Validate homepage slides
run: node scripts/slides/slides.js validate

- name: Slides pipeline unit tests
run: node --test scripts/slides/slides.test.js

- name: Build (static output)
run: pnpm build
env:
Expand Down
143 changes: 143 additions & 0 deletions .github/workflows/refresh-highlights.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
name: Refresh Highlights

on:
schedule:
- cron: '0 7 * * 1' # Mon ~08:00 Europe/Oslo (UTC; ±1h DST drift)
- cron: '0 15 * * 5' # Fri ~16:00 Europe/Oslo
workflow_dispatch:

concurrency:
group: refresh-highlights
cancel-in-progress: false

permissions:
contents: write
pull-requests: write
issues: write

jobs:
refresh:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6

- name: Setup pnpm
uses: pnpm/action-setup@v4

- name: Setup Node
uses: actions/setup-node@v6
with:
node-version-file: .nvmrc
cache: pnpm

- name: Install dependencies
run: pnpm install

- name: Install OpenCode (pinned; agent is optional)
continue-on-error: true
run: npm install -g opencode-ai@0.5.29

- name: Run refresh pipeline
id: refresh
continue-on-error: true
env:
# Optional: set repo secrets to enable the caption agent. Absent → the
# pipeline uses summary-derived captions (fully functional).
SLIDES_AGENT_MODEL: ${{ vars.SLIDES_AGENT_MODEL }}
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
run: node scripts/slides/slides.js refresh --diff-scope

- name: Build sanity gate
id: build
if: steps.refresh.outcome == 'success' && steps.refresh.outputs.result == 'changed'
continue-on-error: true
run: pnpm build
env:
GITHUB_PAGES: true

- name: Open PR
id: pr
if: steps.refresh.outputs.result == 'changed' && steps.build.outcome == 'success'
continue-on-error: true
env:
# A PAT makes pr-test.yml run on the PR; PRs opened with the default
# GITHUB_TOKEN get no checks. Validation and pnpm build above already
# gated this branch either way.
GH_TOKEN: ${{ secrets.SLIDES_BOT_TOKEN || github.token }}
run: |
set -euo pipefail
BRANCH="bot/slides-refresh-${{ github.run_id }}"
git config user.name "elixir-no-bot"
git config user.email "actions@github.com"
git checkout -b "$BRANCH"
git add src/data/slides.json src/data/slides
git commit -m "chore(slides): refresh homepage highlights"

# Read before creating, or the new PR supersedes itself. This runs
# before the push because a bare assignment from a command
# substitution carries its exit status: if the list call fails,
# set -e ends the step here, and there is no pushed branch to orphan.
SUPERSEDED=$(gh pr list --state open --base main --limit 100 --json number,headRefName \
--jq '.[] | select(.headRefName | startswith("bot/slides-refresh-")) | .number')

git push origin "$BRANCH"

cat > "$RUNNER_TEMP/pr-body.md" <<'EOF'
Automated highlights refresh, open for review.

Slide selection and captions were regenerated from recent content. The
pipeline validated the result after writing it, and `pnpm build` passed,
both in the workflow run that opened this.

Merge it or close it, but do not leave it sitting. The branch holds a
whole `slides.json` written against main as of this run, so the next
run replaces this PR rather than rebasing it.
EOF

if ! PR_URL=$(gh pr create --base main --head "$BRANCH" \
--title "chore(slides): refresh homepage highlights" \
--body-file "$RUNNER_TEMP/pr-body.md"); then
git push origin --delete "$BRANCH" || true
exit 1
fi

# One open refresh at a time. Each run recomputes the carousel from
# main as it stands now, so an older PR is not a smaller change, it is
# a different answer to the same question; merging both can put back a
# slide the newer run deliberately dropped. Closed PRs keep a Restore
# branch button, so nothing is lost.
# A close that fails must not turn this run red: the new PR is already
# open and mergeable, and someone merging the old one by hand in the
# seconds since the list above is a normal way to get here.
for N in $SUPERSEDED; do
gh pr close "$N" --delete-branch \
--comment "Superseded by ${PR_URL}, computed from the current main." \
|| echo "::warning::could not close superseded PR #${N}; close it by hand"
done

- name: Report failure
# failure() catches the steps above these three: a broken `pnpm install`
# stops the carousel refreshing just as effectively as a broken pipeline,
# and would otherwise only show up as a red run nobody is watching.
if: failure() || steps.refresh.outcome == 'failure' || steps.build.outcome == 'failure' || steps.pr.outcome == 'failure'
env:
GH_TOKEN: ${{ github.token }}
# gh reads the repo from the git remote, and a failure before checkout
# leaves no remote to read.
GH_REPO: ${{ github.repository }}
run: |
set -euo pipefail
TITLE="Highlights refresh failed"
BODY="The scheduled highlights refresh failed on run ${{ github.run_id }}. See the [workflow logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})."
EXISTING=$(gh issue list --label slides-bot --state open --json number --jq '.[0].number // empty' || echo "")
if [ -n "$EXISTING" ]; then
gh issue comment "$EXISTING" --body "$BODY"
else
gh label create slides-bot --color BFD4F2 --description "Automated highlights refresh" 2>/dev/null || true
gh issue create --title "$TITLE" --label slides-bot --body "$BODY"
fi

- name: Fail job if pipeline, build, or PR failed
if: steps.refresh.outcome == 'failure' || steps.build.outcome == 'failure' || steps.pr.outcome == 'failure'
run: exit 1
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@
"astro": "astro",
"postbuild": "pagefind --site dist",
"test:slugs": "node scripts/check-slugs.mjs",
"test:pages": "node scripts/test-pages.mjs"
"test:pages": "node scripts/test-pages.mjs",
"slides:collect": "node scripts/slides/slides.js collect",
"slides:refresh": "node scripts/slides/slides.js refresh",
"slides:validate": "node scripts/slides/slides.js validate",
"slides:test": "node --test scripts/slides/slides.test.js"
},
"dependencies": {
"@astrojs/check": "^0.9.0",
Expand Down Expand Up @@ -46,6 +50,7 @@
"@types/dompurify": "^3.2.0",
"@types/react": "^18.2.37",
"@types/react-dom": "^18.2.15",
"gray-matter": "^4.0.3",
"npm-run-all2": "^9.0.2",
"pagefind": "^1.5.0"
},
Expand Down
Loading
Loading