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
163 changes: 162 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ on:
- main
pull_request:
workflow_dispatch:
inputs:
pr-number:
description: Dependabot pull request to verify after a provenance refresh
required: false
type: string
expected-head:
description: Exact refreshed pull request head commit
required: false
type: string

permissions:
contents: read
Expand All @@ -19,22 +28,90 @@ env:
LFC_NPM_VERSION: "12.0.2"

jobs:
source:
name: Resolve the verified source
runs-on: ubuntu-latest
timeout-minutes: 5

permissions:
contents: read
pull-requests: read

outputs:
checkout-ref: ${{ steps.dispatch.outputs.checkout-ref || steps.default.outputs.checkout-ref }}
head: ${{ steps.dispatch.outputs.head || steps.default.outputs.head }}
base: ${{ steps.dispatch.outputs.base || steps.default.outputs.base }}
pr-number: ${{ steps.dispatch.outputs.pr-number || steps.default.outputs.pr-number }}

steps:
- name: Set up exact Node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: ${{ env.LFC_NODE_VERSION }}
package-manager-cache: false

- name: Preserve the ordinary event source
id: default
if: ${{ github.event_name != 'workflow_dispatch' || (inputs.pr-number == '' && inputs.expected-head == '') }}
env:
LFC_CHECKOUT_REF: ${{ github.sha }}
LFC_HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
LFC_BASE_SHA: ${{ github.event.pull_request.base.sha }}
LFC_PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
node --input-type=module --eval '
import { appendFileSync } from "node:fs";
const values = {
"checkout-ref": process.env.LFC_CHECKOUT_REF,
head: process.env.LFC_HEAD_SHA,
base: process.env.LFC_BASE_SHA ?? "",
"pr-number": process.env.LFC_PR_NUMBER ?? ""
};
appendFileSync(process.env.GITHUB_OUTPUT, Object.entries(values).map(([key, value]) => `${key}=${value}\n`).join(""));
'

- name: Check out trusted dispatch validation without persisted credentials
if: ${{ github.event_name == 'workflow_dispatch' && (inputs.pr-number != '' || inputs.expected-head != '') }}
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
ref: ${{ github.event.repository.default_branch }}

- name: Validate the requested Dependabot head before installing dependencies
id: dispatch
if: ${{ github.event_name == 'workflow_dispatch' && (inputs.pr-number != '' || inputs.expected-head != '') }}
env:
GITHUB_TOKEN: ${{ github.token }}
LFC_PR_NUMBER: ${{ inputs.pr-number }}
LFC_HEAD_SHA: ${{ inputs.expected-head }}
run: node scripts/screenshot-automation.mjs verify-dispatch

verify:
name: Build, test, and verify package
needs: source
if: ${{ always() && !cancelled() }}
runs-on: ubuntu-latest
timeout-minutes: 45

steps:
- name: Reject an invalid source selection
env:
LFC_SOURCE_RESULT: ${{ needs.source.result }}
run: test "${LFC_SOURCE_RESULT}" = success

- name: Check out source without persisted credentials
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
ref: ${{ github.sha }}

- name: Reject high-severity dependency regressions
if: ${{ github.event_name == 'pull_request' }}
if: ${{ github.event_name == 'pull_request' || needs.source.outputs.pr-number != '' }}
uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0
with:
fail-on-severity: high
base-ref: ${{ needs.source.outputs.base }}
head-ref: ${{ needs.source.outputs.head }}

- name: Set up exact Node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
Expand All @@ -53,8 +130,20 @@ jobs:
run: npx playwright install --with-deps chromium webkit

- name: Run the CI quality gate
id: quality-gate
run: npm run check

- name: Verify dependency package after a failed quality gate
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
id: dependency-package
if: >-
${{
failure() && steps.quality-gate.outcome == 'failure' &&
github.event_name == 'pull_request' &&
github.event.pull_request.user.login == 'dependabot[bot]' &&
github.event.pull_request.head.repo.full_name == github.repository
}}
run: npm run check:tarball

- name: Upload browser results and failure diagnostics
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
Expand All @@ -65,3 +154,75 @@ jobs:
playwright-report/
test-results/
retention-days: 7

screenshot-candidate:
name: Capture Dependabot screenshot candidates
if: >-
${{
github.event_name == 'pull_request' &&
github.event.pull_request.user.login == 'dependabot[bot]' &&
github.event.pull_request.head.repo.full_name == github.repository
}}
runs-on: windows-2025
timeout-minutes: 30

permissions:
contents: read
pull-requests: read

env:
LFC_PR_NUMBER: ${{ github.event.pull_request.number }}
LFC_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
LFC_BASE_SHA: ${{ github.event.pull_request.base.sha }}

steps:
- name: Check out the exact Dependabot head without persisted credentials
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
ref: ${{ github.event.pull_request.head.sha }}

- name: Set up exact Node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: ${{ env.LFC_NODE_VERSION }}
package-manager-cache: false

