Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
6024d02
Re:Builder: keep unique illustrations, fix collection and digital own…
Yutsa Oct 6, 2026
4863fae
Re:Builder: number alt-art copies 1, 2, 3, one unnumbered marker for …
Yutsa Oct 6, 2026
3132752
Re:Builder: check several effect values in a row, without reopening a…
Yutsa Oct 6, 2026
74cb76f
Re:Builder: larger Uniques on very wide windows, 7 per row at most (#…
Yutsa Oct 6, 2026
3bee2b6
Re:Builder: the whole deck at a glance from 768 px, read and edit (#135)
Yutsa Oct 6, 2026
86141f8
Re:Builder: check several cards or subtypes in a row, without reopeni…
Yutsa Oct 6, 2026
7d501f1
Re:Builder: rarity caps in the deck bar, « Mes decks » instead of « T…
Yutsa Oct 6, 2026
8cc12ef
Stack: the collection mock answers like the collection API (cardType,…
Yutsa Oct 6, 2026
f472adf
Re:Builder: phones open « Mes decks » in the editor, « ⋯ » menu in it…
Yutsa Oct 7, 2026
5775024
CI: e2e reads the stack images from preprod's cache, mobile replays o…
Yutsa Oct 7, 2026
ac34381
Re:Builder: loading skeletons, never an empty page or an empty deck o…
Yutsa Oct 7, 2026
2b7b0f4
Re:Builder: the hero portrait opens « Choisir un héros » (#142)
Yutsa Oct 7, 2026
a24d883
Re:Builder: phones keep the hero and the rarity caps in the editor's …
Yutsa Oct 7, 2026
df71cbc
Re:Builder: faster on a slow network (editor usable in 8 s instead of…
Yutsa Oct 7, 2026
8f66f19
Re:Builder: a deck opens at the top of its page; « Propriété numériqu…
Yutsa Oct 7, 2026
99f1b20
Re:Builder e2e: back from a deck puts the decks list where it was, ph…
Yutsa Oct 7, 2026
272c7eb
Scroll offset under the sticky site header and the Re:Builder app bar…
Yutsa Oct 7, 2026
bd3b36e
Re:Builder: the account tabs show the Uniques with their stats and ef…
Yutsa Oct 7, 2026
8f74bb5
Re:Builder: the card window fits a phone screen with its browser bars…
Yutsa Oct 8, 2026
1615969
SPA pages: no empty frame before the skeleton or between the skeleton…
Yutsa Oct 8, 2026
a29823d
Default alt arts: one setting for the defaults, final prints per deck…
Yutsa Oct 8, 2026
549739e
Re:Builder loading: skeleton to final state without flicker or shift …
Yutsa Oct 8, 2026
2ce3c0c
Re:Builder: no layout shift while a screen loads, at every screen siz…
Yutsa Oct 8, 2026
3066116
Re:Builder: no « Arts alternatifs » switch in the card search
Yutsa Oct 8, 2026
c0e3772
Re:Builder: the brush shows each copy's default faded, places by touc…
Yutsa Oct 8, 2026
94d5872
Remove a stray Playwright run file
Yutsa Oct 8, 2026
fd053b0
Re:Builder: « Regrouper les illustrations » puts a card's prints on o…
Yutsa Oct 8, 2026
f2a654a
Re:Builder brush: the default alt arts show as chosen; a touched copy…
Yutsa Oct 8, 2026
d0dfa47
Re:Builder: the save status before « Regrouper les illustrations », s…
Yutsa Oct 8, 2026
743b182
CI: check only the plugins a pull request changes; CLS over budget as…
Yutsa Oct 9, 2026
765722e
Re:Builder brush: the illustrations with their 1st, 2nd and 3rd choic…
Yutsa Oct 9, 2026
fafdfad
Re:Builder brush: « Ce que le deck utilise » shows the copies and its…
Yutsa Oct 9, 2026
bd24ef4
Re:Builder editor at 1366 px: three columns of cards, a deck tab that…
Yutsa Oct 9, 2026
4be8c2a
Re:Builder brush: the illustration of each card (1ère, 2ème, 3ème car…
Yutsa Oct 9, 2026
f0e4308
Re:Builder brush: the illustrations in the order of their cards, an a…
Yutsa Oct 10, 2026
2ae1879
Re:Builder brush: two steps, the illustrations on a band ending in a …
Yutsa Oct 10, 2026
9d2b6eb
Re:Builder brush: a late family answer keeps the defaults saved since…
Yutsa Oct 10, 2026
dd76238
Re:Builder brush: the cards' illustrations are this deck's only, kept…
Yutsa Oct 10, 2026
672c57e
Brush choices: a deleted deck loses them; no row for a card whose thr…
Yutsa Oct 10, 2026
a0ec13f
Re:Builder brush: the illustrations keep the service's order; a choic…
Yutsa Oct 10, 2026
bca7b26
Re:Builder brush: each card's ribbon in its own place, 1st on the lef…
Yutsa Oct 10, 2026
4748835
Simplify the brush: one deck-delete helper, cleanup by player and dec…
Yutsa Oct 10, 2026
9c8b423
Re:Builder brush: a choice that cannot be saved puts the copies back …
Yutsa Oct 10, 2026
170ed3e
fix booster opening
julesduroux Oct 11, 2026
d56ddbc
Keep the « Global » alt-art mode; leaving it writes the defaults in e…
Yutsa Oct 13, 2026
b797208
Alt-art modes: review and simplify
Yutsa Oct 13, 2026
38151e1
Re:Builder brush: three keys above each illustration instead of ribbo…
Yutsa Oct 13, 2026
2f14105
Default alt arts page: one row per card, its cards fanned out, keys a…
Yutsa Oct 13, 2026
68e0946
Alt-art keys: review fixes
Yutsa Oct 13, 2026
fb69b8b
Default alt arts e2e: Fée Clochette (CORE and MUSUBI prints differ)
Yutsa Oct 13, 2026
98e1450
Default alt arts page: full-size keys, wider illustrations, two a row…
Yutsa Oct 13, 2026
ac58173
Ownership mock: the alt-art search lists every card type, as the service
Yutsa Oct 13, 2026
bf46365
Default alt arts page: larger previews, a card opens large on a click
Yutsa Oct 13, 2026
bc1e9db
Keys: a key that can be pressed at full strength, a disabled key flat…
Yutsa Oct 13, 2026
988522a
Default alt arts page: the choices on the left, what decks take on th…
Yutsa Oct 14, 2026
f530f5a
Default alt arts page: the choices' band ends in a chevron into what …
Yutsa Oct 14, 2026
ae79232
Default alt arts page: a card's illustrations on one line that scroll…
Yutsa Oct 14, 2026
85181ea
Default alt arts page: the illustration cut at an edge fades out
Yutsa Oct 14, 2026
01b0b8d
Leaving « Global »: list the decks once, the decks API ignores the page
Yutsa Oct 14, 2026
79814a1
Alt arts in « Global »: one apply-to-deck line a family
Yutsa Oct 14, 2026
62a3cfd
Default alt arts page: on a phone, what decks take is centred under t…
Yutsa Oct 14, 2026
1910a84
Discover page: new /pages/discover, reachable by direct URL only
gjuillot Oct 18, 2026
21d58b2
Merge pull request #157 from gjuillot/discover-page
julesduroux Oct 20, 2026
c2f861b
hidden-creature plugin
julesduroux Oct 20, 2026
07997b1
Merge chore/php-8.4 (PHP 8.4.27 migration) into preprod
alexandrewavelet Oct 21, 2026
ed32d04
CI: lint and test under PHP 7.4 and 8.4 until preprod has switched
alexandrewavelet Oct 21, 2026
e89a9be
Merge chore/php-8.4 (e2e stack fix) into chore/php-8.4-preprod
alexandrewavelet Oct 21, 2026
3f66f81
Merge pull request #159 from Altered-Community/chore/php-8.4-preprod
alexandrewavelet Oct 21, 2026
ef3e3e5
CI: drop the PHP 7.4 lint now that preprod runs 8.4.27
alexandrewavelet Oct 22, 2026
b73255d
Merge origin/preprod into feat/alt-art-defaults
Yutsa Oct 22, 2026
eed52a9
Leaving « Global »: best effort, a deck that cannot be written no lon…
Yutsa Oct 22, 2026
d69bd5d
Never show the signed-in user's e-mail address in full
Yutsa Oct 22, 2026
0d06ed9
Re:Builder Aperçu: a skeleton for « Regrouper les illustrations » too…
Yutsa Oct 22, 2026
3792986
E2E evidence of the masked address (homepage, account page and menu)
Yutsa Oct 22, 2026
4c2b415
Merge pull request #161 from Altered-Community/fix/newsletter-email-leak
julesduroux Oct 22, 2026
98f6d8c
Merge pull request #160 from Altered-Community/chore/drop-php-7.4-ci
julesduroux Oct 22, 2026
4fa6646
Merge pull request #153 from Altered-Community/feat/alt-art-defaults
julesduroux Oct 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
203 changes: 187 additions & 16 deletions .github/workflows/plugins-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@ name: Plugins CI
# • each plugin's tests, found through bin/plugins.php (plugin.json "tests" / "build",
# or the conventions tests/run.php, **/*.test.mjs, e2e/*.spec.ts);
# • front-end plugins ("build" in plugin.json): install, lint, unit tests, build;
# • Playwright (tests/e2e + plugins/*/e2e) against the full stack (docker-compose.stack.yml).
# • Playwright (tests/e2e + plugins/*/e2e) against the full stack (docker-compose.stack.yml);
# • a CLS over budget in the loading specs does not fail: the job "CLS warnings" posts it, with a screenshot, as a
# pull request comment.
# Nothing here is plugin-specific: a new plugin is picked up from its manifest.
# A pull request that only changes files under plugins/<id>/ checks only those plugins (their PHP, Node, front-end and
# Playwright tests); a change anywhere else (core, design system, tests/e2e, Docker, this workflow…) checks them all.
# Make the "Plugins CI" check required in the branch protection of preprod and main.
on:
pull_request:
Expand All @@ -25,26 +29,71 @@ jobs:
name: Discover plugins
runs-on: ubuntu-latest
outputs:
ids: ${{ steps.plan.outputs.ids }}
frontends: ${{ steps.plan.outputs.frontends }}
all_frontends: ${{ steps.plan.outputs.all_frontends }}
e2e: ${{ steps.plan.outputs.e2e }}
e2e_specs: ${{ steps.plan.outputs.e2e_specs }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2 # the pull request's merge commit and its first parent, the base branch
- uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
coverage: none
- id: plan
env:
EVENT: ${{ github.event_name }}
run: |
php bin/plugins.php plan | tee plan.json
echo "frontends=$(jq -c '[.[] | select(.build != null) | {id, dir, workdir: .build.workdir, output: .build.output, nvmrc: (.build.nvmrc // "")}]' plan.json)" >> "$GITHUB_OUTPUT"
# Files changed by the pull request; null (check every plugin) on a manual run.
if [ "$EVENT" = pull_request ]; then
git diff --name-only HEAD^1 HEAD | tee changed.txt
changed="$(jq -R . changed.txt | jq -sc .)"
else
changed=null
fi
# A file under plugins/<id>/ belongs to that plugin; any other file is shared and affects every plugin.
jq --argjson changed "$changed" '
[.[].id] as $ids
| def owner: (capture("^plugins/(?<id>[^/]+)/").id // null) | select(. != null and IN($ids[]));
($changed == null or any($changed[]; [owner] == [])) as $shared
| (if $shared then $ids else [$changed[] | owner] | unique end) as $affected
| {shared: $shared, plan: [.[] | .checked = (.id | IN($affected[]))]}
' plan.json > affected.json
# Ids and e2e folders come from the manifests and end up as command arguments: allow only safe characters.
jq -e '[.plan[] | .id, (.e2e // "e2e")] | all(test("^[A-Za-z0-9_./-]+$"))' affected.json > /dev/null \
|| { echo "::error::A plugin id or e2e folder contains characters other than A-Z a-z 0-9 _ . / -"; exit 1; }
shared="$(jq '.shared' affected.json)"
frontend='{id, dir, workdir: .build.workdir, output: .build.output, nvmrc: (.build.nvmrc // "")}'
{
echo "ids=$(jq -r '[.plan[] | select(.checked) | .id] | join(" ")' affected.json)"
echo "frontends=$(jq -c "[.plan[] | select(.checked and .build != null) | $frontend]" affected.json)"
echo "all_frontends=$(jq -c "[.plan[] | select(.build != null) | $frontend]" affected.json)"
if [ "$shared" = true ]; then
# Shell specs (tests/e2e) and every plugin's specs: no filter.
echo "e2e=true"
echo "e2e_specs="
else
specs="$(jq -r '[.plan[] | select(.checked and .e2e != null) | "\(.dir)/\(.e2e)/"] | join(" ")' affected.json)"
echo "e2e=$([ -n "$specs" ] && echo true || echo false)"
echo "e2e_specs=$specs"
fi
} | tee -a "$GITHUB_OUTPUT"
{
echo '### Plugins'
echo '| Plugin | PHP tests | Node tests | Front-end build | E2E |'
echo '|---|---|---|---|---|'
jq -r '.[] | "| \(.id) | \(.php_tests | length) | \(.node_tests | length) | \(if .build then "yes" else "—" end) | \(.e2e // "—") |"' plan.json
if [ "$shared" = true ]; then echo 'Shared files changed (or manual run): every plugin is checked.'
else echo 'Only plugin files changed: only those plugins are checked.'; fi
echo
echo '| Plugin | Checked | PHP tests | Node tests | Front-end build | E2E |'
echo '|---|---|---|---|---|---|'
jq -r '.plan[] | "| \(.id) | \(if .checked then "yes" else "—" end) | \(.php_tests | length) | \(.node_tests | length) | \(if .build then "yes" else "—" end) | \(.e2e // "—") |"' affected.json
} >> "$GITHUB_STEP_SUMMARY"

php:
name: PHP lint and plugin PHP tests
needs: plan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand All @@ -60,10 +109,17 @@ jobs:
- name: Core PHP tests (helpers, design system)
run: php tests/run.php
- name: Plugin PHP tests
run: php bin/plugins.php run php-tests
if: needs.plan.outputs.ids != ''
env:
IDS: ${{ needs.plan.outputs.ids }}
run: |
# shellcheck disable=SC2086 # one argument per id
php bin/plugins.php run php-tests $IDS

node:
name: Plugin Node tests
needs: plan
if: needs.plan.outputs.ids != ''
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand All @@ -74,7 +130,11 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: '22'
- run: php bin/plugins.php run node-tests
- env:
IDS: ${{ needs.plan.outputs.ids }}
run: |
# shellcheck disable=SC2086 # one argument per id
php bin/plugins.php run node-tests $IDS

frontend:
name: Front-end ${{ matrix.plugin.id }}
Expand Down Expand Up @@ -110,11 +170,13 @@ jobs:
name: Playwright (shell and plugins)
# Does not wait for the front-end jobs: it builds the bundles itself, while the stack starts.
needs: [plan, php]
if: needs.plan.outputs.e2e == 'true'
runs-on: ubuntu-latest
timeout-minutes: 40
env:
E2E_BASE_URL: http://localhost:8080
E2E_EVIDENCE_DIR: ${{ github.workspace }}/tests/e2e/evidence
E2E_CLS_WARNINGS_DIR: ${{ github.workspace }}/tests/e2e/cls-warnings
COMPOSE: docker compose -f docker-compose.yml -f docker-compose.stack.yml
steps:
- uses: actions/checkout@v4
Expand All @@ -137,8 +199,10 @@ jobs:
tests/e2e/package-lock.json
- name: Build front-end plugins (background)
# The web container mounts the checkout: the bundles only have to exist before the tests.
# Every front-end, not only the checked ones: a plugin's specs can go through another plugin's pages
# (core-altered-cards creates its decks through Re:Builder's relay).
env:
FRONTENDS: ${{ needs.plan.outputs.frontends }}
FRONTENDS: ${{ needs.plan.outputs.all_frontends }}
run: |
cat > "$RUNNER_TEMP/frontends.sh" <<'EOF'
set -euo pipefail
Expand All @@ -151,20 +215,20 @@ jobs:
nohup bash -c 'bash "$RUNNER_TEMP/frontends.sh"; echo $? > "$RUNNER_TEMP/frontends.exit"' > "$RUNNER_TEMP/frontends.log" 2>&1 &
- uses: docker/setup-buildx-action@v3
- name: Build the stack images
# From the GitHub Actions cache (one scope per image): rebuilt only when the Dockerfile or
# the pinned decks API commit changes.
# From preprod's GitHub Actions cache (one scope per image, written by stack-images.yml): rebuilt only when
# the pull request changes the Dockerfile or the pinned decks API commit. Read only: a cache written here
# would be scoped to this pull request, unreadable by the others, and would fill the repository's 10 GB.
uses: docker/bake-action@v6
with:
source: .
files: |
docker-compose.yml
docker-compose.stack.yml
targets: web,decks-api
load: true
set: |
web.cache-from=type=gha,scope=stack-web
web.cache-to=type=gha,scope=stack-web,mode=max
decks-api.cache-from=type=gha,scope=stack-decks-api
decks-api.cache-to=type=gha,scope=stack-decks-api,mode=max
- name: Start the stack
run: $COMPOSE up -d --no-build --wait --wait-timeout 600
- name: Wait for the front-end builds
Expand All @@ -176,8 +240,13 @@ jobs:
working-directory: tests/e2e
run: npm ci && npx playwright install --with-deps chromium
- name: Run Playwright
# Every spec when shared files changed, otherwise only the specs of the changed plugins.
working-directory: tests/e2e
run: npx playwright test
env:
SPECS: ${{ needs.plan.outputs.e2e_specs }}
run: |
# shellcheck disable=SC2086 # one argument per folder
npx playwright test $SPECS
- name: Stack logs
if: failure()
run: $COMPOSE logs --no-color --tail=200
Expand All @@ -190,6 +259,105 @@ jobs:
tests/e2e/evidence
tests/e2e/test-results
retention-days: 14
- uses: actions/upload-artifact@v4
if: always()
with:
name: cls-warnings
path: tests/e2e/cls-warnings
if-no-files-found: ignore
retention-days: 14

cls:
# CLS over budget (tests/e2e/loading.ts, clsWarning): a warning on the run and one pull request comment, kept up to
# date, with the screenshots (pushed to the pr-screenshots branch). Runs no code of the pull request: it reads the
# artifact as data. Not a dependency of "Plugins CI": it never makes the pull request red.
name: CLS warnings
needs: e2e
if: always() && github.event_name == 'pull_request' && (needs.e2e.result == 'success' || needs.e2e.result == 'failure')
runs-on: ubuntu-latest
permissions:
contents: write # the screenshots, on the pr-screenshots branch
pull-requests: write # the comment
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
PR: ${{ github.event.pull_request.number }}
# A fork's token is read only: warnings on the run, no screenshots and no comment.
CAN_WRITE: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
SHOTS: ${{ github.event.pull_request.number }}/cls-${{ github.run_id }}-${{ github.run_attempt }}
steps:
- uses: actions/download-artifact@v4
continue-on-error: true # no artifact: no CLS over budget
with:
name: cls-warnings
path: cls-warnings
- name: List the warnings
id: list
run: |
# Only <slug>.json with a safe slug: the artifact comes from the pull request's code.
slugs=()
for f in cls-warnings/*.json; do
[ -f "$f" ] || continue
slug="$(basename "$f" .json)"
[[ "$slug" =~ ^[a-z0-9-]+$ ]] && slugs+=("$slug")
done
echo "slugs=${slugs[*]}" >> "$GITHUB_OUTPUT"
for slug in "${slugs[@]}"; do
jq -r '"::warning title=CLS over budget::\(.project) · \(.title): CLS \(.cls) > \(.budget)"' "cls-warnings/$slug.json"
done
- uses: actions/checkout@v4
if: steps.list.outputs.slugs != '' && env.CAN_WRITE == 'true'
with:
ref: pr-screenshots
path: shots
- name: Push the screenshots
if: steps.list.outputs.slugs != '' && env.CAN_WRITE == 'true'
env:
SLUGS: ${{ steps.list.outputs.slugs }}
working-directory: shots
run: |
mkdir -p "$SHOTS"
for slug in $SLUGS; do [ -f "../cls-warnings/$slug.png" ] && cp "../cls-warnings/$slug.png" "$SHOTS/"; done
git config user.name 'github-actions[bot]'
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
git add "$SHOTS"
git commit -qm "CLS warnings of #$PR (run $GITHUB_RUN_ID)"
for _ in 1 2 3; do git push -q origin HEAD:pr-screenshots && exit 0; git pull -q --rebase origin pr-screenshots; done
exit 1
- name: Comment on the pull request
if: env.CAN_WRITE == 'true'
env:
SLUGS: ${{ steps.list.outputs.slugs }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
marker='<!-- plugins-ci:cls-warnings -->'
existing="$(gh api "repos/$REPO/issues/$PR/comments" --paginate --jq ".[] | select(.body | startswith(\"$marker\")) | .id" | head -1)"
if [ -z "$SLUGS" ]; then
# Nothing over budget: update an earlier comment, never add one.
[ -n "$existing" ] || exit 0
body="$marker"$'\n'"### ✅ Loading: no CLS over budget"$'\n\n'"Last run: $RUN_URL"
else
{
echo "$marker"
echo '### ⚠️ Loading: CLS over budget'
echo
echo "Not a failure: the CLS of a screen varies from one CI run to the next. Each screenshot is the settled page, the boxes that moved drawn on it (dashed: before, solid: after). Frame by frame: \`npm run loading:timeline -- <screen>\` in \`tests/e2e\`. Run: $RUN_URL"
for slug in $SLUGS; do
url="https://github.com/$REPO/blob/pr-screenshots/$SHOTS/$slug.png?raw=true"
jq -r --arg url "$url" '
"\n#### \(.project) · \(.title)\n\nCLS **\(.cls)** (budget \(.budget))\n\n<img src=\"\($url)\" width=\"600\">\n\n<details><summary>Shifts</summary>\n\n```\n"
+ ([.shifts[] | "\(.t) ms \(.value)\n" + ([.boxes[] | " " + gsub("`"; "")] | join("\n"))] | join("\n"))
+ "\n```\n</details>"
' "cls-warnings/$slug.json"
done
} > body.md
body="$(cat body.md)"
fi
if [ -n "$existing" ]; then
gh api -X PATCH "repos/$REPO/issues/comments/$existing" -f body="$body" > /dev/null
else
gh api -X POST "repos/$REPO/issues/$PR/comments" -f body="$body" > /dev/null
fi

result:
# Single check to require in branch protection.
Expand All @@ -199,7 +367,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: All jobs passed
env:
NEEDS: ${{ toJSON(needs) }}
run: |
echo '${{ toJSON(needs) }}' | jq -r 'to_entries[] | "\(.key): \(.value.result)"'
echo '${{ toJSON(needs) }}' | jq -e 'all(.[]; .result == "success" or .result == "skipped")' > /dev/null
[ "${{ needs.e2e.result }}" = "success" ]
jq -r 'to_entries[] | "\(.key): \(.value.result)"' <<< "$NEEDS"
jq -e 'all(.[]; .result == "success" or .result == "skipped")' <<< "$NEEDS" > /dev/null
# Playwright may be skipped only when the plan says so (no checked plugin has specs).
jq -e '.e2e.result == "success" or .plan.outputs.e2e == "false"' <<< "$NEEDS" > /dev/null
42 changes: 42 additions & 0 deletions .github/workflows/stack-images.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: Stack images

# Builds the e2e stack images (web, decks-api) on preprod and writes their layers to the GitHub Actions cache.
# Pull requests only read that cache (plugins-ci.yml, job e2e): a cache written by a pull request is scoped to its
# own ref and no other pull request can read it, while every pull request against preprod reads preprod's.
# • push on preprod: a merged Dockerfile or decks API commit is cached for the next pull requests;
# • weekly: GitHub drops caches unused for 7 days, the schedule keeps this one when nothing is merged.
on:
push:
branches: [preprod]
schedule:
- cron: '0 4 * * 1'
workflow_dispatch: {}

concurrency:
group: stack-images
cancel-in-progress: true

permissions:
contents: read

jobs:
cache:
name: Build and cache the stack images
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- name: Build the stack images
# Same targets and scopes as plugins-ci.yml (job e2e).
uses: docker/bake-action@v6
with:
source: .
files: |
docker-compose.yml
docker-compose.stack.yml
targets: web,decks-api
set: |
web.cache-from=type=gha,scope=stack-web
web.cache-to=type=gha,scope=stack-web,mode=max
decks-api.cache-from=type=gha,scope=stack-decks-api
decks-api.cache-to=type=gha,scope=stack-decks-api,mode=max
18 changes: 18 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,21 @@ The rules that matter most:
- PHP tests: `php tests/run.php` (in Docker: `docker compose exec web php tests/run.php`).
- SPA plugins: `plugins/<id>/plugin.json` → `build` (lint, test, build); e2e with Playwright in
`tests/e2e` and `plugins/<id>/e2e` against the full stack (`README.md` § Full stack).
- Loading (skeletons, layout shifts): after any change to what a screen shows while it loads (placeholder, skeletons,
data or images arriving after the first screen, the runtime), in `tests/e2e`:
- `npm run loading`: every Re:Builder screen on a slow phone network, first and second visit, desktop and mobile
(`plugins/rebuilder/e2e/loading.spec.ts`, also run by the CI). It fails on an empty frame, the app shown unstyled,
the skeleton losing the site's font or a « Chargement… » title. A CLS over the screen's budget
(`plugins/rebuilder/e2e/loading-budgets.json`: 0.01, or today's value for a known shift; lower a budget when a fix
lands, never raise one to pass) is a warning, not a failure: on the CI, a pull request comment with a screenshot of
the boxes that moved (job « CLS warnings »).
- `npm run loading:timeline -- <screen> [--viewport <size> | --desktop] [--first-visit] [--frames] [--compare <other stack>]`:
the loading frame by frame (empty frames in red, shifts marked) with its report, for the PR's evidence; `--compare`
puts the base branch's stack (before) and this one (after) in one image. Header of `tests/e2e/loading-timeline.ts`
for the options.
- `npm run loading:matrix [-- --screens a,b --viewports a,b --runs n]`: the CLS of every screen at eight sizes (phones,
tablet, laptops, desktops: `VIEWPORTS` in `tests/e2e/loading.ts`), on both sides of the breakpoints, in one table
with what moved. Run it after a layout change that depends on the width (a breakpoint, a container query, a skeleton
whose size is set for one width).
- A new screen or a new loading state: add it to `plugins/rebuilder/e2e/screens.ts`; the recorder
(`tests/e2e/loading.ts`) works for any SPA plugin.
Loading
Loading