Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 35 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ jobs:
php-version: '8.1'
tools: composer:v2
coverage: none
- name: Validate composer.json / composer.lock are in sync
run: composer validate --strict --no-check-publish
- name: Run composer audit
run: composer audit --locked --no-interaction

Expand All @@ -52,9 +54,12 @@ jobs:
name: Coverage (PHP 8.1)
runs-on: ubuntu-latest
timeout-minutes: 15
# Advisory: coverage data is uploaded to Coveralls but does not gate.
# Promote to required once we have a baseline % to enforce.
continue-on-error: true
# Promoted from advisory to gating in S3 of #161. The floor is set
# below the current line-coverage baseline so the gate doesn't fire
# on day one but does catch a regression. Ratchet the floor upward
# whenever a PR genuinely improves coverage.
env:
COVERAGE_FLOOR_LINES: '50'
steps:
- uses: actions/checkout@v6
- uses: ./.github/actions/setup-composer
Expand All @@ -64,6 +69,33 @@ jobs:
ini-values: pcov.directory=./includes,pcov.exclude="#(libraries|views)/#",memory_limit=512M
- name: Run PHPUnit with coverage
run: vendor/bin/phpunit --coverage-clover coverage.xml
- name: Enforce line-coverage floor
run: |
# Parse the project-level <metrics> element from the clover XML
# and compare `coveredstatements / statements` against the floor.
php -r '
$xml = simplexml_load_file("coverage.xml");
if ( ! $xml ) { fwrite( STDERR, "Failed to parse coverage.xml\n" ); exit( 2 ); }
$m = $xml->project->metrics;
$stmts = (int) $m["statements"];
$covered = (int) $m["coveredstatements"];
if ( $stmts === 0 ) { fwrite( STDERR, "No statements recorded\n" ); exit( 2 ); }
$pct = ( $covered / $stmts ) * 100;
$floor = (float) getenv( "COVERAGE_FLOOR_LINES" );
printf( "Line coverage: %.2f%% (floor: %.2f%%)\n", $pct, $floor );
// Also emit to the GitHub Actions step summary for at-a-glance review.
$summary = getenv( "GITHUB_STEP_SUMMARY" );
if ( $summary ) {
file_put_contents( $summary, sprintf(
"### Coverage\n\n| Lines | %.2f%% |\n| --- | --- |\n| Floor | %.2f%% |\n| Statements | %d / %d |\n",
$pct, $floor, $covered, $stmts
) );
}
if ( $pct < $floor ) {
fwrite( STDERR, sprintf( "::error::Line coverage %.2f%% is below the floor %.2f%%.\n", $pct, $floor ) );
exit( 1 );
}
'
- name: Upload coverage to Coveralls
uses: coverallsapp/github-action@v2
with:
Expand Down
50 changes: 50 additions & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: CodeQL

on:
pull_request:
branches: [main]
push:
branches: [main]
schedule:
# Weekly catch-all run so newly-disclosed query packs surface on
# unchanged code too. Monday 03:00 UTC keeps it off business hours.
- cron: '0 3 * * 1'

permissions:
contents: read
security-events: write
actions: read

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
analyze:
name: Analyze (${{ matrix.language }})
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
# CodeQL doesn't ship a PHP analyzer (Aug 2025: js/ts/py/java/go/c#/
# c++/swift/kotlin/ruby only). The PHP side of the codebase is
# covered by PHPStan + composer audit + WPCS. If CodeQL adds PHP
# later, add it here.
language: ['javascript']
steps:
- uses: actions/checkout@v6
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
# `security-extended` enables the extended security pack;
# `security-and-quality` would also add maintainability rules
# which are noisy on a WP plugin. Pick the security-only one.
queries: security-extended
- name: Autobuild
uses: github/codeql-action/autobuild@v3
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: '/language:${{ matrix.language }}'
62 changes: 62 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
name: Lint

on:
pull_request:
branches: [main]

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
js:
name: ESLint on assets/js
runs-on: ubuntu-latest
# Gating from day one — the codebase has 28 source files and 0 errors
# after S1 of #161 (only 9 unused-vars warnings remain, which don't gate).
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run ESLint
run: npm run lint:js

jstest:
name: Vitest on tests/js
runs-on: ubuntu-latest
# Gating from day one — these tests are NEW (no legacy to clean up).
# They cover the pure helpers in ffc-geofence-frontend.js /
# ffc-geofence-admin.js shipped via #160 (S2 of #161).
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run Vitest
run: npm run test:js

css:
name: Stylelint on assets/css
runs-on: ubuntu-latest
# Was an `npm run lint:css` script with no CI surface. Promoted to a
# gated job in S4-extras of #161. If it surfaces a wave of legacy
# violations, drop to non-gating via `continue-on-error: true` and
# chip away — the script and config are correct, only the policy
# changes.
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run Stylelint
run: npm run lint:css
6 changes: 5 additions & 1 deletion assets/js/ffc-admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,11 @@
var $status = $('#ffc_gen_status');
var originalText = $btn.text();

var strings = (typeof ffc_ajax !== 'undefined' && ffc_ajax.strings) ? ffc_ajax.strings : {};
// `strings` is declared earlier in this function (line ~80) — `var`
// hoists to function scope, so we just re-assign here without
// redeclaring (the line-80 path only runs when quantity is invalid
// and the function returns; this path needs its own assignment).
strings = (typeof ffc_ajax !== 'undefined' && ffc_ajax.strings) ? ffc_ajax.strings : {};
var generatingText = strings.generating || 'Generating...';
var generatingTicketsText = strings.generatingTickets || 'Generating tickets...';

Expand Down
Loading
Loading