- name: Check whether the dependency diff needs screenshot evidence
id: eligibility
env:
GITHUB_TOKEN: ${{ github.token }}
run: node scripts/screenshot-automation.mjs candidate-eligibility

- name: Set up exact npm
if: ${{ steps.eligibility.outputs.eligible == 'true' }}
working-directory: ${{ runner.temp }}
run: npm install --global "npm@$env:LFC_NPM_VERSION" --ignore-scripts

- name: Install locked development tooling
if: ${{ steps.eligibility.outputs.eligible == 'true' }}
run: npm ci --ignore-scripts

- name: Install pinned capture browser
if: ${{ steps.eligibility.outputs.eligible == 'true' }}
run: npx playwright install chromium

- name: Capture candidates for pixel comparison and human review
if: ${{ steps.eligibility.outputs.eligible == 'true' }}
env:
LFC_BROWSER_TARGET_DATE: ${{ steps.eligibility.outputs.capture-date }}
run: npm run screenshots:candidate

- name: Seal candidate provenance
if: ${{ steps.eligibility.outputs.eligible == 'true' }}
run: node scripts/screenshot-automation.mjs seal-candidate

- name: Upload screenshot candidates and comparison gallery
if: ${{ always() && steps.eligibility.outputs.eligible == 'true' }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: screenshot-candidate-${{ github.run_id }}-${{ github.run_attempt }}
path: .cache/screenshots/candidate/
include-hidden-files: true
if-no-files-found: ignore
retention-days: 7
75 changes: 75 additions & 0 deletions .github/workflows/refresh-screenshot-evidence.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
name: Refresh unchanged screenshot evidence

on:
workflow_run:
workflows:
- CI
types:
- completed

permissions:
contents: read

concurrency:
group: screenshot-evidence-${{ github.repository }}-${{ github.event.workflow_run.head_branch }}
queue: max
cancel-in-progress: false

env:
LFC_NODE_VERSION: "24.19.0"

jobs:
promote:
name: Promote identical Dependabot screenshot evidence
if: >-
${{
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.conclusion != 'cancelled' &&
github.event.workflow_run.head_repository.full_name == github.repository
}}
runs-on: ubuntu-latest
timeout-minutes: 10

permissions:
contents: write
pull-requests: read
actions: write

env:
GITHUB_TOKEN: ${{ github.token }}
LFC_UPSTREAM_RUN_ID: ${{ github.event.workflow_run.id }}
LFC_UPSTREAM_RUN_ATTEMPT: ${{ github.event.workflow_run.run_attempt }}
LFC_UPSTREAM_HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
LFC_CANDIDATE_DIRECTORY: ${{ runner.temp }}/screenshot-candidate

steps:
- name: Check out trusted promotion code without persisted credentials
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
ref: ${{ github.sha }}

- name: Set up exact Node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: ${{ env.LFC_NODE_VERSION }}
package-manager-cache: false

- name: Validate the originating run and dependency pull request
id: context
run: node scripts/screenshot-automation.mjs promotion-context

- name: Download the validated candidate artifact
if: ${{ steps.context.outputs.eligible == 'true' }}
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
github-token: ${{ github.token }}
repository: ${{ github.repository }}
run-id: ${{ github.event.workflow_run.id }}
artifact-ids: ${{ steps.context.outputs.artifact-id }}
merge-multiple: true
path: ${{ runner.temp }}/screenshot-candidate

- name: Refresh identical evidence and verify the new exact head
if: ${{ steps.context.outputs.eligible == 'true' }}
run: node scripts/screenshot-automation.mjs promote
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),

### Changed

