mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-09 01:16:15 -08:00
Third step of the performance-journeys ratchet, stacked on #1693 (which is stacked on #1692; merge in order, GitHub retargets each to `master`). - New `Initial bytes ratchet` job in `.github/workflows/ci.yml` (ubuntu-latest): install, `pnpm nx build web --skip-nx-cache` (production configuration, the one users download), then `pnpm run perf:initial-bytes:check`. The job fails when `renderer.initialBytes` exceeds `tools/performance/journey-baselines.json`. - `dist/performance/` is uploaded as the `performance-journey-summary` artifact on every run, so a failing or tightenable run carries its evidence. - After review: the job first runs the new `tools/performance/check-baseline-direction.mjs`, which compares `journey-baselines.json` with the revision the change is measured against (the target branch of a pull request, `github.event.before` for a `master` push, `master` for a manual dispatch) and fails on any raised enforced limit (`value × toleranceRatio`), any widened or newly added tolerance, or any removed entry, so a PR cannot grow the payload and raise the baseline to match (lowered limits and new entries pass; a target branch without the file has nothing to weaken). Node tests cover it. - Docs: the performance-journeys contract and the validation map name the job, and the contract now states that this runner is the canonical measurer (take baseline values from its output, not from a local build). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
282 lines
11 KiB
YAML
282 lines
11 KiB
YAML
name: CI
|
|
|
|
on:
|
|
push:
|
|
branches:
|
|
- master
|
|
pull_request:
|
|
branches:
|
|
- master
|
|
workflow_dispatch:
|
|
|
|
# Superseded PR pushes cancel their still-running checks. Non-PR runs get a
|
|
# unique group (run_id) because GitHub keeps at most one pending run per
|
|
# group even with cancel-in-progress: false — a shared ref group would let a
|
|
# rapid master push silently replace a queued sibling and leave a merged
|
|
# commit without a lint/test record.
|
|
concurrency:
|
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
|
|
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
jobs:
|
|
actionlint:
|
|
name: Workflow lint
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 10
|
|
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@v7
|
|
|
|
# Image pinned by digest (tag 1.7.12). False positives are
|
|
# suppressed in .github/actionlint.yaml; shellcheck runs at
|
|
# warning+ severity so style/info notes in long release scripts
|
|
# don't fail CI while real quoting/logic bugs still do.
|
|
- name: Run actionlint
|
|
uses: docker://rhysd/actionlint:1.7.12@sha256:b1934ee5f1c509618f2508e6eb47ee0d3520686341fec936f3b79331f9315667
|
|
with:
|
|
args: -color
|
|
env:
|
|
SHELLCHECK_OPTS: --severity=warning
|
|
|
|
release-note-gate:
|
|
name: Release note gate
|
|
if: github.event_name == 'pull_request'
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 10
|
|
permissions:
|
|
contents: read
|
|
pull-requests: read
|
|
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@v7
|
|
|
|
# The gate scripts are dependency-free Node, so this job skips
|
|
# pnpm install entirely and stays cheap.
|
|
- name: Validate release note format
|
|
run: node tools/release/build-release-notes.mjs --validate
|
|
|
|
# Labels are fetched live rather than read from the (stale) event
|
|
# payload, so applying `no-release-note` and re-running the check
|
|
# works without a new push. Policy lives in a unit-tested script,
|
|
# not in workflow bash.
|
|
- name: Require a release note for user-visible changes
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
PR_NUMBER: ${{ github.event.pull_request.number }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/files?per_page=100" \
|
|
--paginate --jq '[.[] | {filename, status}]' |
|
|
jq -s 'add // []' > /tmp/pr-files.json
|
|
|
|
gh api "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/labels?per_page=100" \
|
|
--paginate --jq '[.[].name]' |
|
|
jq -s 'add // []' > /tmp/pr-labels.json
|
|
|
|
jq -n \
|
|
--slurpfile files /tmp/pr-files.json \
|
|
--slurpfile labels /tmp/pr-labels.json \
|
|
'{files: $files[0], labels: $labels[0]}' |
|
|
node tools/release/check-release-note-gate.mjs
|
|
|
|
lint:
|
|
name: Lint
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 30
|
|
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@v7
|
|
with:
|
|
# nx affected needs the merge-base with the PR target branch.
|
|
fetch-depth: 0
|
|
|
|
- name: Install pnpm
|
|
uses: pnpm/action-setup@v6.0.10
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v7
|
|
with:
|
|
node-version-file: '.nvmrc'
|
|
cache: 'pnpm'
|
|
|
|
- name: Install dependencies
|
|
run: pnpm install --frozen-lockfile
|
|
|
|
# PRs lint only affected projects for faster feedback; root config
|
|
# or lockfile changes make every project affected, so the
|
|
# module-boundary and max-lines rules cannot be dodged this way.
|
|
- name: Lint affected projects (PR)
|
|
if: github.event_name == 'pull_request'
|
|
run: pnpm nx affected --target=lint --base=origin/${{ github.base_ref }} --head=HEAD --parallel=3 --output-style=static
|
|
env:
|
|
CI: true
|
|
NX_TASKS_RUNNER_DYNAMIC_OUTPUT: false
|
|
|
|
- name: Lint all projects (master)
|
|
if: github.event_name != 'pull_request'
|
|
run: pnpm nx run-many --target=lint --all --parallel=3 --output-style=static
|
|
env:
|
|
CI: true
|
|
NX_TASKS_RUNNER_DYNAMIC_OUTPUT: false
|
|
|
|
initial-bytes-ratchet:
|
|
name: Initial bytes ratchet
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 30
|
|
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@v7
|
|
|
|
- name: Install pnpm
|
|
uses: pnpm/action-setup@v6.0.10
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v7
|
|
with:
|
|
node-version-file: '.nvmrc'
|
|
cache: 'pnpm'
|
|
|
|
- name: Install dependencies
|
|
run: pnpm install --frozen-lockfile
|
|
|
|
# The ratchet below compares a measurement with the baselines file
|
|
# of the same commit, so it cannot see a change that grows the
|
|
# payload and raises the baseline to match. Compare the file with
|
|
# the revision this one is measured against instead: the target
|
|
# branch for a pull request, the previous head for a master push,
|
|
# master for a manual dispatch. Any raised limit, widened tolerance
|
|
# or removed entry fails.
|
|
- name: Refuse baseline increases against the previous revision
|
|
env:
|
|
EVENT_NAME: ${{ github.event_name }}
|
|
BASE_REF: ${{ github.base_ref }}
|
|
BEFORE_SHA: ${{ github.event.before }}
|
|
run: |
|
|
set -euo pipefail
|
|
case "$EVENT_NAME" in
|
|
pull_request)
|
|
git fetch --no-tags --depth=1 origin "$BASE_REF"
|
|
;;
|
|
push)
|
|
if [ -z "$BEFORE_SHA" ] || [ "$BEFORE_SHA" = "0000000000000000000000000000000000000000" ]; then
|
|
echo "No previous revision for this push; nothing to compare against."
|
|
exit 0
|
|
fi
|
|
git fetch --no-tags --depth=1 origin "$BEFORE_SHA"
|
|
;;
|
|
*)
|
|
git fetch --no-tags --depth=1 origin master
|
|
;;
|
|
esac
|
|
git show "FETCH_HEAD:tools/performance/journey-baselines.json" > /tmp/base-journey-baselines.json 2>/dev/null ||
|
|
rm -f /tmp/base-journey-baselines.json
|
|
node tools/performance/check-baseline-direction.mjs \
|
|
--base /tmp/base-journey-baselines.json \
|
|
--head tools/performance/journey-baselines.json
|
|
|
|
# The production configuration is what users download; measuring
|
|
# any other build would ratchet a number nobody ships.
|
|
- name: Build web app (production)
|
|
run: pnpm nx build web --skip-nx-cache
|
|
env:
|
|
CI: true
|
|
NX_TASKS_RUNNER_DYNAMIC_OUTPUT: false
|
|
|
|
# Fails when renderer.initialBytes exceeds the value committed in
|
|
# tools/performance/journey-baselines.json. Baselines only move
|
|
# down, with the printed measurement as evidence; the contract is
|
|
# docs/architecture/performance-journeys.md.
|
|
- name: Check renderer.initialBytes against the baseline
|
|
run: pnpm run perf:initial-bytes:check
|
|
|
|
- name: Upload journey summary
|
|
if: always()
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: performance-journey-summary
|
|
path: dist/performance/
|
|
retention-days: 14
|
|
|
|
unit-and-typecheck:
|
|
name: Unit Tests and Typechecks
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 45
|
|
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@v7
|
|
|
|
- name: Install pnpm
|
|
uses: pnpm/action-setup@v6.0.10
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v7
|
|
with:
|
|
node-version-file: '.nvmrc'
|
|
cache: 'pnpm'
|
|
|
|
- name: Install dependencies
|
|
run: pnpm install --frozen-lockfile
|
|
|
|
- name: Validate agent guidance
|
|
run: pnpm run agents:validate
|
|
|
|
- name: Validate Nx dependency version policy
|
|
run: pnpm run deps:nx:validate
|
|
|
|
- name: Validate Vite dev-server transform filter patch
|
|
run: pnpm run deps:vite:test
|
|
|
|
- name: Validate electron-builder keychain password patch
|
|
run: pnpm run deps:electron-builder:test
|
|
|
|
- name: Validate stylesheet Nx inputs
|
|
run: pnpm run styles:inputs:validate
|
|
|
|
- name: Typecheck web and Electron entry points
|
|
run: pnpm run typecheck:ci
|
|
|
|
- name: Check i18n drift
|
|
run: pnpm run i18n:check
|
|
|
|
- name: Run Tier A unit coverage suite
|
|
run: pnpm run coverage:ci
|
|
env:
|
|
CI: true
|
|
NX_TASKS_RUNNER_DYNAMIC_OUTPUT: false
|
|
|
|
- name: Run Tier B/C validation commands
|
|
run: node tools/coverage/check-coverage-policy.mjs --run-non-tier-a
|
|
env:
|
|
CI: true
|
|
NX_TASKS_RUNNER_DYNAMIC_OUTPUT: false
|
|
|
|
- name: Upload unit coverage artifact
|
|
if: always()
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: unit-coverage
|
|
path: |
|
|
coverage/merged/
|
|
retention-days: 14
|
|
|
|
- name: Upload unit coverage to Codecov
|
|
if: always()
|
|
uses: codecov/codecov-action@v7
|
|
with:
|
|
files: ./coverage/merged/lcov.info,./coverage/merged/cobertura-coverage.xml
|
|
flags: unit
|
|
name: iptvnator-unit
|
|
fail_ci_if_error: false
|
|
handle_no_reports_found: true
|
|
disable_search: true
|
|
token: ${{ secrets.CODECOV_TOKEN }}
|