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 @@

๐ŸŒˆ Framework Benchmarks

- 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 @@

### Intro -I've built the same weather app in 10 different frontend web frameworks. +I've built the same weather app in 12 different frontend web frameworks. Along with automated scripts to benchmark each of their performance, quality and capabilities. To finally answer the age-old question: "Which is the _best_* frontend framework?"
So, without further ado, let's see how every framework weathers the storm! โ›ˆ๏ธ @@ -29,13 +29,16 @@ So, without further ado, let's see how every framework weathers the storm! โ›ˆ - [Frameworks Covered](#frameworks-covered) - [Usage Guide](#usage) - [Project Outline](#project-outline) -- [Requirement Spec](#requirement-spec) -- [Benchmarking](#benchmarking) - [Results](#results) - [Real-world Applications](#side-note) - [Status](#status) +- [Requirement Spec](#requirement-spec) - [Attributions and License](#attributions) +> [!TIP] +> Choosing a framework for your next project? I've also built Stack Match, a comparison tool to help you pick the right framework based on your requirements. +> Check it out at [stack-match.as93.net](https://stack-match.as93.net/) :) + --- ## Frameworks Covered @@ -64,8 +67,7 @@ So, without further ado, let's see how every framework weathers the storm! โ›ˆ ### Prerequisites -You'll need to ensure you've got -Git, Node (LTS or v22+), Python (3.10) and uv installed +You'll need to ensure you've got Git, Node (v22+) and Python (3.10+) installed ### Setup @@ -90,15 +92,18 @@ You should also verify the lint checks pass, with `npm run lint` or `npm run lin ### Deploying Build the app for production, with `npm run build:[app-name]`
-Then upload `./apps/[app-name]/dist/` to any web server, CDN or static hosting provider +Then upload the app's build output (`dist/`, `build/` or the app root, depending on the framework) to any web server, CDN or static hosting provider -### Adding a Framework -1. Create app directory: `apps/your-framework/` with `package.json`, `vite.config.js`, and a `src/` dir -2. Build your app (ensuring it meets the [requirements spec](#requirement-spec) above) -3. Update [`frameworks.json`](https://github.com/lissy93/framework-benchmarks/blob/main/frameworks.json) -4. Add a test config file in `tests/config/` -6. Them run `node scripts/setup/generate-scripts.js` and `node scripts/setup/sync-assets.js` +### Adding a Framework +1. Create app directory: `apps/[app-name]/` with `package.json`, a build config (e.g. `vite.config.js`), and a `src/` dir +2. Register the framework in [`frameworks.json`](https://github.com/lissy93/framework-benchmarks/blob/main/frameworks.json) +3. Add a test config at `tests/config/playwright-[app-name].config.js` +4. Run `npm run setup` to generate scripts, sync shared assets and mocks, and install deps. Verify with `npm run check` +5. Code your app!
+ 5.1. Preview locally with `npm run dev:[app-name]`
+ 5.2. then test with `npm run test:[app-name]` to ensure it meets the [requirements spec](#requirement-spec) +6. Validate everything passes with the test, lint and build scripts --- @@ -113,6 +118,7 @@ framework-benchmarks โ”‚ โ”œโ”€โ”€ icons # SVG icons, used by all apps โ”‚ โ”œโ”€โ”€ styles # CSS classes and variables, used by all apps โ”‚ โ””โ”€โ”€ mocks # Mocked data, used by apps when running benchmarks +โ”œโ”€โ”€ website # Source templates for the results website โ”œโ”€โ”€ tests # Test suit โ””โ”€โ”€ apps # Directory for each app as a standalone project โ”œโ”€โ”€ react/ @@ -126,7 +132,6 @@ The **[`scripts/`](https://github.com/lissy93/framework-benchmarks/tree/main/scr everything for managing the project (setup, testing, benchmarking, reporting, etc). You can view a list of scripts by running `npm run help`. - ### Shared Assets To keep things uniform, all apps will share certain assets @@ -138,7 +143,6 @@ To keep things uniform, all apps will share certain assets - **Dependencies**: Beyond their framework code, none of the apps use any additional dependencies, libraries or third-party "stuff" - **Data**: Apps support using real weather data, from [open-meteo api](https://open-meteo.com). However, to keep tests fair, we use mocked data when running benchmarks. - ### Commands - `npm run setup` - Creates mock data, syncs assets, updates scripts and installs dependencies @@ -149,9 +153,9 @@ To keep things uniform, all apps will share certain assets - `npm run start` - Starts the demo server, which serves up all built apps - `npm run help` - Displays a list of all available commands -See the [`package.json`](https://github.com/lissy93/framework-benchmarks/blob/main/package.json) for all commands +See the [`package.json`](https://github.com/lissy93/framework-benchmarks/blob/main/package.json) for all commands, and `npm run help` for details. -Note that the project commands get generated automatically by the [`generate_scripts.py`](https://github.com/lissy93/framework-benchmarks/blob/main/scripts/setup/generate_scripts.py) script, based on the contents of [`frameworks.json`](https://github.com/lissy93/framework-benchmarks/blob/main/frameworks.json) and [`config.json`](https://github.com/lissy93/framework-benchmarks/blob/main/config.json). +Note that the project commands get generated automatically by the [`generate_scripts.py`](https://github.com/lissy93/framework-benchmarks/blob/main/scripts/setup/generate_scripts.py) script, based on the contents of [`frameworks.json`](https://github.com/lissy93/framework-benchmarks/blob/main/frameworks.json) and [`config.json`](https://github.com/lissy93/framework-benchmarks/blob/main/config.json). That's what `npm run setup` is for. --- @@ -202,7 +206,7 @@ and also view a stats on a per-framework basis. ## Side note Different frameworks shine in different ways, and therefore have very different usecases.
-So, in order to let each one shine, I have I have built real-world apps in each framework. +So to properly demonstrate each frameworks ideal usecase, I've also built a real-world app in each framework. | Project | Framework | GitHub | Website | @@ -210,12 +214,12 @@ So, in order to let each one shine, I have I have built real-world apps in each | [ Web Check](https://github.com/Lissy93/web-check) - All-in-one OSINT tool for analyzing any site | [![React](https://img.shields.io/static/v1?label=&message=React&color=61DAFB&logo=react&logoColor=FFFFFF)](https://react.dev/) | [![GitHub Repo stars](https://img.shields.io/github/stars/Lissy93/web-check)](https://github.com/Lissy93/web-check) | [๐ŸŒ web-check.xyz](https://web-check.xyz) | | [ Dashy](https://github.com/Lissy93/dashy) - Highly configurable self-hostable server dashboard | [![Vue.js](https://img.shields.io/static/v1?label=&message=Vue.js&color=4FC08D&logo=vuedotjs&logoColor=FFFFFF)](https://vuejs.org/) | [![GitHub Repo stars](https://img.shields.io/github/stars/Lissy93/dashy)](https://github.com/Lissy93/dashy) | [๐ŸŒ dashy.to](https://dashy.to) | | [ Digital Defense](https://github.com/Lissy93/personal-security-checklist) - Interactive personal security checklist | [![Qwik](https://img.shields.io/static/v1?label=&message=Qwik&color=ac7ef4&logo=qwik&logoColor=FFFFFF)](https://qwik.builder.io/) | [![GitHub Repo stars](https://img.shields.io/github/stars/Lissy93/personal-security-checklist)](https://github.com/Lissy93/personal-security-checklist) | [๐ŸŒ digital-defense.io](https://digital-defense.io) | -| [ Networking Toolbox](https://github.com/Lissy93/networking-toolbox) - 100+ offline-first networking tools for sysadmins | [![Svelte](https://img.shields.io/static/v1?label=&message=Svelte&color=ff3e00&logo=svelte&logoColor=FFFFFF)](https://svelte.dev/) | [![GitHub Repo stars](https://img.shields.io/github/stars/Lissy93/networking-toolbox)](https://github.com/Lissy93/networking-toolbox) | [๐ŸŒ networkingtoolbox.net](https://networkingtoolbox.net/) | +| [ Networking Toolbox](https://github.com/Lissy93/networking-toolbox) - offline-first net utils for sysadmins | [![Svelte](https://img.shields.io/static/v1?label=&message=Svelte&color=ff3e00&logo=svelte&logoColor=FFFFFF)](https://svelte.dev/) | [![GitHub Repo stars](https://img.shields.io/github/stars/Lissy93/networking-toolbox)](https://github.com/Lissy93/networking-toolbox) | [๐ŸŒ networkingtoolbox.net](https://networkingtoolbox.net/) | +| [ Awesome Privacy](https://github.com/Lissy93/awesome-privacy) - Curated directory of respectful apps | [![Astro](https://img.shields.io/static/v1?label=&message=Astro&color=E83CB9&logo=astro&logoColor=FFFFFF)](https://astro.build/) | [![GitHub Repo stars](https://img.shields.io/github/stars/Lissy93/awesome-privacy)](https://github.com/Lissy93/awesome-privacy) | [๐ŸŒ awesome-privacy.xyz](https://awesome-privacy.xyz/) | | [ Domain Locker](https://github.com/Lissy93/domain-locker) - Domain name portfolio manager | [![Angular](https://img.shields.io/static/v1?label=&message=Angular&color=DD0031&logo=angular&logoColor=FFFFFF)](https://angular.io/) | [![GitHub Repo stars](https://img.shields.io/github/stars/Lissy93/domain-locker)](https://github.com/Lissy93/domain-locker) | [๐ŸŒ domain-locker.com](https://domain-locker.com) | | [ Email Comparison](https://github.com/Lissy93/email-comparison) - Objective testing of mail providers | [![Lit](https://img.shields.io/static/v1?label=&message=Lit&color=00ffff&logo=lit&logoColor=FFFFFF)](https://lit.dev/) | [![GitHub Repo stars](https://img.shields.io/github/stars/Lissy93/email-comparison)](https://github.com/Lissy93/email-comparison) | [๐ŸŒ email-comparison](https://email-comparison.as93.net/) | | [ Who Dat](https://github.com/Lissy93/who-dat) - WHOIS lookup for domain registration info | [![Alpine.js](https://img.shields.io/static/v1?label=&message=Alpine.js&color=8BC0D0&logo=alpinedotjs&logoColor=FFFFFF)](https://alpinejs.dev/) | [![GitHub Repo stars](https://img.shields.io/github/stars/Lissy93/who-dat)](https://github.com/Lissy93/who-dat) | [๐ŸŒ who-dat.as93.net](https://who-dat.as93.net) | | [ Chief Snack Officer](https://github.com/Lissy93/cso) - Office snack management app | [![Solid](https://img.shields.io/static/v1?label=&message=Solid&color=2C4F7C&logo=solid&logoColor=FFFFFF)](https://www.solidjs.com/) | [![GitHub Repo stars](https://img.shields.io/github/stars/Lissy93/cso)](https://github.com/Lissy93/cso) | [๐ŸŒ N/A](https://lissy93.github.io/cso) | -| [ Awesome Privacy](https://github.com/Lissy93/awesome-privacy) - Curated directory of respectful apps | [![Astro](https://img.shields.io/static/v1?label=&message=Astro&color=E83CB9&logo=astro&logoColor=FFFFFF)](https://astro.build/) | [![GitHub Repo stars](https://img.shields.io/github/stars/Lissy93/awesome-privacy)](https://github.com/Lissy93/awesome-privacy) | [๐ŸŒ awesome-privacy.xyz](https://awesome-privacy.xyz/) | | [ RAID Calculator](https://github.com/Lissy93/raid-calculator) - RAID array capacity and fault tolerance | [![Van.js](https://img.shields.io/static/v1?label=&message=Van.js&color=F44336&logo=vitess&logoColor=FFFFFF)](https://vanjs.org/) | [![GitHub Repo stars](https://img.shields.io/github/stars/Lissy93/raid-calculator)](https://github.com/Lissy93/raid-calculator) | [๐ŸŒ raid-calculator](https://raid-calculator.as93.net/) | | [ Permissionator](https://github.com/Lissy93/permissionator) - Generating Linux file permissions | [![Marko](https://img.shields.io/static/v1?label=&message=Marko&color=2596BE&logo=marko&logoColor=FFFFFF)](https://markojs.com/) | [![GitHub Repo stars](https://img.shields.io/github/stars/Lissy93/permissionator)](https://github.com/Lissy93/permissionator) | [๐ŸŒ permissionator](https://permissionator.as93.net) | @@ -232,11 +236,13 @@ Each app gets built and tested to ensure that it is functional, compliant with t | **Lint**: Ensures lint/consistency checks pass | [![๐Ÿงผ Lint](https://github.com/Lissy93/framework-benchmarks/actions/workflows/lint.yml/badge.svg)](https://github.com/Lissy93/framework-benchmarks/actions/workflows/lint.yml) | | **Benchmark**: Executes all app benchmarks | [![๐Ÿ“ˆ Benchmark](https://github.com/Lissy93/framework-benchmarks/actions/workflows/benchmark.yml/badge.svg)](https://github.com/Lissy93/framework-benchmarks/actions/workflows/benchmark.yml) | | **Transform**: Formats and publishes results | [![๐Ÿ”„ Transform Results](https://github.com/Lissy93/framework-benchmarks/actions/workflows/transform-results.yml/badge.svg)](https://github.com/Lissy93/framework-benchmarks/actions/workflows/transform-results.yml) | +| **CI**: Runs checks on PRs to ensure all good | [![๐Ÿšฆ CI](https://github.com/lissy93/framework-benchmarks/actions/workflows/ci.yml/badge.svg)](https://github.com/lissy93/framework-benchmarks/actions/workflows/ci.yml) | | **Docker**: Builds and publishes the image | [![๐Ÿณ Build & Publish Docker Image](https://github.com/Lissy93/framework-benchmarks/actions/workflows/docker.yml/badge.svg)](https://github.com/Lissy93/framework-benchmarks/actions/workflows/docker.yml) | +| **Tag**: Bumps version and tags on merge | [![๐Ÿ”– Tag](https://github.com/Lissy93/framework-benchmarks/actions/workflows/tag.yml/badge.svg)](https://github.com/Lissy93/framework-benchmarks/actions/workflows/tag.yml) | +| **Release**: Drafts a release with assets | [![๐Ÿš€ Release](https://github.com/Lissy93/framework-benchmarks/actions/workflows/release.yml/badge.svg)](https://github.com/Lissy93/framework-benchmarks/actions/workflows/release.yml) | | **Docs**: Updates dynamic info in markdown | [![๐Ÿ“„ Update readme](https://github.com/Lissy93/framework-benchmarks/actions/workflows/update-docs.yml/badge.svg)](https://github.com/Lissy93/framework-benchmarks/actions/workflows/update-docs.yml) | | **Mirror**: Syncs repo to Codeberg mirror | [![๐Ÿชž Mirror to Codeberg](https://github.com/Lissy93/framework-benchmarks/actions/workflows/mirror.yml/badge.svg)](https://github.com/Lissy93/framework-benchmarks/actions/workflows/mirror.yml) | - | App | Build | Test | Lint | @@ -284,14 +290,14 @@ For our app to be somewhat complete and useful, it must do the following: There's certain standards every app should follow, and we want to use best practices, so: - Theming: The app should support both light and dark mode, based on the user's preferences - Internationalization: The copy should be extracted out of the code, so that it is translatable -- Accessibility: The app should meet AA standard of accessibility +- Accessibility: The app should meet AA standard of WCAG in line with the EAA - Mobile: The app should be fully responsive and optimized for mobile - Performance: The app should be efficiently coded as best as the framework allows -- Testing: The app should meet 90% test coverage -- Error Handling: Errors should be handled, correctly surfaced, and tracible -- Quality: The code should be linted for consistent formatting -- Security: Inputs must be validated, data via HTTPS, and no known vulnerabilities -- SEO: Basic meta and og tags, SSR where possible, +- Testing: Core functionality, logic and complexity should be tested, aiming for 90% coverage +- Error Handling: App should be robust, with errors handled gracefully and correctly surfaced +- Quality: The code should be clean, typechecked, linted and formatted consistently +- Security: Code should follow secure best practices, inline with OWASP reccomentations +- SEO: Correct semantic elements, meta and og tags and SSR compatible where applicable - CI: Automated tests, lints and validation should ensure all changes are compliant ### Benchmarking Requirements @@ -304,33 +310,38 @@ To compare the frameworks, we need to measure: - Build time & dev server HMR latency ### UI Requirements -The interface is simple, but must be identical arcorss all apps. As validated by the snapshots in the tests.
+The interface is nothing special. It's a simple form which must be identical arcorss all apps, as validated by the snapshots in the tests.
The screenshots will all look like this: +### 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 -![Sponsors](https://readme-contribs.as93.net/sponsors/lissy93?avatarSize=80&perRow=10) +[![sponsors badge](https://readme-contribs.as93.net/sponsors/lissy93?shape=squircle)](https://github.com/sponsors/lissy93) ### Contributors -![Contributors](https://readme-contribs.as93.net/contributors/lissy93/framework-benchmarks?avatarSize=80&perRow=10) +[![contributors badge](https://readme-contribs.as93.net/contributors/lissy93/framework-benchmarks?shape=squircle)](https://github.com/lissy93/framework-benchmarks/graphs/contributors) ### Stargzers -![Stargazers](https://readme-contribs.as93.net/stargazers/lissy93/framework-benchmarks?perRow=16&limit=64) +[![stargazers badge](https://readme-contribs.as93.net/stargazers/lissy93/framework-benchmarks?perRow=16&shape=squircle)](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._
> For information, see TLDR Legal > MIT @@ -339,7 +350,7 @@ The screenshots will all look like this: ``` The MIT License (MIT) -Copyright (c) Alicia Sykes +Copyright (c) Alicia Sykes Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -363,7 +374,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

- ยฉ 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 @@

{{ framework.meta.emoji or '๐Ÿ”ง' }} @@ -351,6 +351,7 @@

{{framework.exampleRealApp.title}}

+ {% if framework.meta.video %}

Intro to {{ framework.name or framework.id.title() }}

@@ -363,6 +364,7 @@

Intro to {{ framework.name or framework.id.title() }}

+ {% endif %} {% if framework_commentary.about %} diff --git a/website/templates/homepage.html b/website/templates/homepage.html index 4f794463..f5d2d195 100644 --- a/website/templates/homepage.html +++ b/website/templates/homepage.html @@ -44,7 +44,7 @@

Framework Benchmarks

> {{ framework.meta.emoji or '๐Ÿ”ง' }} @@ -203,7 +203,7 @@

Frameworks

{{ framework.meta.emoji or '๐Ÿ”ง' }}