- Narrowed screenshot freshness to build and capture dependency closures. Dependency-only Dependabot updates can refresh verified provenance automatically when every PNG is byte-identical; changed screenshots retain human review.
- Expanded the visual specification for existing typography, count roles, agenda states, loading, and optional event layouts; corrected labels and documentation links.
- **Migration:** Event titles are no longer trimmed. Any nonempty string of at most 1,024 UTF-16 code units is preserved exactly, including whitespace-only titles; applications are responsible for supplying meaningful visible and accessible text.
- Reduced the default day padding to `clamp(0.25rem, 0.75cqi, 0.5rem)` for a more compact grid while retaining control target floors and intrinsic content growth.
Expand Down
4 changes: 4 additions & 0 deletions CONTRIBUTOR_COMMANDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,10 @@ Prepare screenshot evidence after source and build inputs settle. The preparatio
command skips current evidence, otherwise builds and validates a staged capture
batch and produces a before/after gallery. Review every changed scene using the
[screenshot contract](docs/screenshots/README.md). CI verification remains read-only.
Dependency-only Dependabot updates can refresh provenance automatically when every
captured PNG is byte-identical; changed images still require human review. See
[automatic dependency evidence](docs/screenshots/README.md#automatic-dependency-evidence)
for the eligibility checks and candidate gallery command.

```shell
npm run screenshots:prepare
Expand Down
20 changes: 18 additions & 2 deletions docs/screenshots/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,23 @@ On a fresh Linux machine, add `--with-deps` to the Playwright install command wh

All captures go into an ignored staging directory first. The updater validates the complete batch, checks that sources did not change during capture, and creates `.cache/screenshots/review/index.html` with before/after images plus `changes.json`. It replaces canonical images only after successful capture and validation, commits the manifest last, and restores originals if replacement fails. Failed capture batches remain under `.cache/screenshots/staging-*` for diagnosis; canonical evidence stays untouched. The replacement is rollback-capable, not a cross-file filesystem transaction, so interruption during replacement requires inspecting the retained backup before retrying.

Open the local review gallery and review changed scenes at native dimensions before staging files. CI runs read-only `check:screenshots`; preparation and capture reject attempts to accept stale evidence in CI. A capture lock prevents simultaneous updaters. After a forcibly terminated updater, verify no capture is still running before removing `.cache/screenshots/capture.lock`.
Open the local review gallery and review changed scenes at native dimensions before staging files. CI runs read-only `check:screenshots`; preparation and capture reject canonical acceptance in CI. CI can explicitly capture candidates under the automatic dependency workflow below. A capture lock prevents simultaneous updaters. After a forcibly terminated updater, verify no capture is still running before removing `.cache/screenshots/capture.lock`.

### Automatic dependency evidence

For same-repository Dependabot pull requests against `main`, CI checks whether the diff is limited to locked dependencies and existing exact-pinned development dependency versions. Changes to scripts, source, workflows, scenes, or screenshot images are ineligible for automatic acceptance.

The capture job uses the pinned Playwright Chromium binary on `windows-2025`, with the workflow-selected Node and npm versions. It prepares candidates in the ignored `.cache/screenshots/candidate/` directory and uploads a seven-day artifact containing the before/after gallery, PNGs, candidate manifest, and exact run/base/head identities. It never replaces tracked evidence. Contributors can produce the same candidate layout locally:

```shell
npm run screenshots:candidate
```

A separate workflow executes only trusted default-branch tooling, checks the originating CI attempt and current pull request, and recomputes the fingerprint without installing or executing changed dependencies. All required checks must pass; a failure solely due to the stale screenshot fingerprint is allowed only after independent tarball verification succeeds. Every candidate PNG must be byte-identical to its canonical image, and effective browser targets and scene definitions must remain unchanged. Only then does the workflow atomically commit refreshed provenance and dispatch CI for that exact new head.

Changed screenshots, browser targets, missing evidence, unexpected files, or platform/font drift require human review. Download the candidate artifact from the CI run and open its `index.html` at native dimensions. Prepare and review canonical captures locally before committing any changed images. Automation does not approve or merge a pull request, and release verification remains read-only.

The workflow becomes available after its trusted implementation is merged into `main`. The dependency branch must include the current base before capture. If main advances, update the branch with main and start fresh CI; rerunning an old attempt retains its old event base. Stale head or base results are discarded. A changed capture environment can require a reviewed baseline refresh rather than automatic provenance acceptance.

Nine scenes capture settled states. The held-pull scene intentionally captures direct manipulation before release so the decorative adjacent-month/year lane can be reviewed; selection timing and post-release browser physics remain browser-test and supported-device concerns. Review every canonical scene after an intentional source-layout change. The four count scenes mount a fixture through the public built-package API, with empty, single-event, and multiple-event days. They retain the example's general palette but remove its independent overflow-theme class, preserving evidence for the contextual count defaults. The advanced scenes exercise the borderless overflow theme.

Expand All @@ -112,7 +128,7 @@ The screenshot manifest is machine-owned and records, for every scene:
- Example URL and deterministic setup steps.
- Viewport and output dimensions.
- SHA-256 hash.
- Source fingerprint covering package source, Vite configuration, the advanced route and shared stylesheet, build/server/capture helpers, locked compiler/browser dependencies, and effective JavaScript/CSS browser targets. The preparation scripts and their automatic `pre`/`post` hooks are included. Browser target evidence records its resolution date, but a date change alone does not require recapture. Other example routes, deployment metadata, Playwright's test configuration, root version metadata, and npm scripts outside that capture lifecycle are excluded.
- Source fingerprint covering package source, Vite configuration, the advanced route and shared stylesheet, build/server/capture helpers, and effective JavaScript/CSS browser targets. Locked development inputs include TypeScript, Vite, esbuild, and Playwright plus their resolved transitive, optional, and present peer dependencies. Unrelated lint/test dependency entries are excluded; a dependency shared with the build/capture graph remains included. The preparation and candidate scripts and their automatic `pre`/`post` hooks are included. Browser target evidence records its resolution date, but a date change alone does not require recapture. Other example routes, deployment metadata, Playwright's test configuration, root version metadata, and npm scripts outside that capture lifecycle are excluded.
- Exact Node patch used for capture; validation accepts that provenance when it belongs to major version 24, while npm, Playwright, and Chromium remain exact-pinned. The Playwright value is derived from the exact `package.json` development dependency, and preparation rejects a stale local install before capture.
- Documentation references and exact alt text.

Expand Down
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading