diff --git a/.github/README.md b/.github/README.md index 2faeeb43..41493b92 100644 --- a/.github/README.md +++ b/.github/README.md @@ -1,6 +1,6 @@
- The same weather app built in 10 different frontend frameworks
+ The same weather app built in 12 different frontend frameworks
For automated cross-framework web performance benchmarking

@@ -9,7 +9,7 @@
+### Caveats
+This is a fair, like-for-like comparison - but it's not the final word. The app uses all the everyday stuff (state, fetching, input, lists, lifecycle), but it won't push any framework to its limits. So worth bearing in mind:
+- **Scale**: We're not rendering tens of thousands of nodes, or stress-testing giant lists and rapid re-renders - which is where some frameworks pull ahead
+- **Scope**: It's one app archetype. Things like complex routing, deep global state, streaming SSR and heavy animation aren't covered
+- **Real-world variance**: Benchmarks run in CI on a single environment, so treat the numbers as a guide, not gospel
+
---
## Attributions
### Sponsors
-
+[](https://github.com/sponsors/lissy93)
### Contributors
-
+[](https://github.com/lissy93/framework-benchmarks/graphs/contributors)
### Stargzers
-
+[](https://github.com/lissy93/framework-benchmarks/stargazers)
---
## License
-
> _**[lissy93/framework-benchmarks](https://github.com/lissy93/framework-benchmarks)** is licensed under [MIT](https://github.com/lissy93/framework-benchmarks/blob/HEAD/LICENSE) ยฉ [Alicia Sykes](https://aliciasykes.com) 2025._
- ยฉ Alicia Sykes 2025
+ ยฉ Alicia Sykes 2025 - present
Licensed under MIT

Thanks for visiting :)
diff --git a/.github/SECURITY.md b/.github/SECURITY.md
index e69de29b..914e8447 100644
--- a/.github/SECURITY.md
+++ b/.github/SECURITY.md
@@ -0,0 +1 @@
+Report security issues via github advisories, or email me at `security@as93.net`.
diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml
index f91534f7..89ad6ec0 100644
--- a/.github/workflows/benchmark.yml
+++ b/.github/workflows/benchmark.yml
@@ -35,6 +35,9 @@ env:
NODE_VERSION: '20'
PYTHON_VERSION: '3.11'
+permissions:
+ contents: read
+
jobs:
benchmark:
name: Run Framework Benchmarks
@@ -43,18 +46,19 @@ jobs:
steps:
- name: ๐ฅ Checkout Repository
- uses: actions/checkout@v4
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
fetch-depth: 1
+ persist-credentials: false
- name: ๐ง Setup Node.js
- uses: actions/setup-node@v4
+ uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: ๐ Setup Python
- uses: actions/setup-python@v4
+ uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: 'pip'
@@ -86,15 +90,17 @@ jobs:
sudo apt-get install -y libasound2t64 || sudo apt-get install -y libasound2
- name: ๐ Install Google Chrome
- uses: browser-actions/setup-chrome@v1
+ uses: browser-actions/setup-chrome@c785b87e244131f27c9f19c1a33e2ead956ab7ce # v1
with:
chrome-version: stable
id: setup-chrome
- name: ๐ Verify Chrome Installation
+ env:
+ CHROME_PATH: ${{ steps.setup-chrome.outputs.chrome-path }}
run: |
- echo "Chrome path: ${{ steps.setup-chrome.outputs.chrome-path }}"
- ${{ steps.setup-chrome.outputs.chrome-path }} --version
+ echo "Chrome path: $CHROME_PATH"
+ "$CHROME_PATH" --version
which google-chrome || which chromium-browser || echo "Chrome executable not found in PATH"
- name: ๐ง Setup Project
@@ -162,38 +168,47 @@ jobs:
echo "โ
Project verification complete"
- name: ๐ง Prepare Benchmark Environment
+ env:
+ COMMIT_TO_MAIN_INPUT: ${{ github.event.inputs.commit_to_main || 'false' }}
+ BENCHMARK_TYPES_INPUT: ${{ github.event.inputs.benchmark_types || 'all' }}
+ FRAMEWORKS_INPUT: ${{ github.event.inputs.frameworks || 'all' }}
+ EXECUTIONS_INPUT: ${{ github.event.inputs.executions || '1' }}
run: |
- echo "PYTHONPATH=$PWD:$PWD/scripts" >> $GITHUB_ENV
-
+ echo "PYTHONPATH=$PWD:$PWD/scripts" >> "$GITHUB_ENV"
+
# Set commit behavior based on event type or manual input
if [ "${{ github.event_name }}" = "schedule" ]; then
- echo "COMMIT_TO_MAIN=false" >> $GITHUB_ENV
+ echo "COMMIT_TO_MAIN=false" >> "$GITHUB_ENV"
echo "โฐ Scheduled run - will skip main branch commit"
- echo "SCHEDULED_RUN=true" >> $GITHUB_ENV
+ echo "SCHEDULED_RUN=true" >> "$GITHUB_ENV"
else
- echo "COMMIT_TO_MAIN=${{ github.event.inputs.commit_to_main || 'false' }}" >> $GITHUB_ENV
- echo "๐ค Manual run - commit to main: ${{ github.event.inputs.commit_to_main || 'false' }}"
- echo "SCHEDULED_RUN=false" >> $GITHUB_ENV
+ echo "COMMIT_TO_MAIN=$COMMIT_TO_MAIN_INPUT" >> "$GITHUB_ENV"
+ echo "๐ค Manual run - commit to main: $COMMIT_TO_MAIN_INPUT"
+ echo "SCHEDULED_RUN=false" >> "$GITHUB_ENV"
fi
-
+
echo "๐ Benchmark Configuration:"
- if [ "${{ env.SCHEDULED_RUN }}" = "true" ]; then
+ if [ "$SCHEDULED_RUN" = "true" ]; then
echo " Types: lighthouse,bundle-size,source-analysis,build-time,dev-server,resource-usage (scheduled)"
echo " Frameworks: all (scheduled)"
echo " Executions: 5 (scheduled)"
else
- echo " Types: ${{ github.event.inputs.benchmark_types || 'all' }}"
- echo " Frameworks: ${{ github.event.inputs.frameworks || 'all' }}"
- echo " Executions: ${{ github.event.inputs.executions || '1' }}"
+ echo " Types: $BENCHMARK_TYPES_INPUT"
+ echo " Frameworks: $FRAMEWORKS_INPUT"
+ echo " Executions: $EXECUTIONS_INPUT"
fi
- echo " Commit to Main: ${{ env.COMMIT_TO_MAIN }}"
+ echo " Commit to Main: $COMMIT_TO_MAIN"
- name: ๐งช Run Benchmarks
+ env:
+ BENCHMARK_TYPES_INPUT: ${{ github.event.inputs.benchmark_types }}
+ FRAMEWORKS_INPUT: ${{ github.event.inputs.frameworks }}
+ EXECUTIONS_INPUT: ${{ github.event.inputs.executions }}
run: |
set -e # Exit on error
-
+
# Configure parameters based on trigger type
- if [ "${{ env.SCHEDULED_RUN }}" = "true" ]; then
+ if [ "$SCHEDULED_RUN" = "true" ]; then
# Scheduled run: comprehensive benchmarks
benchmark_types="lighthouse,bundle-size,source-analysis,build-time,dev-server,resource-usage"
frameworks="" # All frameworks
@@ -201,9 +216,9 @@ jobs:
echo "โฐ Scheduled run configuration: all benchmarks, all frameworks, 5 executions"
else
# Manual run: use provided inputs
- benchmark_types="${{ github.event.inputs.benchmark_types }}"
- frameworks="${{ github.event.inputs.frameworks }}"
- executions="${{ github.event.inputs.executions }}"
+ benchmark_types="$BENCHMARK_TYPES_INPUT"
+ frameworks="$FRAMEWORKS_INPUT"
+ executions="$EXECUTIONS_INPUT"
fi
# Build command arguments for the benchmark script
@@ -260,36 +275,42 @@ jobs:
- name: ๐ Generate Benchmark Summary
if: always()
run: |
- echo "## ๐ Benchmark Results Summary" >> $GITHUB_STEP_SUMMARY
- echo "" >> $GITHUB_STEP_SUMMARY
- echo "- **Run Date**: $(date -u '+%Y-%m-%d %H:%M:%S UTC')" >> $GITHUB_STEP_SUMMARY
+ {
+ echo "## ๐ Benchmark Results Summary"
+ echo ""
+ echo "- **Run Date**: $(date -u '+%Y-%m-%d %H:%M:%S UTC')"
+ } >> "$GITHUB_STEP_SUMMARY"
# Check if benchmarks ran successfully
if [ -d "benchmark-results" ] && [ "$(find benchmark-results -name "*.json" -type f | wc -l)" -gt 0 ]; then
result_count=$(find benchmark-results -name "*.json" -type f | wc -l)
latest_dir=$(find benchmark-results -mindepth 1 -maxdepth 1 -type d | sort -r | head -1)
- echo "- **Status**: โ
Success" >> $GITHUB_STEP_SUMMARY
- echo "- **Results Generated**: $result_count files" >> $GITHUB_STEP_SUMMARY
- echo "- **Artifacts**: Available for download below" >> $GITHUB_STEP_SUMMARY
+ {
+ echo "- **Status**: โ
Success"
+ echo "- **Results Generated**: $result_count files"
+ echo "- **Artifacts**: Available for download below"
+ } >> "$GITHUB_STEP_SUMMARY"
if [ -n "$latest_dir" ]; then
- echo "- **Latest Results**: $(basename "$latest_dir")" >> $GITHUB_STEP_SUMMARY
+ echo "- **Latest Results**: $(basename "$latest_dir")" >> "$GITHUB_STEP_SUMMARY"
fi
else
- echo "- **Status**: โ Failed" >> $GITHUB_STEP_SUMMARY
- echo "- **Reason**: Benchmarks did not complete successfully" >> $GITHUB_STEP_SUMMARY
- echo "- **Check**: Review the workflow logs for error details" >> $GITHUB_STEP_SUMMARY
- echo "- **Artifacts**: Debug logs may be available below (if any)" >> $GITHUB_STEP_SUMMARY
+ {
+ echo "- **Status**: โ Failed"
+ echo "- **Reason**: Benchmarks did not complete successfully"
+ echo "- **Check**: Review the workflow logs for error details"
+ echo "- **Artifacts**: Debug logs may be available below (if any)"
+ } >> "$GITHUB_STEP_SUMMARY"
fi
- name: ๐ Save Workflow Context
run: |
- echo '{"commit_to_main": "${{ env.COMMIT_TO_MAIN }}"}' > workflow-context.json
+ echo "{\"commit_to_main\": \"${COMMIT_TO_MAIN}\"}" > workflow-context.json
cat workflow-context.json
- name: ๐ค Upload Benchmark Results
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
if: always()
with:
name: benchmark-results-${{ github.run_number }}
@@ -301,7 +322,7 @@ jobs:
compression-level: 6
- name: ๐ค Upload Detailed Logs
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
if: failure()
with:
name: benchmark-logs-${{ github.run_number }}
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 0345eda3..65025517 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -16,7 +16,7 @@ on:
- 'website/**'
- 'frameworks.json'
- 'config.json'
- workflow_run:
+ workflow_run: # zizmor: ignore[dangerous-triggers] internal pipeline step, only runs for main-branch runs of our own trusted workflow
workflows: ["๐ Transform Results"]
types: [completed]
branches: [main]
@@ -27,7 +27,7 @@ concurrency:
cancel-in-progress: true
permissions:
- contents: write
+ contents: read
actions: read
jobs:
@@ -39,16 +39,18 @@ jobs:
frameworks: ${{ steps.matrix.outputs.frameworks }}
steps:
- name: Checkout repository
- uses: actions/checkout@v4
-
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
+ with:
+ persist-credentials: false
+
- name: Setup Node.js
- uses: actions/setup-node@v4
+ uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: '18'
cache: 'npm'
- name: Setup Python
- uses: actions/setup-python@v4
+ uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: '3.9'
cache: 'pip'
@@ -64,7 +66,7 @@ jobs:
run: python scripts/verify/check.py
- name: Cache setup for build jobs
- uses: actions/cache/save@v4
+ uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
with:
path: |
node_modules
@@ -76,13 +78,15 @@ jobs:
- name: Determine frameworks to build
id: matrix
+ env:
+ FRAMEWORKS_INPUT: ${{ github.event.inputs.frameworks }}
run: |
- if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ "${{ github.event.inputs.frameworks }}" != "all" ]; then
- frameworks="${{ github.event.inputs.frameworks }}"
+ if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ "$FRAMEWORKS_INPUT" != "all" ]; then
+ frameworks="$FRAMEWORKS_INPUT"
else
frameworks=$(python scripts/get_frameworks.py)
fi
- echo "frameworks=$(echo "[$frameworks]" | sed 's/,/", "/g' | sed 's/\[/[\"/ ; s/\]/\"]/')" >> $GITHUB_OUTPUT
+ echo "frameworks=$(echo "[$frameworks]" | sed 's/,/", "/g' | sed 's/\[/[\"/ ; s/\]/\"]/')" >> "$GITHUB_OUTPUT"
echo "Building frameworks: $frameworks"
build:
@@ -98,10 +102,12 @@ jobs:
framework: ${{ fromJSON(needs.setup.outputs.frameworks) }}
steps:
- name: Checkout repository
- uses: actions/checkout@v4
-
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
+ with:
+ persist-credentials: false
+
- name: Setup Python
- uses: actions/setup-python@v4
+ uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: '3.9'
cache: 'pip'
@@ -111,7 +117,7 @@ jobs:
run: pip install -r scripts/requirements.txt
- name: Restore setup cache
- uses: actions/cache/restore@v4
+ uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
with:
path: |
node_modules
@@ -124,28 +130,36 @@ jobs:
- name: Build ${{ matrix.framework }}
id: build-run
+ env:
+ FRAMEWORK: ${{ matrix.framework }}
run: |
- if python scripts/run/build.py --framework ${{ matrix.framework }} --ci --static-site; then
- echo "status=success" >> $GITHUB_OUTPUT
+ if python scripts/run/build.py --framework "$FRAMEWORK" --ci --static-site; then
+ echo "status=success" >> "$GITHUB_OUTPUT"
else
- echo "status=failure" >> $GITHUB_OUTPUT
+ echo "status=failure" >> "$GITHUB_OUTPUT"
exit 1
fi
-
+
- name: Log build status
if: always()
+ env:
+ FRAMEWORK: ${{ matrix.framework }}
+ BUILD_STATUS: ${{ steps.build-run.outputs.status }}
run: |
- echo "Build status for ${{ matrix.framework }}: ${{ steps.build-run.outputs.status }}"
- if [ "${{ steps.build-run.outputs.status }}" == "success" ]; then
- echo "โ
${{ matrix.framework }} built successfully"
+ echo "Build status for $FRAMEWORK: $BUILD_STATUS"
+ if [ "$BUILD_STATUS" == "success" ]; then
+ echo "โ
$FRAMEWORK built successfully"
else
- echo "โ ${{ matrix.framework }} build failed"
+ echo "โ $FRAMEWORK build failed"
fi
-
+
- name: Generate build badge
if: always()
+ env:
+ FRAMEWORK: ${{ matrix.framework }}
+ BUILD_STATUS: ${{ steps.build-run.outputs.status }}
run: |
- if [[ "${{ steps.build-run.outputs.status }}" == "success" ]]; then
+ if [[ "$BUILD_STATUS" == "success" ]]; then
status="Success"
color="3cd96b"
label_color="33b348"
@@ -154,13 +168,13 @@ jobs:
color="ff5666"
label_color="d5334a"
fi
-
+
badge_url="https://img.shields.io/badge/Build-${status}-${color}?logo=rocket&logoColor=fff&labelColor=${label_color}"
- curl -o "build-${{ matrix.framework }}.svg" "$badge_url"
+ curl -o "build-${FRAMEWORK}.svg" "$badge_url"
- name: Upload badge
if: always()
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: badge-build-${{ matrix.framework }}
path: build-${{ matrix.framework }}.svg
@@ -173,10 +187,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
- uses: actions/checkout@v4
-
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
+ with:
+ persist-credentials: false
+
- name: Setup Python
- uses: actions/setup-python@v4
+ uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: '3.9'
cache: 'pip'
@@ -186,7 +202,7 @@ jobs:
run: pip install -r scripts/requirements.txt
- name: Restore setup cache
- uses: actions/cache/restore@v4
+ uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
with:
path: |
node_modules
@@ -203,7 +219,7 @@ jobs:
npm run build -- --static-site
- name: Upload website artifacts
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: website
path: dist-website
@@ -214,23 +230,30 @@ jobs:
needs: [setup, build, website]
if: always()
runs-on: ubuntu-latest
+ permissions:
+ contents: write
steps:
- name: Checkout repository
- uses: actions/checkout@v4
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
fetch-depth: 0
-
+ persist-credentials: false
+
- name: Download website
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: website
path: website-artifact
-
+
- name: Deploy to website branch
+ env:
+ TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ REPO: ${{ github.repository }}
run: |
set -euo pipefail
git config user.name 'liss-bot'
git config user.email 'alicia-gh-bot@mail.as93.net'
+ git remote set-url origin "https://x-access-token:${TOKEN}@github.com/${REPO}.git"
# Switch to website branch (create if missing)
if git ls-remote --exit-code --heads origin website >/dev/null 2>&1; then
@@ -277,24 +300,31 @@ jobs:
if: always()
runs-on: ubuntu-latest
continue-on-error: true
+ permissions:
+ contents: write
steps:
- name: Checkout repository
- uses: actions/checkout@v4
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
fetch-depth: 0
-
+ persist-credentials: false
+
- name: Download all badges
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
pattern: badge-build-*
path: badges
merge-multiple: true
-
+
- name: Commit badges
+ env:
+ TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ REPO: ${{ github.repository }}
run: |
git config user.name 'liss-bot'
git config user.email 'alicia-gh-bot@mail.as93.net'
-
+ git remote set-url origin "https://x-access-token:${TOKEN}@github.com/${REPO}.git"
+
# Switch to badges branch
git fetch origin badges:badges 2>/dev/null || git checkout --orphan badges
git checkout badges 2>/dev/null || true
@@ -303,7 +333,7 @@ jobs:
# Copy badges and commit
cp badges/*.svg . 2>/dev/null || echo "โ ๏ธ No badge files found"
- if git add *.svg && git diff --staged --quiet; then
+ if git add ./*.svg && git diff --staged --quiet; then
echo "โน๏ธ No badge changes to commit"
else
git commit -m "Update build badges"
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 00000000..bfa1857c
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,261 @@
+# CI checks to run when a PR is opened, or manually via workflow_dispatch
+# Test and lint are handled by their own dedicated workflows
+name: ๐ฆ CI
+
+on:
+ pull_request:
+ branches: [main]
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+env:
+ PYTHON_VERSION: '3.11'
+
+jobs:
+ changes:
+ name: ๐ Detect Changes
+ runs-on: ubuntu-latest
+ outputs:
+ lockfile: ${{ steps.filter.outputs.lockfile }}
+ workflows: ${{ steps.filter.outputs.workflows }}
+ src: ${{ steps.filter.outputs.src }}
+ docker: ${{ steps.filter.outputs.docker }}
+ steps:
+ - name: Checkout Code
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
+ with:
+ persist-credentials: false
+
+ - name: Filter Paths
+ uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4
+ id: filter
+ with:
+ filters: |
+ lockfile:
+ - 'package-lock.json'
+ - 'scripts/requirements.txt'
+ workflows:
+ - '.github/workflows/**'
+ src:
+ - 'apps/**'
+ - 'scripts/**'
+ - 'package.json'
+ - 'frameworks.json'
+ - 'config.json'
+ docker:
+ - 'Dockerfile'
+ - '.dockerignore'
+ - 'docker-compose.yml'
+
+ dependency-audit:
+ name: ๐ Dependency Audit
+ runs-on: ubuntu-latest
+ needs: changes
+ if: needs.changes.outputs.lockfile == 'true' && github.event_name == 'pull_request'
+ steps:
+ - name: Checkout Code
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
+ with:
+ persist-credentials: false
+
+ - name: Review Dependencies
+ uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5
+ with:
+ fail-on-severity: moderate
+
+ workflow-audit:
+ name: ๐ ๏ธ Workflow Audit
+ runs-on: ubuntu-latest
+ needs: changes
+ if: needs.changes.outputs.workflows == 'true' || github.event_name == 'workflow_dispatch'
+ steps:
+ - name: Checkout Code
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
+ with:
+ persist-credentials: false
+
+ - name: Run Actionlint
+ uses: raven-actions/actionlint@3d39aea434753780c3b3d4a1a31c854b4dbf49d7 # v2
+ with:
+ fail-on-error: true
+
+ - name: Run Zizmor
+ uses: zizmorcore/zizmor-action@192e21d79ab29983730a13d1382995c2307fbcaa # v0.5.7
+ with:
+ inputs: .github/workflows/
+ advanced-security: false
+ annotations: true
+
+ smoke:
+ name: ๐จ Smoke Test
+ runs-on: ubuntu-latest
+ needs: changes
+ if: needs.changes.outputs.src == 'true' || github.event_name == 'workflow_dispatch'
+ steps:
+ - name: Checkout Code
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
+ with:
+ persist-credentials: false
+
+ - name: Setup Python
+ uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
+ with:
+ python-version: ${{ env.PYTHON_VERSION }}
+ cache: 'pip'
+ cache-dependency-path: scripts/requirements.txt
+
+ - name: Install dependencies
+ run: pip install -r scripts/requirements.txt
+
+ - name: Validate config schemas
+ run: python scripts/verify/validate_schemas.py
+
+ config-sync:
+ name: ๐งฌ Config Sync
+ runs-on: ubuntu-latest
+ needs: changes
+ if: needs.changes.outputs.src == 'true' || github.event_name == 'workflow_dispatch'
+ steps:
+ - name: Checkout Code
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
+ with:
+ persist-credentials: false
+
+ - name: Setup Python
+ uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
+ with:
+ python-version: ${{ env.PYTHON_VERSION }}
+ cache: 'pip'
+ cache-dependency-path: scripts/requirements.txt
+
+ - name: Install dependencies
+ run: pip install -r scripts/requirements.txt
+
+ - name: Regenerate config-derived files
+ run: |
+ python scripts/setup/generate_scripts.py
+ python scripts/setup/generate_mocks.py
+
+ - name: Check for drift
+ run: |
+ if ! git diff --quiet -- package.json assets/mocks/; then
+ echo "โ Generated files are out of sync with frameworks.json"
+ echo " Run generate_scripts.py and generate_mocks.py, then commit"
+ git diff -- package.json assets/mocks/
+ exit 1
+ fi
+ echo "โ
Config-derived files in sync"
+
+ docker-smoke:
+ name: ๐ณ Docker Smoke Test
+ runs-on: ubuntu-latest
+ needs: changes
+ if: needs.changes.outputs.docker == 'true' || github.event_name == 'workflow_dispatch'
+ timeout-minutes: 20
+ steps:
+ - name: Checkout Code
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
+ with:
+ persist-credentials: false
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4
+
+ - name: Build production image
+ uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7
+ with:
+ context: .
+ target: production
+ load: true
+ tags: framework-benchmarks:ci
+ cache-from: type=gha
+ cache-to: type=gha,mode=max
+
+ - name: Run container & check health
+ run: |
+ docker run -d --rm --name fb-smoke framework-benchmarks:ci
+ for _ in $(seq 1 30); do
+ if docker exec fb-smoke curl -fsS http://localhost:3000/health >/dev/null 2>&1; then
+ echo "โ
Container healthy"
+ docker stop fb-smoke
+ exit 0
+ fi
+ sleep 2
+ done
+ echo "โ Container failed health check"
+ docker logs fb-smoke || true
+ docker stop fb-smoke || true
+ exit 1
+
+ secret-scan:
+ name: ๐ Secret Scanning
+ runs-on: ubuntu-latest
+ if: github.event_name == 'pull_request'
+ steps:
+ - name: Checkout Code
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
+ with:
+ fetch-depth: 0
+ persist-credentials: false
+
+ - name: Scan PR Diff for Secrets
+ uses: trufflesecurity/trufflehog@30d5bb91af1a771378349dbbb0c82129392acf70 # v3.95.6
+ with:
+ base: ${{ github.event.pull_request.base.sha }}
+ head: ${{ github.event.pull_request.head.sha }}
+ extra_args: --only-verified
+
+ # Renders markdown summary of all checks at the end
+ summary:
+ name: ๐ Summary
+ runs-on: ubuntu-latest
+ if: always()
+ continue-on-error: true
+ needs:
+ - dependency-audit
+ - workflow-audit
+ - smoke
+ - config-sync
+ - docker-smoke
+ - secret-scan
+ steps:
+ - name: Render Summary
+ env:
+ NEEDS: ${{ toJSON(needs) }}
+ run: |
+ label() {
+ case "$1" in
+ dependency-audit) echo "๐ Dependency Audit" ;;
+ workflow-audit) echo "๐ ๏ธ Workflow Audit" ;;
+ smoke) echo "๐จ Smoke Test" ;;
+ config-sync) echo "๐งฌ Config Sync" ;;
+ docker-smoke) echo "๐ณ Docker Smoke Test" ;;
+ secret-scan) echo "๐ Secret Scanning" ;;
+ *) echo "$1" ;;
+ esac
+ }
+ status() {
+ case "$1" in
+ success) echo "โ
Passed" ;;
+ skipped) echo "โญ๏ธ Skipped" ;;
+ cancelled) echo "๐ซ Cancelled" ;;
+ failure) echo "โ Failed" ;;
+ *) echo "โ $1" ;;
+ esac
+ }
+ {
+ echo "## CI Summary"
+ echo ""
+ echo "| Check | Status |"
+ echo "|-------|--------|"
+ } >> "$GITHUB_STEP_SUMMARY"
+ for job in $(echo "$NEEDS" | jq -r 'keys[]'); do
+ result=$(echo "$NEEDS" | jq -r --arg j "$job" '.[$j].result')
+ echo "| $(label "$job") | $(status "$result") |" >> "$GITHUB_STEP_SUMMARY"
+ done
diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml
index 428364bb..a027e44c 100644
--- a/.github/workflows/docker.yml
+++ b/.github/workflows/docker.yml
@@ -8,8 +8,6 @@ on:
- '**.md'
- '.github/**'
- '!.github/workflows/docker.yml'
- pull_request:
- branches: [main]
workflow_dispatch:
env:
@@ -26,14 +24,15 @@ jobs:
steps:
- name: ๐ฅ Checkout Repository
- uses: actions/checkout@v4
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
+ with:
+ persist-credentials: false
- name: ๐ณ Set up Docker Buildx
- uses: docker/setup-buildx-action@v3
+ uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4
- name: ๐ Login to Container Registry
- if: github.event_name != 'pull_request'
- uses: docker/login-action@v3
+ uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
@@ -41,7 +40,7 @@ jobs:
- name: ๐ท๏ธ Extract Metadata
id: meta
- uses: docker/metadata-action@v5
+ uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
@@ -50,29 +49,32 @@ jobs:
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,value=latest,enable={{is_default_branch}}
+ type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }}
- name: ๐จ Build and Push Docker Image
- uses: docker/build-push-action@v5
+ uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7
with:
context: .
target: production
platforms: linux/amd64,linux/arm64
- push: ${{ github.event_name != 'pull_request' }}
+ push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: ๐ Generate Summary
- if: success() && github.event_name != 'pull_request'
+ if: success()
+ env:
+ TAGS: ${{ steps.meta.outputs.tags }}
run: |
- cat >> $GITHUB_STEP_SUMMARY << EOF
+ cat >> "$GITHUB_STEP_SUMMARY" << EOF
## ๐ณ Docker Build Results
- โ
**Published**: ${{ steps.meta.outputs.tags }}
+ โ
**Published**: ${TAGS}
### ๐ Usage
\`\`\`bash
- docker run -p 3000:3000 ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
+ docker run -p 3000:3000 ${REGISTRY}/${IMAGE_NAME}:latest
\`\`\`
EOF
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index c827cefa..04b8f2b1 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -29,7 +29,7 @@ concurrency:
cancel-in-progress: true
permissions:
- contents: write
+ contents: read
actions: read
jobs:
@@ -41,16 +41,18 @@ jobs:
frameworks: ${{ steps.matrix.outputs.frameworks }}
steps:
- name: Checkout repository
- uses: actions/checkout@v4
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
+ with:
+ persist-credentials: false
- name: Setup Node.js
- uses: actions/setup-node@v4
+ uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: '18'
cache: 'npm'
- name: Setup Python
- uses: actions/setup-python@v4
+ uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: '3.9'
cache: 'pip'
@@ -62,16 +64,39 @@ jobs:
- name: Install dependencies
run: npm ci
+ - name: Filter changed paths
+ if: github.event_name == 'pull_request'
+ uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4
+ id: changed
+ with:
+ list-files: shell
+ filters: |
+ shared:
+ - 'package.json'
+ - 'package-lock.json'
+ - 'eslint.config.mjs'
+ - '.github/workflows/lint.yml'
+ apps:
+ - 'apps/**'
+
- name: Determine frameworks to lint
id: matrix
+ env:
+ FRAMEWORKS_INPUT: ${{ github.event.inputs.frameworks }}
+ EVENT_NAME: ${{ github.event_name }}
+ SHARED: ${{ steps.changed.outputs.shared }}
+ APPS_FILES: ${{ steps.changed.outputs.apps_files }}
run: |
- if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ "${{ github.event.inputs.frameworks }}" != "all" ]; then
- frameworks="${{ github.event.inputs.frameworks }}"
+ if [ "$EVENT_NAME" = "workflow_dispatch" ] && [ "$FRAMEWORKS_INPUT" != "all" ]; then
+ frameworks="$FRAMEWORKS_INPUT"
+ elif [ "$EVENT_NAME" = "pull_request" ] && [ "$SHARED" != "true" ]; then
+ # PR touching only specific apps: lint just those frameworks
+ frameworks=$(echo "$APPS_FILES" | tr ' ' '\n' | sed -nE 's#^apps/([^/]+)/.*#\1#p' | sort -u | paste -sd, -)
else
# Get framework list from Python config
frameworks=$(python scripts/get_frameworks.py)
fi
- echo "frameworks=$(echo "[$frameworks]" | sed 's/,/", "/g' | sed 's/\[/[\"/ ; s/\]/\"]/')" >> $GITHUB_OUTPUT
+ echo "frameworks=$(echo "[$frameworks]" | sed 's/,/", "/g' | sed 's/\[/[\"/ ; s/\]/\"]/')" >> "$GITHUB_OUTPUT"
echo "Linting frameworks: $frameworks"
lint:
@@ -87,10 +112,12 @@ jobs:
framework: ${{ fromJSON(needs.setup.outputs.frameworks) }}
steps:
- name: Checkout repository
- uses: actions/checkout@v4
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
+ with:
+ persist-credentials: false
- name: Setup Node.js
- uses: actions/setup-node@v4
+ uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: '18'
cache: 'npm'
@@ -99,17 +126,21 @@ jobs:
run: npm ci
- name: Run ESLint for ${{ matrix.framework }}
- run: npm run lint:${{ matrix.framework }}
-
+ env:
+ FRAMEWORK: ${{ matrix.framework }}
+ run: npm run "lint:$FRAMEWORK"
+
- name: Generate lint report for ${{ matrix.framework }}
if: always()
+ env:
+ FRAMEWORK: ${{ matrix.framework }}
run: |
mkdir -p lint-reports
- npm run lint:${{ matrix.framework }} -- --format=json --output-file=lint-reports/${{ matrix.framework }}-lint-report.json || true
+ npm run "lint:$FRAMEWORK" -- --format=json --output-file="lint-reports/${FRAMEWORK}-lint-report.json" || true
- name: Upload lint report
if: always()
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: lint-report-${{ matrix.framework }}
path: lint-reports/${{ matrix.framework }}-lint-report.json
@@ -117,9 +148,11 @@ jobs:
- name: Generate lint badge
if: always()
+ env:
+ FRAMEWORK: ${{ matrix.framework }}
run: |
# Determine badge status and color
- report_file="lint-reports/${{ matrix.framework }}-lint-report.json"
+ report_file="lint-reports/${FRAMEWORK}-lint-report.json"
if [ -f "$report_file" ]; then
error_count=$(jq '[.[].errorCount] | add // 0' "$report_file" 2>/dev/null || echo "0")
warning_count=$(jq '[.[].warningCount] | add // 0' "$report_file" 2>/dev/null || echo "0")
@@ -145,11 +178,11 @@ jobs:
# Generate badge
badge_url="https://img.shields.io/badge/Lint-${status}-${color}?logo=eslint&logoColor=fff&labelColor=${label_color}"
- curl -o "lint-${{ matrix.framework }}.svg" "$badge_url"
+ curl -o "lint-${FRAMEWORK}.svg" "$badge_url"
- name: Upload badge
if: always()
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: badge-lint-${{ matrix.framework }}
path: lint-${{ matrix.framework }}.svg
@@ -162,10 +195,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
- uses: actions/checkout@v4
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
+ with:
+ persist-credentials: false
- name: Setup Node.js
- uses: actions/setup-node@v4
+ uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: '18'
cache: 'npm'
@@ -174,15 +209,17 @@ jobs:
run: npm ci
- name: Download all lint reports
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
path: lint-reports
merge-multiple: true
- name: Generate lint summary
+ env:
+ FRAMEWORKS_JSON: ${{ needs.setup.outputs.frameworks }}
run: |
# Get the frameworks that were actually tested
- frameworks=$(echo '${{ needs.setup.outputs.frameworks }}' | jq -r '.[]' | tr '\n' ' ')
+ frameworks=$(echo "$FRAMEWORKS_JSON" | jq -r '.[]' | tr '\n' ' ')
total_apps=0
passed_apps=0
@@ -252,7 +289,7 @@ jobs:
echo "|-----------|--------|--------|"
echo -e "$table_rows"
fi
- } >> $GITHUB_STEP_SUMMARY
+ } >> "$GITHUB_STEP_SUMMARY"
- name: Check overall status
run: |
@@ -269,25 +306,32 @@ jobs:
if: ${{ always() && github.event_name != 'pull_request' }}
runs-on: ubuntu-latest
continue-on-error: true
+ permissions:
+ contents: write
steps:
- name: Checkout with bot token
- uses: actions/checkout@v4
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
token: ${{ secrets.BOT_TOKEN }}
fetch-depth: 0
-
+ persist-credentials: false
+
- name: Download all badges
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
pattern: badge-lint-*
path: badges
merge-multiple: true
-
+
- name: Commit badges
+ env:
+ TOKEN: ${{ secrets.BOT_TOKEN }}
+ REPO: ${{ github.repository }}
run: |
git config user.name 'liss-bot'
git config user.email 'alicia-gh-bot@mail.as93.net'
-
+ git remote set-url origin "https://x-access-token:${TOKEN}@github.com/${REPO}.git"
+
# Switch to badges branch
git fetch origin badges:badges 2>/dev/null || git checkout --orphan badges
git checkout badges 2>/dev/null || true
@@ -296,7 +340,7 @@ jobs:
# Copy badges and commit
cp badges/*.svg . 2>/dev/null || echo "โ ๏ธ No badge files found"
- if git add *.svg && git diff --staged --quiet; then
+ if git add ./*.svg && git diff --staged --quiet; then
echo "โน๏ธ No badge changes to commit"
else
git commit -m "Update lint badges"
diff --git a/.github/workflows/mirror.yml b/.github/workflows/mirror.yml
index b7a68e8d..00d59956 100644
--- a/.github/workflows/mirror.yml
+++ b/.github/workflows/mirror.yml
@@ -6,13 +6,17 @@ on:
- cron: '30 1 * * 0'
push:
tags: [ 'v*' ]
+
+permissions:
+ contents: read
+
jobs:
codeberg:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
- with: { fetch-depth: 0 }
- - uses: pixta-dev/repository-mirroring-action@v1
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
+ with: { fetch-depth: 0, persist-credentials: false }
+ - uses: pixta-dev/repository-mirroring-action@674e65a7d483ca28dafaacba0d07351bdcc8bd75 # v1
with:
target_repo_url: git@codeberg.org:alicia/frontend-benchmarks.git
ssh_private_key: ${{ secrets.CODEBERG_SSH }}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 00000000..cf4e1c2e
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,191 @@
+# Builds all framework apps and drafts a GitHub release with the compiled
+# apps, the comparison website, the benchmark results, SHA256 checksums and
+# a SLSA build-provenance attestation attached as assets
+#
+# Triggered by:
+# - Push of any major/minor (vX.Y.0) git tag
+# - Manual dispatch with any existing tag (any version)
+
+name: ๐ Release
+
+on:
+ push:
+ tags: ['v*.*.0']
+ workflow_dispatch:
+ inputs:
+ tag:
+ description: 'Existing git tag to release (e.g. v1.2.0)'
+ required: true
+
+concurrency:
+ group: ${{ github.workflow }}-${{ inputs.tag || github.ref_name }}
+ cancel-in-progress: false
+
+permissions:
+ contents: read
+
+jobs:
+ release:
+ name: ๐ Build & Draft Release
+ runs-on: ubuntu-latest
+ timeout-minutes: 60
+ permissions:
+ contents: write
+ id-token: write
+ attestations: write
+ env:
+ TAG: ${{ inputs.tag || github.ref_name }}
+ steps:
+ - name: ๐๏ธ Checkout tag
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
+ with:
+ ref: refs/tags/${{ env.TAG }}
+ fetch-depth: 0
+ persist-credentials: false
+
+ - name: ๐ง Setup Node.js
+ uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 # zizmor: ignore[cache-poisoning] no cache is configured here
+ with:
+ node-version: '20'
+
+ - name: ๐ Setup Python
+ uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
+ with:
+ python-version: '3.11'
+
+ - name: ๐ฅ Install dependencies and prepare project
+ run: |
+ set -euo pipefail
+ pip install -r scripts/requirements.txt
+ npm ci
+ python scripts/setup/main.py --skip-build
+
+ - name: ๐๏ธ Build all apps and comparison website
+ run: |
+ set -euo pipefail
+ npm run build -- --static-site --ci
+ test -f dist-website/index.html \
+ || { echo "::error::dist-website was not built"; exit 1; }
+
+ - name: ๐ฆ Package release assets
+ id: package
+ run: |
+ set -euo pipefail
+ mkdir -p release-assets
+
+ # Combined site: all compiled apps + the comparison website
+ tar -czf "release-assets/framework-benchmarks-website-${TAG}.tar.gz" -C dist-website .
+
+ # Individual compiled apps
+ count=0
+ IFS=',' read -ra IDS <<< "$(python scripts/get_frameworks.py)"
+ for id in "${IDS[@]}"; do
+ if [ -d "dist-website/$id" ]; then
+ tar -czf "release-assets/app-${id}-${TAG}.tar.gz" -C dist-website "$id"
+ count=$((count + 1))
+ else
+ echo "::warning::No build output for '$id', skipping"
+ fi
+ done
+ echo "Packaged $count app(s)"
+
+ # Benchmark results (committed JSON/TSV in results/)
+ mkdir -p results-staging
+ cp results/*.json results/*.tsv results-staging/ 2>/dev/null || true
+ if [ -n "$(ls -A results-staging 2>/dev/null)" ]; then
+ tar -czf "release-assets/framework-benchmarks-results-${TAG}.tar.gz" -C results-staging .
+ else
+ echo "::warning::No benchmark results found in results/"
+ fi
+
+ echo "Release assets:"
+ ls -lh release-assets
+
+ - name: ๐ชช Generate build provenance attestation
+ id: attest
+ continue-on-error: true
+ uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4
+ with:
+ subject-path: 'release-assets/*.tar.gz'
+ show-summary: false
+
+ - name: ๐ค Stage attestation bundle
+ if: steps.attest.outcome == 'success'
+ env:
+ BUNDLE: ${{ steps.attest.outputs.bundle-path }}
+ run: |
+ set -euo pipefail
+ cp "$BUNDLE" "release-assets/provenance-${TAG}.intoto.jsonl"
+
+ - name: ๐ข Generate SHA256 checksums
+ working-directory: release-assets
+ run: |
+ set -euo pipefail
+ sha256sum ./*.tar.gz > SHA256SUMS.txt
+ cat SHA256SUMS.txt
+
+ - name: ๐ Find previous release tag
+ id: prev
+ env:
+ CURRENT_TAG: ${{ env.TAG }}
+ run: |
+ set -euo pipefail
+ git fetch --tags --force
+ PREV=$({ echo "$CURRENT_TAG"; git tag -l 'v*.*.0' || true; } \
+ | sort -uV \
+ | awk -v cur="$CURRENT_TAG" '$0 == cur { print prev; exit } { prev = $0 }')
+ echo "tag=$PREV" >> "$GITHUB_OUTPUT"
+
+ - name: ๐ Create draft release
+ id: release
+ # Kept over `gh release create` for built-in generate_release_notes,
+ # previous_tag selection, fail_on_unmatched_files and multi-file upload.
+ uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3 # zizmor: ignore[superfluous-actions]
+ with:
+ tag_name: ${{ env.TAG }}
+ name: Release ${{ env.TAG }}
+ draft: true
+ prerelease: false
+ generate_release_notes: true
+ previous_tag: ${{ steps.prev.outputs.tag }}
+ fail_on_unmatched_files: true
+ files: release-assets/*
+ token: ${{ secrets.BOT_TOKEN != '' && secrets.BOT_TOKEN || secrets.GITHUB_TOKEN }}
+
+ - name: ๐ Job summary
+ if: always()
+ env:
+ REPO_URL: ${{ github.server_url }}/${{ github.repository }}
+ PREV_TAG: ${{ steps.prev.outputs.tag }}
+ RELEASE_URL: ${{ steps.release.outputs.url }}
+ ATTEST_URL: ${{ steps.attest.outputs.attestation-url }}
+ ATTEST_OUTCOME: ${{ steps.attest.outcome }}
+ run: |
+ set -euo pipefail
+ {
+ echo "## ๐ Release ${TAG}"
+ echo ""
+ echo "| Item | Value |"
+ echo "|------|-------|"
+ echo "| Tag | [\`${TAG}\`](${REPO_URL}/releases/tag/${TAG}) |"
+ if [ -n "$PREV_TAG" ]; then
+ echo "| Previous tag | [\`${PREV_TAG}\`](${REPO_URL}/releases/tag/${PREV_TAG}) |"
+ fi
+ echo ""
+ echo "### Assets"
+ echo ""
+ echo '```'
+ ( cd release-assets && ls -1 ) 2>/dev/null || echo "none"
+ echo '```'
+ echo ""
+ if [ "$ATTEST_OUTCOME" = "success" ] && [ -n "$ATTEST_URL" ]; then
+ echo "| Attestation | โ
[View](${ATTEST_URL}) |"
+ else
+ echo "| Attestation | โ ๏ธ Failed or skipped |"
+ fi
+ if [ -n "$RELEASE_URL" ]; then
+ echo "| Draft release | โ
[Review and publish](${RELEASE_URL}) |"
+ else
+ echo "| Draft release | โ Failed |"
+ fi
+ } >> "$GITHUB_STEP_SUMMARY"
diff --git a/.github/workflows/tag.yml b/.github/workflows/tag.yml
new file mode 100644
index 00000000..62bac201
--- /dev/null
+++ b/.github/workflows/tag.yml
@@ -0,0 +1,372 @@
+# Creates a new git tag when a PR is merged, or on manual dispatch
+#
+# PR trigger flow:
+# - Triggered whenever a PR is merged, if that PR made code changes
+# - If version wasn't bumped in PR, increment patch version and update package.json
+# - Otherwise (if the PR did bump version) we use the new version from package.json
+# - Creates and pushes a git tag for the new version
+# - That git tag then triggers the Docker workflow to publish the image
+# - Adds release labels and comments to any referenced issues
+# - Finally, shows a summary of actions taken and the new tag published
+#
+# Manual dispatch flow:
+# - If a version is provided, sets package.json to that version
+# - If no version is provided, increments patch version automatically
+# - Creates and pushes a git tag
+#
+# Note: pushing the tag triggers docker.yml only when BOT_TOKEN (a PAT) is set;
+# the default GITHUB_TOKEN cannot trigger other workflows. Without it the tag is
+# still created, but the Docker workflow must be run manually.
+
+name: ๐ Tag
+
+on:
+ workflow_dispatch:
+ inputs:
+ version:
+ description: 'Version to tag (e.g. 1.1.0). Leave blank to auto-increment patch.'
+ required: false
+ type: string
+ # Safe and intentional: only runs once a reviewed PR is merged into main,
+ # checks out the base branch (not PR code), and runs no code from the PR.
+ pull_request_target: # zizmor: ignore[dangerous-triggers]
+ types: [closed]
+ branches: [main]
+
+concurrency:
+ group: auto-version-and-tag
+ cancel-in-progress: false
+
+permissions:
+ contents: read
+
+env:
+ IS_MANUAL: ${{ github.event_name == 'workflow_dispatch' }}
+
+jobs:
+ version-and-tag:
+ if: >-
+ github.event_name == 'workflow_dispatch'
+ || github.event.pull_request.merged == true
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+ permissions:
+ contents: write
+ pull-requests: read
+ issues: write
+
+ steps:
+ - name: ๐ข Validate manual dispatch
+ if: env.IS_MANUAL == 'true'
+ env:
+ INPUT_VERSION: ${{ inputs.version }}
+ DISPATCH_REF: ${{ github.ref }}
+ run: |
+ set -euo pipefail
+ if [ "$DISPATCH_REF" != "refs/heads/main" ]; then
+ echo "::error::Manual dispatch only allowed from main (got: $DISPATCH_REF)"
+ exit 1
+ fi
+ if [ -n "$INPUT_VERSION" ] && ! printf '%s' "$INPUT_VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then
+ echo "::error::Invalid version '${INPUT_VERSION}'. Must be semver (e.g. 1.1.0)."
+ exit 1
+ fi
+
+ - name: ๐ Check PR for code changes and version bump
+ id: check_pr
+ if: env.IS_MANUAL != 'true'
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ with:
+ script: |
+ const { owner, repo } = context.repo;
+ const pull_number = context.payload.pull_request.number;
+
+ const files = await github.paginate(
+ github.rest.pulls.listFiles, { owner, repo, pull_number }
+ );
+ const codePatterns = [
+ /^apps\//, /^scripts\//, /^website\//, /^tests\//,
+ /^Dockerfile$/, /^[^/]+\.(js|cjs|mjs|json)$/,
+ ];
+ const codeChanged = files.some(f =>
+ codePatterns.some(p => p.test(f.filename))
+ );
+ const pkgChanged = files.some(f => f.filename === 'package.json');
+
+ if (!codeChanged && !pkgChanged) {
+ core.info('No code or package.json changes, skipping');
+ core.setOutput('needs_bump', 'false');
+ core.setOutput('needs_tag', 'false');
+ return;
+ }
+
+ let versionBumped = false;
+ if (pkgChanged) {
+ const mergeSha = context.payload.pull_request.merge_commit_sha;
+ const { data: mergeCommit } = await github.rest.git.getCommit({
+ owner, repo, commit_sha: mergeSha,
+ });
+ const parentSha = mergeCommit.parents[0].sha;
+ const getVersion = async (ref) => {
+ const { data } = await github.rest.repos.getContent({
+ owner, repo, path: 'package.json', ref,
+ });
+ return JSON.parse(Buffer.from(data.content, 'base64').toString()).version;
+ };
+ const [prevVersion, mergeVersion] = await Promise.all([
+ getVersion(parentSha), getVersion(mergeSha),
+ ]);
+ versionBumped = prevVersion !== mergeVersion;
+ core.info(`Version: ${prevVersion} โ ${mergeVersion}`);
+ }
+
+ const needsBump = codeChanged && !versionBumped;
+ const needsTag = codeChanged || versionBumped;
+ core.info(`Needs bump: ${needsBump}, Needs tag: ${needsTag}`);
+ core.setOutput('needs_bump', needsBump.toString());
+ core.setOutput('needs_tag', needsTag.toString());
+
+ - name: ๐๏ธ Checkout repository
+ if: env.IS_MANUAL == 'true' || steps.check_pr.outputs.needs_tag == 'true'
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
+ with:
+ ref: ${{ github.event.pull_request.merge_commit_sha || github.ref }}
+ fetch-depth: 0
+ persist-credentials: false
+
+ - name: ๐ง Setup Node.js
+ if: env.IS_MANUAL == 'true' || steps.check_pr.outputs.needs_tag == 'true'
+ uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 # zizmor: ignore[cache-poisoning] no cache is configured here
+ with:
+ node-version: '20'
+
+ - name: ๐ค Configure git identity and remote
+ if: env.IS_MANUAL == 'true' || steps.check_pr.outputs.needs_tag == 'true'
+ env:
+ TOKEN: ${{ secrets.BOT_TOKEN != '' && secrets.BOT_TOKEN || secrets.GITHUB_TOKEN }}
+ REPO: ${{ github.repository }}
+ run: |
+ set -euo pipefail
+ git config user.name "liss-bot"
+ git config user.email "alicia-gh-bot@mail.as93.net"
+ git remote set-url origin "https://x-access-token:${TOKEN}@github.com/${REPO}.git"
+
+ - name: ๐ Extract referenced issues
+ id: issues
+ if: env.IS_MANUAL != 'true' && steps.check_pr.outputs.needs_tag == 'true'
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ with:
+ script: |
+ const body = context.payload.pull_request.body || '';
+ const prNumber = String(context.payload.pull_request.number);
+ const matches = body.match(/#(\d+)(?![a-fA-F0-9])/g);
+ if (!matches) {
+ core.info('No issue references found in PR body');
+ core.setOutput('numbers', '');
+ return;
+ }
+ const unique = [...new Set(matches.map(m => m.replace('#', '')))]
+ .filter(n => n !== prNumber && parseInt(n, 10) > 0);
+ if (unique.length === 0) {
+ core.info('No issue references after filtering');
+ core.setOutput('numbers', '');
+ return;
+ }
+ core.info(`Found issue references: ${unique.join(', ')}`);
+ core.setOutput('numbers', unique.join(','));
+
+ - name: โฌ๏ธ Bump version
+ id: bump
+ if: >-
+ env.IS_MANUAL == 'true'
+ || steps.check_pr.outputs.needs_bump == 'true'
+ env:
+ INPUT_VERSION: ${{ inputs.version }}
+ run: |
+ set -euo pipefail
+ if [ "$IS_MANUAL" = "true" ] && [ -n "$INPUT_VERSION" ]; then
+ npm version "$INPUT_VERSION" --no-git-tag-version --allow-same-version
+ else
+ npm version patch --no-git-tag-version
+ fi
+ NEW_VERSION=$(node -p "require('./package.json').version")
+ git add package.json package-lock.json
+ if git diff --cached --quiet; then
+ echo "package.json already at $NEW_VERSION, nothing to commit"
+ echo "bumped=false" >> "$GITHUB_OUTPUT"
+ else
+ git commit -m "chore: bump version to $NEW_VERSION [skip ci]"
+ git push origin HEAD:main
+ echo "bumped=true" >> "$GITHUB_OUTPUT"
+ echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
+ fi
+
+ - name: ๐ท๏ธ Create and push tag
+ id: tag
+ if: env.IS_MANUAL == 'true' || steps.check_pr.outputs.needs_tag == 'true'
+ env:
+ PR_NUMBER: ${{ github.event.pull_request.number || '' }}
+ PR_TITLE: ${{ github.event.pull_request.title || '' }}
+ PR_AUTHOR: ${{ github.event.pull_request.user.login || github.actor }}
+ MERGE_SHA: ${{ github.event.pull_request.merge_commit_sha || github.sha }}
+ ISSUES: ${{ steps.issues.outputs.numbers }}
+ run: |
+ set -euo pipefail
+ VERSION=$(node -p "require('./package.json').version")
+ TAG="v$VERSION"
+ git fetch --tags --force
+ if git rev-parse "refs/tags/$TAG" >/dev/null 2>&1; then
+ echo "Tag $TAG already exists, skipping"
+ echo "result=existed" >> "$GITHUB_OUTPUT"
+ echo "version=$TAG" >> "$GITHUB_OUTPUT"
+ exit 0
+ fi
+
+ {
+ printf 'Framework Benchmarks %s\n\n' "$TAG"
+ if [ -n "$PR_NUMBER" ]; then
+ printf 'PR: #%s - %s\n' "$PR_NUMBER" "$PR_TITLE"
+ else
+ printf 'Manual release by @%s\n' "$PR_AUTHOR"
+ fi
+ if [ -n "$ISSUES" ]; then
+ printf 'Resolves: %s\n' "$(echo "$ISSUES" | sed 's/,/, #/g; s/^/#/')"
+ fi
+ printf 'Author: @%s\n' "$PR_AUTHOR"
+ printf 'Commit: %s\n' "$MERGE_SHA"
+ } > tag-message.txt
+
+ git tag -a "$TAG" -F tag-message.txt
+ git push origin "$TAG"
+ echo "result=created" >> "$GITHUB_OUTPUT"
+ echo "version=$TAG" >> "$GITHUB_OUTPUT"
+
+ - name: ๐ฉ๏ธ Label referenced issues
+ id: label
+ if: >-
+ env.IS_MANUAL != 'true'
+ && steps.check_pr.outputs.needs_tag == 'true'
+ && steps.issues.outputs.numbers != ''
+ continue-on-error: true
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ env:
+ ISSUES: ${{ steps.issues.outputs.numbers }}
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ script: |
+ const { owner, repo } = context.repo;
+ const { version } = JSON.parse(require('fs').readFileSync('package.json', 'utf8'));
+ const labelName = `Released v${version}`;
+ const issues = process.env.ISSUES.split(',').filter(Boolean);
+
+ try {
+ await github.rest.issues.createLabel({
+ owner, repo,
+ name: labelName,
+ color: 'EDEDED',
+ description: `Included in release v${version}`,
+ });
+ core.info(`Created label: ${labelName}`);
+ } catch (e) {
+ if (e.status === 422) {
+ core.info(`Label already exists: ${labelName}`);
+ } else {
+ core.warning(`Failed to create label: ${e.message}`);
+ }
+ }
+
+ const prNumber = context.payload.pull_request.number;
+ const marker = `released-${version}`;
+ for (const num of issues) {
+ const issue_number = parseInt(num, 10);
+ try {
+ const comments = await github.rest.issues.listComments({
+ owner, repo, issue_number, per_page: 100,
+ });
+ const alreadyCommented = comments.data.some(
+ c => c.body?.includes(marker)
+ );
+ if (!alreadyCommented) {
+ const body = [
+ `This has now been implemented in #${prNumber},`,
+ `and will be released shortly in v${version} ๐`,
+ ``,
+ ].join(' ');
+ await github.rest.issues.createComment({
+ owner, repo, issue_number, body,
+ });
+ }
+ await github.rest.issues.addLabels({
+ owner, repo, issue_number, labels: [labelName],
+ });
+ core.info(alreadyCommented
+ ? `Already commented on #${num}, label applied`
+ : `Commented and labeled #${num}`);
+ } catch (e) {
+ core.warning(`Failed to process #${num}: ${e.message}`);
+ }
+ }
+
+ - name: ๐ Job summary
+ if: always()
+ env:
+ PR_NUMBER: ${{ github.event.pull_request.number || '' }}
+ PR_TITLE: ${{ github.event.pull_request.title || '' }}
+ PR_AUTHOR: ${{ github.event.pull_request.user.login || github.actor }}
+ REPO_URL: ${{ github.server_url }}/${{ github.repository }}
+ NEEDS_TAG: ${{ steps.check_pr.outputs.needs_tag }}
+ ISSUES: ${{ steps.issues.outputs.numbers }}
+ BUMPED: ${{ steps.bump.outputs.bumped }}
+ BUMP_SHA: ${{ steps.bump.outputs.sha }}
+ TAG_OUTCOME: ${{ steps.tag.outcome }}
+ TAG_RESULT: ${{ steps.tag.outputs.result }}
+ TAG_VERSION: ${{ steps.tag.outputs.version }}
+ LABEL_OUTCOME: ${{ steps.label.outcome }}
+ run: |
+ set -euo pipefail
+ VERSION="${TAG_VERSION:-v$(node -p "require('./package.json').version" 2>/dev/null || echo "unknown")}"
+
+ {
+ echo "## Auto Version & Tag"
+ echo ""
+ echo "| Step | Result |"
+ echo "|------|--------|"
+
+ if [ "$IS_MANUAL" = "true" ]; then
+ echo "| Trigger | Manual dispatch |"
+ elif [ -n "$PR_NUMBER" ]; then
+ echo "| PR | [#${PR_NUMBER}](${REPO_URL}/pull/${PR_NUMBER}): ${PR_TITLE} |"
+ fi
+ if [ -n "$PR_AUTHOR" ]; then
+ echo "| Author | [@${PR_AUTHOR}](${REPO_URL%/*/*}/${PR_AUTHOR}) |"
+ fi
+
+ if [ "$NEEDS_TAG" = "false" ]; then
+ echo "| Result | โญ๏ธ No code changes, nothing to do |"
+ fi
+
+ if [ "$BUMPED" = "true" ]; then
+ echo "| Version bump | โ
\`${VERSION}\` ([\`${BUMP_SHA:0:7}\`](${REPO_URL}/commit/${BUMP_SHA})) |"
+ else
+ echo "| Version bump | โญ๏ธ Skipped |"
+ fi
+
+ if [ "$TAG_RESULT" = "created" ]; then
+ echo "| Tag | โ
[\`${VERSION}\`](${REPO_URL}/releases/tag/${VERSION}) |"
+ elif [ "$TAG_RESULT" = "existed" ]; then
+ echo "| Tag | โญ๏ธ Already exists: \`${VERSION}\` |"
+ elif [ "$TAG_OUTCOME" = "failure" ]; then
+ echo "| Tag | โ Failed |"
+ else
+ echo "| Tag | โญ๏ธ Skipped |"
+ fi
+
+ if [ -n "$ISSUES" ]; then
+ ISSUE_LINKS=$(echo "$ISSUES" | tr ',' '\n' | sed "s|.*|[#&](${REPO_URL}/issues/&)|" | paste -sd ' ' -)
+ if [ "$LABEL_OUTCOME" = "success" ]; then
+ echo "| Issues labeled | โ
${ISSUE_LINKS} |"
+ else
+ echo "| Issues labeled | โ ๏ธ ${ISSUE_LINKS} |"
+ fi
+ fi
+ } >> "$GITHUB_STEP_SUMMARY"
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index dd15d864..6b438dd1 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -23,7 +23,7 @@ concurrency:
cancel-in-progress: true
permissions:
- contents: write
+ contents: read
actions: read
env:
@@ -39,17 +39,19 @@ jobs:
frameworks: ${{ steps.matrix.outputs.frameworks }}
steps:
- name: Checkout repository
- uses: actions/checkout@v4
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
+ with:
+ persist-credentials: false
- name: Setup Node.js
- uses: actions/setup-node@v4
+ uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: '18'
cache: 'npm'
cache-dependency-path: package-lock.json
- name: Setup Python
- uses: actions/setup-python@v4
+ uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: '3.9'
cache: 'pip'
@@ -67,7 +69,7 @@ jobs:
run: python scripts/setup/main.py --skip-build
- name: Cache node_modules and Playwright browsers
- uses: actions/cache@v4
+ uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
with:
path: |
node_modules
@@ -78,16 +80,43 @@ jobs:
${{ runner.os }}-node-modules-playwright-
${{ runner.os }}-node-modules-
+ - name: Filter changed paths
+ if: github.event_name == 'pull_request'
+ uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4
+ id: changed
+ with:
+ list-files: shell
+ filters: |
+ shared:
+ - 'package.json'
+ - 'package-lock.json'
+ - 'frameworks.json'
+ - 'config.json'
+ - 'playwright.config.js'
+ - 'tests/**'
+ - 'scripts/**'
+ - '.github/workflows/test.yml'
+ apps:
+ - 'apps/**'
+
- name: Determine frameworks to test
id: matrix
+ env:
+ FRAMEWORKS_INPUT: ${{ github.event.inputs.frameworks }}
+ EVENT_NAME: ${{ github.event_name }}
+ SHARED: ${{ steps.changed.outputs.shared }}
+ APPS_FILES: ${{ steps.changed.outputs.apps_files }}
run: |
- if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ "${{ github.event.inputs.frameworks }}" != "all" ]; then
- frameworks="${{ github.event.inputs.frameworks }}"
+ if [ "$EVENT_NAME" = "workflow_dispatch" ] && [ "$FRAMEWORKS_INPUT" != "all" ]; then
+ frameworks="$FRAMEWORKS_INPUT"
+ elif [ "$EVENT_NAME" = "pull_request" ] && [ "$SHARED" != "true" ]; then
+ # PR touching only specific apps: test just those frameworks
+ frameworks=$(echo "$APPS_FILES" | tr ' ' '\n' | sed -nE 's#^apps/([^/]+)/.*#\1#p' | sort -u | paste -sd, -)
else
# Get framework list from Python config
frameworks=$(python scripts/get_frameworks.py)
fi
- echo "frameworks=$(echo "[$frameworks]" | sed 's/,/", "/g' | sed 's/\[/[\"/ ; s/\]/\"]/')" >> $GITHUB_OUTPUT
+ echo "frameworks=$(echo "[$frameworks]" | sed 's/,/", "/g' | sed 's/\[/[\"/ ; s/\]/\"]/')" >> "$GITHUB_OUTPUT"
echo "Testing frameworks: $frameworks"
test:
@@ -103,16 +132,18 @@ jobs:
framework: ${{ fromJSON(needs.setup.outputs.frameworks) }}
steps:
- name: Checkout repository
- uses: actions/checkout@v4
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
+ with:
+ persist-credentials: false
- name: Setup Node.js
- uses: actions/setup-node@v4
+ uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: '18'
cache: 'npm'
- name: Setup Python
- uses: actions/setup-python@v4
+ uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: '3.9'
cache: 'pip'
@@ -122,7 +153,7 @@ jobs:
run: pip install -r scripts/requirements.txt
- name: Restore dependencies cache
- uses: actions/cache@v4
+ uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
with:
path: |
node_modules
@@ -140,7 +171,7 @@ jobs:
fi
- name: Add node_modules/.bin to PATH
- run: echo "${{ github.workspace }}/node_modules/.bin" >> $GITHUB_PATH
+ run: echo "${{ github.workspace }}/node_modules/.bin" >> "$GITHUB_PATH"
- name: Install Playwright browsers (if cache miss)
run: |
@@ -167,7 +198,7 @@ jobs:
- name: Run tests for ${{ matrix.framework }}
id: test-run
- uses: nick-fields/retry@v2
+ uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4
with:
timeout_minutes: 10
max_attempts: 4
@@ -181,30 +212,36 @@ jobs:
- name: Create detailed result file
if: always()
+ env:
+ OUTCOME: ${{ steps.test-run.outcome }}
+ FRAMEWORK: ${{ matrix.framework }}
+ TOTAL_ATTEMPTS: ${{ steps.test-run.outputs.total_attempts || '1' }}
+ EXIT_CODE: ${{ steps.test-run.outputs.exit_code || '0' }}
+ ELAPSED_TIME: ${{ steps.test-run.outputs.elapsed_time || 'unknown' }}
run: |
mkdir -p test-status test-details
-
+
# Determine final status (failed if all retries exhausted)
- if [[ "${{ steps.test-run.outcome }}" == "success" ]]; then
+ if [[ "$OUTCOME" == "success" ]]; then
status="passed"
icon="โ
"
else
status="failed"
icon="โ"
fi
-
+
# Create status file
- echo "$status" > test-status/${{ matrix.framework }}.txt
-
+ echo "$status" > "test-status/${FRAMEWORK}.txt"
+
# Create detailed results file
- cat > test-details/${{ matrix.framework }}.json << EOF
+ cat > "test-details/${FRAMEWORK}.json" << EOF
{
- "framework": "${{ matrix.framework }}",
+ "framework": "${FRAMEWORK}",
"status": "$status",
"icon": "$icon",
- "attempts": ${{ steps.test-run.outputs.total_attempts || '1' }},
- "final_attempt": ${{ steps.test-run.outputs.exit_code || '0' }},
- "duration": "${{ steps.test-run.outputs.elapsed_time || 'unknown' }}",
+ "attempts": ${TOTAL_ATTEMPTS},
+ "final_attempt": ${EXIT_CODE},
+ "duration": "${ELAPSED_TIME}",
"timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"runner": "${{ runner.os }}",
"node_version": "$(node --version)",
@@ -214,7 +251,7 @@ jobs:
- name: Upload test results (conditional)
if: always()
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: ${{ matrix.framework }}-test-results
path: |
@@ -225,7 +262,7 @@ jobs:
- name: Upload test status and details
if: always()
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: ${{ matrix.framework }}-status
path: |
@@ -235,14 +272,18 @@ jobs:
- name: Generate test badge
if: always()
+ env:
+ OUTCOME: ${{ steps.test-run.outcome }}
+ TOTAL_ATTEMPTS: ${{ steps.test-run.outputs.total_attempts || '1' }}
+ FRAMEWORK: ${{ matrix.framework }}
run: |
# Determine badge status and color
- if [[ "${{ steps.test-run.outcome }}" == "success" ]]; then
+ if [[ "$OUTCOME" == "success" ]]; then
status="Passing"
color="3cd96b"
label_color="33b348"
else
- attempts="${{ steps.test-run.outputs.total_attempts || '1' }}"
+ attempts="$TOTAL_ATTEMPTS"
if [[ "$attempts" -gt "1" ]]; then
status="Unstable"
color="fb974e"
@@ -253,14 +294,14 @@ jobs:
label_color="d5334a"
fi
fi
-
+
# Generate badge
badge_url="https://img.shields.io/badge/Tests-${status}-${color}?logo=vitest&logoColor=fff&labelColor=${label_color}"
- curl -o "test-${{ matrix.framework }}.svg" "$badge_url"
+ curl -o "test-${FRAMEWORK}.svg" "$badge_url"
- name: Upload badge
if: always()
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: badge-test-${{ matrix.framework }}
path: test-${{ matrix.framework }}.svg
@@ -273,29 +314,37 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
- uses: actions/checkout@v4
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
+ with:
+ persist-credentials: false
- name: Setup Node.js
- uses: actions/setup-node@v4
+ uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: '18'
cache: 'npm'
- name: Download all test status artifacts
if: always()
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
pattern: "*-status"
path: test-artifacts
merge-multiple: true
- name: Generate test summary
+ env:
+ FRAMEWORKS_JSON: ${{ needs.setup.outputs.frameworks }}
+ EVENT_NAME: ${{ github.event_name }}
+ REF_NAME: ${{ github.ref_name }}
+ ACTOR: ${{ github.actor }}
+ RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
cd test-artifacts || exit 1
-
+
# Get frameworks and initialize counters
- frameworks="${{ needs.setup.outputs.frameworks }}"
- frameworks_array=($(echo "$frameworks" | tr -d '[]"' | tr ',' '\n' | xargs))
+ frameworks="$FRAMEWORKS_JSON"
+ read -ra frameworks_array <<< "$(echo "$frameworks" | tr -d '[]"' | tr ',' ' ')"
passed_count=0
failed_count=0
total_count=${#frameworks_array[@]}
@@ -350,14 +399,16 @@ jobs:
# Add workflow information
{
echo ""
- echo "**Workflow Info:** Triggered by ${{ github.event_name }} on \`${{ github.ref_name }}\` by @${{ github.actor }} | [View Run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})"
+ echo "**Workflow Info:** Triggered by ${EVENT_NAME} on \`${REF_NAME}\` by @${ACTOR} | [View Run](${RUN_URL})"
} >> "$GITHUB_STEP_SUMMARY"
# Set environment variables
success_rate=$(( passed_count * 100 / total_count ))
- echo "FAILED_COUNT=$failed_count" >> $GITHUB_ENV
- echo "PASSED_COUNT=$passed_count" >> $GITHUB_ENV
- echo "SUCCESS_RATE=$success_rate" >> $GITHUB_ENV
+ {
+ echo "FAILED_COUNT=$failed_count"
+ echo "PASSED_COUNT=$passed_count"
+ echo "SUCCESS_RATE=$success_rate"
+ } >> "$GITHUB_ENV"
cd ..
@@ -382,25 +433,27 @@ jobs:
- name: Create status badge data
if: always()
+ env:
+ FRAMEWORKS_JSON: ${{ needs.setup.outputs.frameworks }}
run: |
mkdir -p badge-data
-
+
# Calculate total frameworks from the array
- frameworks="${{ needs.setup.outputs.frameworks }}"
- frameworks_array=($(echo "$frameworks" | tr -d '[]"' | tr ',' '\n' | xargs))
+ frameworks="$FRAMEWORKS_JSON"
+ read -ra frameworks_array <<< "$(echo "$frameworks" | tr -d '[]"' | tr ',' ' ')"
total_count=${#frameworks_array[@]}
# Determine badge color (with null checks)
if [[ -z "$FAILED_COUNT" || -z "$SUCCESS_RATE" ]]; then
badge_color="lightgrey"
badge_message="unknown"
- elif [ $FAILED_COUNT -eq 0 ]; then
+ elif [ "$FAILED_COUNT" -eq 0 ]; then
badge_color="brightgreen"
badge_message="${PASSED_COUNT}/${total_count} passed"
- elif [ $SUCCESS_RATE -ge 80 ]; then
+ elif [ "$SUCCESS_RATE" -ge 80 ]; then
badge_color="green"
badge_message="${PASSED_COUNT}/${total_count} passed"
- elif [ $SUCCESS_RATE -ge 50 ]; then
+ elif [ "$SUCCESS_RATE" -ge 50 ]; then
badge_color="yellow"
badge_message="${PASSED_COUNT}/${total_count} passed"
else
@@ -422,7 +475,7 @@ jobs:
- name: Upload badge data
if: always()
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: badge-data
path: badge-data/test-results.json
@@ -434,25 +487,32 @@ jobs:
if: ${{ always() && github.event_name != 'pull_request' }}
runs-on: ubuntu-latest
continue-on-error: true
+ permissions:
+ contents: write
steps:
- name: Checkout with bot token
- uses: actions/checkout@v4
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
token: ${{ secrets.BOT_TOKEN }}
fetch-depth: 0
-
+ persist-credentials: false
+
- name: Download all badges
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
pattern: badge-test-*
path: badges
merge-multiple: true
-
+
- name: Commit badges
+ env:
+ TOKEN: ${{ secrets.BOT_TOKEN }}
+ REPO: ${{ github.repository }}
run: |
git config user.name 'liss-bot'
git config user.email 'alicia-gh-bot@mail.as93.net'
-
+ git remote set-url origin "https://x-access-token:${TOKEN}@github.com/${REPO}.git"
+
# Switch to badges branch
git fetch origin badges:badges 2>/dev/null || git checkout --orphan badges
git checkout badges 2>/dev/null || true
@@ -461,7 +521,7 @@ jobs:
# Copy badges and commit
cp badges/*.svg . 2>/dev/null || echo "โ ๏ธ No badge files found"
- if git add *.svg && git diff --staged --quiet; then
+ if git add ./*.svg && git diff --staged --quiet; then
echo "โน๏ธ No badge changes to commit"
else
git commit -m "Update test badges"
diff --git a/.github/workflows/transform-results.yml b/.github/workflows/transform-results.yml
index c01e51ea..df4913f5 100644
--- a/.github/workflows/transform-results.yml
+++ b/.github/workflows/transform-results.yml
@@ -1,7 +1,7 @@
name: ๐ Transform Results
on:
- workflow_run:
+ workflow_run: # zizmor: ignore[dangerous-triggers] internal pipeline step, only runs for main-branch runs of our own trusted Benchmark workflow
workflows: ["๐ Benchmark"]
types: [completed]
branches: [main]
@@ -22,13 +22,14 @@ jobs:
steps:
- name: ๐ฅ Checkout Repository
- uses: actions/checkout@v4
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
token: ${{ secrets.GITHUB_TOKEN }}
fetch-depth: 1
+ persist-credentials: false
- name: ๐ Setup Python
- uses: actions/setup-python@v5
+ uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: 'pip'
@@ -38,7 +39,7 @@ jobs:
pip install -r scripts/requirements.txt
- name: ๐ฅ Download Benchmark Results
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}
@@ -58,7 +59,7 @@ jobs:
echo "โ
Found benchmark results"
- name: ๐๏ธ Prepare Benchmark Results Directory
- run: |
+ run: | # zizmor: ignore[github-env] COMMIT_TO_MAIN is a parsed true/false boolean from our own artifact, not arbitrary input
# Create benchmark-results directory structure
mkdir -p benchmark-results
@@ -91,11 +92,11 @@ jobs:
if [ -n "$context_file" ]; then
COMMIT_TO_MAIN=$(python3 -c "import json; print(json.load(open('$context_file')).get('commit_to_main', 'false'))" 2>/dev/null || echo "false")
- echo "COMMIT_TO_MAIN=$COMMIT_TO_MAIN" >> $GITHUB_ENV
+ echo "COMMIT_TO_MAIN=$COMMIT_TO_MAIN" >> "$GITHUB_ENV"
echo "๐ Workflow context: commit_to_main=$COMMIT_TO_MAIN"
rm "$context_file" 2>/dev/null || true # Clean up
else
- echo "COMMIT_TO_MAIN=false" >> $GITHUB_ENV
+ echo "COMMIT_TO_MAIN=false" >> "$GITHUB_ENV"
echo "๐ No workflow context found, defaulting to skip main branch commit"
fi
@@ -137,26 +138,32 @@ jobs:
- name: ๐ Generate Results Summary
run: |
- echo "## ๐ Benchmark Results Transformation" >> $GITHUB_STEP_SUMMARY
- echo "" >> $GITHUB_STEP_SUMMARY
- echo "- **Status**: โ
Success" >> $GITHUB_STEP_SUMMARY
- echo "- **Timestamp**: $(date -u '+%Y-%m-%d %H:%M:%S UTC')" >> $GITHUB_STEP_SUMMARY
- echo "- **Source**: Workflow run #${{ github.event.workflow_run.run_number }}" >> $GITHUB_STEP_SUMMARY
- echo "- **Main Branch Commit**: $([ "$COMMIT_TO_MAIN" = "true" ] && echo "โ
Enabled" || echo "โญ๏ธ Skipped")" >> $GITHUB_STEP_SUMMARY
+ {
+ echo "## ๐ Benchmark Results Transformation"
+ echo ""
+ echo "- **Status**: โ
Success"
+ echo "- **Timestamp**: $(date -u '+%Y-%m-%d %H:%M:%S UTC')"
+ echo "- **Source**: Workflow run #${{ github.event.workflow_run.run_number }}"
+ echo "- **Main Branch Commit**: $([ "$COMMIT_TO_MAIN" = "true" ] && echo "โ
Enabled" || echo "โญ๏ธ Skipped")"
+ } >> "$GITHUB_STEP_SUMMARY"
- name: ๐ Commit to Results Branch
+ env:
+ TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ REPO: ${{ github.repository }}
run: |
set -e
TIMESTAMP=$(date -u '+%Y%m%d_%H%M%S')
-
+
git config user.name "liss-bot"
git config user.email "alicia-gh-bot@mail.as93.net"
-
+ git remote set-url origin "https://x-access-token:${TOKEN}@github.com/${REPO}.git"
+
# Store current files in a temporary location
mkdir -p /tmp/results-data
- [ -d "benchmark-results" ] && cp -r benchmark-results /tmp/results-data/ 2>/dev/null || true
- [ -d "results" ] && cp -r results /tmp/results-data/ 2>/dev/null || true
- [ -d "website/static/charts" ] && cp -r website/static/charts /tmp/results-data/ 2>/dev/null || true
+ if [ -d "benchmark-results" ]; then cp -r benchmark-results /tmp/results-data/ 2>/dev/null || true; fi
+ if [ -d "results" ]; then cp -r results /tmp/results-data/ 2>/dev/null || true; fi
+ if [ -d "website/static/charts" ]; then cp -r website/static/charts /tmp/results-data/ 2>/dev/null || true; fi
# Switch to results branch
git fetch origin results 2>/dev/null || echo "Results branch doesn't exist yet"
@@ -196,14 +203,14 @@ jobs:
mkdir -p raw summary charts stats
# Copy files from temporary location to organized structure
- [ -d "/tmp/results-data/benchmark-results" ] && cp -r /tmp/results-data/benchmark-results/* raw/ 2>/dev/null || true
- [ -f "/tmp/results-data/results/summary.json" ] && cp /tmp/results-data/results/summary.json summary/summary-${TIMESTAMP}.json 2>/dev/null || true
- [ -f "/tmp/results-data/results/summary.tsv" ] && cp /tmp/results-data/results/summary.tsv summary/summary-${TIMESTAMP}.tsv 2>/dev/null || true
- [ -f "/tmp/results-data/results/summary.json" ] && cp /tmp/results-data/results/summary.json summary/summary.json 2>/dev/null || true
- [ -f "/tmp/results-data/results/summary.tsv" ] && cp /tmp/results-data/results/summary.tsv summary/summary.tsv 2>/dev/null || true
- [ -d "/tmp/results-data/charts" ] && cp -r /tmp/results-data/charts/* charts/ 2>/dev/null || true
- [ -f "/tmp/results-data/results/framework-stats.json" ] && cp /tmp/results-data/results/framework-stats.json stats/stats-${TIMESTAMP}.json 2>/dev/null || true
- [ -f "/tmp/results-data/results/framework-stats.json" ] && cp /tmp/results-data/results/framework-stats.json stats/framework-stats.json 2>/dev/null || true
+ if [ -d "/tmp/results-data/benchmark-results" ]; then cp -r /tmp/results-data/benchmark-results/* raw/ 2>/dev/null || true; fi
+ if [ -f "/tmp/results-data/results/summary.json" ]; then cp /tmp/results-data/results/summary.json "summary/summary-${TIMESTAMP}.json" 2>/dev/null || true; fi
+ if [ -f "/tmp/results-data/results/summary.tsv" ]; then cp /tmp/results-data/results/summary.tsv "summary/summary-${TIMESTAMP}.tsv" 2>/dev/null || true; fi
+ if [ -f "/tmp/results-data/results/summary.json" ]; then cp /tmp/results-data/results/summary.json summary/summary.json 2>/dev/null || true; fi
+ if [ -f "/tmp/results-data/results/summary.tsv" ]; then cp /tmp/results-data/results/summary.tsv summary/summary.tsv 2>/dev/null || true; fi
+ if [ -d "/tmp/results-data/charts" ]; then cp -r /tmp/results-data/charts/* charts/ 2>/dev/null || true; fi
+ if [ -f "/tmp/results-data/results/framework-stats.json" ]; then cp /tmp/results-data/results/framework-stats.json "stats/stats-${TIMESTAMP}.json" 2>/dev/null || true; fi
+ if [ -f "/tmp/results-data/results/framework-stats.json" ]; then cp /tmp/results-data/results/framework-stats.json stats/framework-stats.json 2>/dev/null || true; fi
# Commit and push results branch
if git add raw/ summary/ charts/ stats/ && ! git diff --staged --quiet; then
@@ -217,11 +224,15 @@ jobs:
echo "โน๏ธ No changes to commit to results branch"
fi
- - name: ๐ Commit to Main Branch
+ - name: ๐ Commit to Main Branch
+ env:
+ TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ REPO: ${{ github.repository }}
run: |
git config user.name "liss-bot"
git config user.email "alicia-gh-bot@mail.as93.net"
-
+ git remote set-url origin "https://x-access-token:${TOKEN}@github.com/${REPO}.git"
+
# Switch back to main branch
git checkout main
diff --git a/.github/workflows/update-docs.yml b/.github/workflows/update-docs.yml
index d38b1f7c..e723ece2 100644
--- a/.github/workflows/update-docs.yml
+++ b/.github/workflows/update-docs.yml
@@ -13,12 +13,12 @@ jobs:
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
- name: Setup Python
- uses: actions/setup-python@v5
+ uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: "3.11"
cache: "pip"
@@ -88,12 +88,18 @@ jobs:
- name: Summary
if: always()
+ env:
+ FETCH: ${{ steps.fetch.conclusion }}
+ INSERT_TABLE: ${{ steps.insert_table.conclusion }}
+ INSERT_STATUSES: ${{ steps.insert_statuses.conclusion }}
+ BUILD_READMES: ${{ steps.build_readmes.conclusion }}
+ BUILD_CHARTS: ${{ steps.build_charts.conclusion }}
run: |
- fetch="${{ steps.fetch.conclusion }}"
- insert_table="${{ steps.insert_table.conclusion }}"
- insert_statuses="${{ steps.insert_statuses.conclusion }}"
- build_readmes="${{ steps.build_readmes.conclusion }}"
- build_charts="${{ steps.build_charts.conclusion }}"
+ fetch="$FETCH"
+ insert_table="$INSERT_TABLE"
+ insert_statuses="$INSERT_STATUSES"
+ build_readmes="$BUILD_READMES"
+ build_charts="$BUILD_CHARTS"
line() {
if [ "$1" = "success" ]; then echo "- โ
$2"; else echo "- โ $2"; fi
diff --git a/frameworks.json b/frameworks.json
index 4ca91d5d..839a3b20 100644
--- a/frameworks.json
+++ b/frameworks.json
@@ -384,12 +384,12 @@
"hasNodeModules": false,
"devCommand": "python3 -m http.server 3000 || python -m http.server 3000",
"testCommand": "playwright test --config=tests/config/playwright-lume-js.config.js",
- "lintFiles": ["js"],
- "dir": "."
+ "lintFiles": ["js"]
},
"meta": {
"emoji": "๐ก",
- "iconName": "javascript",
+ "iconName": "firewalla",
+ "iconUrl": "https://pixelflare.cc/alicia/icons/lume.png",
"website": "https://sathvikc.github.io/lume-js",
"docs": "https://sathvikc.github.io/lume-js",
"github": "https://github.com/sathvikc/lume-js",
@@ -397,16 +397,8 @@
"color": "#7C3AED",
"accentColor": "#7C3AED",
"logo": "https://raw.githubusercontent.com/sathvikc/lume-js/refs/heads/main/lume-logo.png",
- "description": "Minimal reactive state library (~2.4KB gzipped core, ~5.5KB global build) with no build step",
- "longDescription": "Standards-only reactive library with state, DOM binding, and keyed list rendering.",
- "video": ""
- },
- "exampleRealApp": {
- "title": "Lume.js",
- "description": "Documentation and demo site built with Lume.js itself",
- "repo": "https://github.com/sathvikc/lume-js",
- "website": "https://sathvikc.github.io/lume-js",
- "logo": ""
+ "description": "Minimal reactive state library (~2.4KB core, ~5.5KB global), no build step",
+ "longDescription": "Standards-only reactive library with state, DOM binding, and keyed list rendering."
}
},
{
diff --git a/package.json b/package.json
index 7f94f0c4..bcf3cf63 100644
--- a/package.json
+++ b/package.json
@@ -17,12 +17,12 @@
"start": "python scripts/run/serve.py",
"help": "python scripts/main.py",
"// DEV COMMANDS": "-------------------------------------------------------",
- "dev:all": "npm run dev:alpine && npm run dev:angular && npm run dev:jquery && npm run dev:lit && npm run dev:preact && npm run dev:qwik && npm run dev:react && npm run dev:solid && npm run dev:svelte && npm run dev:vanilla && npm run dev:vanjs && npm run dev:vue",
+ "dev:all": "npm run dev:alpine && npm run dev:angular && npm run dev:jquery && npm run dev:lit && npm run dev:lume-js && npm run dev:preact && npm run dev:qwik && npm run dev:react && npm run dev:solid && npm run dev:svelte && npm run dev:vanilla && npm run dev:vanjs && npm run dev:vue",
"dev:alpine": "cd apps/alpine && python3 -m http.server 3000 || python -m http.server 3000",
- "dev:lume-js": "cd apps/lume-js && python3 -m http.server 3000 || python -m http.server 3000",
"dev:angular": "cd apps/angular && npx ng serve --port 3000",
"dev:jquery": "cd apps/jquery && npx vite --port 3000",
"dev:lit": "cd apps/lit && npx vite --port 3000",
+ "dev:lume-js": "cd apps/lume-js && python3 -m http.server 3000 || python -m http.server 3000",
"dev:preact": "cd apps/preact && npx vite",
"dev:qwik": "cd apps/qwik && npx vite --port 3000",
"dev:react": "cd apps/react && npx vite",
@@ -32,12 +32,12 @@
"dev:vanjs": "cd apps/vanjs && npx vite --port 3000",
"dev:vue": "cd apps/vue && npx vite --port 3000",
"// BUILD COMMANDS": "-----------------------------------------------------",
- "build:all": "npm run build:alpine && npm run build:angular && npm run build:jquery && npm run build:lit && npm run build:preact && npm run build:qwik && npm run build:react && npm run build:solid && npm run build:svelte && npm run build:vanilla && npm run build:vanjs && npm run build:vue",
+ "build:all": "npm run build:alpine && npm run build:angular && npm run build:jquery && npm run build:lit && npm run build:lume-js && npm run build:preact && npm run build:qwik && npm run build:react && npm run build:solid && npm run build:svelte && npm run build:vanilla && npm run build:vanjs && npm run build:vue",
"build:alpine": "cd apps/alpine && echo 'No build step required'",
- "build:lume-js": "cd apps/lume-js && echo 'No build step required'",
"build:angular": "cd apps/angular && npx ng build",
"build:jquery": "cd apps/jquery && npx vite build",
"build:lit": "cd apps/lit && npx vite build",
+ "build:lume-js": "cd apps/lume-js && echo 'No build step required'",
"build:preact": "cd apps/preact && npx vite build",
"build:qwik": "cd apps/qwik && npx vite build",
"build:react": "cd apps/react && npx vite build",
@@ -47,12 +47,12 @@
"build:vanjs": "cd apps/vanjs && npx vite build",
"build:vue": "cd apps/vue && npx vite build",
"// TEST COMMANDS": "------------------------------------------------------",
- "test:all": "npm run test:alpine && npm run test:angular && npm run test:jquery && npm run test:lit && npm run test:preact && npm run test:qwik && npm run test:react && npm run test:solid && npm run test:svelte && npm run test:vanilla && npm run test:vanjs && npm run test:vue",
+ "test:all": "npm run test:alpine && npm run test:angular && npm run test:jquery && npm run test:lit && npm run test:lume-js && npm run test:preact && npm run test:qwik && npm run test:react && npm run test:solid && npm run test:svelte && npm run test:vanilla && npm run test:vanjs && npm run test:vue",
"test:alpine": "npx playwright test --config=tests/config/playwright-alpine.config.js --reporter=list",
- "test:lume-js": "npx playwright test --config=tests/config/playwright-lume-js.config.js --reporter=list",
"test:angular": "npx playwright test --config=tests/config/playwright-angular.config.js --reporter=list",
"test:jquery": "npx playwright test --config=tests/config/playwright-jquery.config.js --reporter=list",
"test:lit": "npx playwright test --config=tests/config/playwright-lit.config.js --reporter=list",
+ "test:lume-js": "npx playwright test --config=tests/config/playwright-lume-js.config.js --reporter=list",
"test:preact": "npx playwright test --config=tests/config/playwright-preact.config.js --reporter=list",
"test:qwik": "npx playwright test --config=tests/config/playwright-qwik.config.js --reporter=list",
"test:react": "npx playwright test --config=tests/config/playwright-react.config.js --reporter=list",
@@ -64,10 +64,10 @@
"// LINT COMMANDS": "------------------------------------------------------",
"lint:all": "npm run lint:alpine && npm run lint:angular && npm run lint:jquery && npm run lint:lit && npm run lint:lume-js && npm run lint:preact && npm run lint:qwik && npm run lint:react && npm run lint:solid && npm run lint:svelte && npm run lint:vanilla && npm run lint:vanjs && npm run lint:vue",
"lint:alpine": "eslint 'apps/alpine/**/*.js'",
- "lint:lume-js": "eslint 'apps/lume-js/**/*.js'",
"lint:angular": "eslint 'apps/angular/**/*.{ts,html}'",
"lint:jquery": "eslint 'apps/jquery/**/*.js'",
"lint:lit": "eslint 'apps/lit/**/*.js'",
+ "lint:lume-js": "eslint 'apps/lume-js/**/*.js'",
"lint:preact": "eslint 'apps/preact/**/*.{js,jsx}'",
"lint:qwik": "eslint 'apps/qwik/**/*.{ts,tsx}'",
"lint:react": "eslint 'apps/react/**/*.{js,jsx}'",
diff --git a/scripts/verify/frameworks-schema.json b/scripts/verify/frameworks-schema.json
index 4d14d8db..e71dbcff 100644
--- a/scripts/verify/frameworks-schema.json
+++ b/scripts/verify/frameworks-schema.json
@@ -14,7 +14,7 @@
"properties": {
"id": {
"type": "string",
- "pattern": "^[a-z]+$",
+ "pattern": "^[a-z-]+$",
"description": "Unique lowercase identifier for the framework"
},
"name": {
@@ -28,7 +28,7 @@
},
"dir": {
"type": "string",
- "pattern": "^[a-z]+$",
+ "pattern": "^[a-z-]+$",
"description": "Directory name in apps/ folder"
},
"assetsDir": {
@@ -78,7 +78,7 @@
},
"meta": {
"type": "object",
- "required": ["emoji", "iconName", "website", "docs", "github", "color", "accentColor", "logo", "description", "longDescription", "video"],
+ "required": ["emoji", "iconName", "website", "docs", "github", "color", "accentColor", "logo", "description", "longDescription"],
"properties": {
"emoji": {
"type": "string",
@@ -90,6 +90,11 @@
"minLength": 1,
"description": "Icon name from Simple Icons"
},
+ "iconUrl": {
+ "type": "string",
+ "pattern": "^(https?://.+|)$",
+ "description": "Custom icon URL, overrides Simple Icons when set"
+ },
"website": {
"type": "string",
"format": "uri",
diff --git a/website/templates/framework.html b/website/templates/framework.html
index 9c107fc7..60f1d1f1 100644
--- a/website/templates/framework.html
+++ b/website/templates/framework.html
@@ -9,7 +9,7 @@
@@ -351,6 +351,7 @@
{{framework.exampleRealApp.title}}
Intro to {{ framework.name or framework.id.title() }}
Intro to {{ framework.name or framework.id.title() }}