Reusable GitHub Actions CI workflows for MediaWiki skins and extensions. Call them from your project's workflow file — no need to copy and maintain CI logic yourself.
Add a .github/workflows/ci.yml to your project:
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
lint:
uses: StarCitizenTools/mediawiki-ci-workflows/.github/workflows/lint.yml@main
with:
lint-php: true
lint-js: true
test-js:
uses: StarCitizenTools/mediawiki-ci-workflows/.github/workflows/test-js.yml@main
test-php:
uses: StarCitizenTools/mediawiki-ci-workflows/.github/workflows/test-php.yml@main
with:
project-type: skin # or 'extension'
project-name: MySkin # your project's directory nameYour project needs:
- PHP:
composer.jsonwithtest(PHPCS),phan, and PHPUnit scripts - JS:
package.jsonwithlint:js,lint:styles,lint:i18n,lint:mdscripts and Vitest - Coverage (optional): Vitest outputs to
coverage/js/lcov.info; PHPUnit outputs Clover XML
Not every project needs all of these — only include the workflows and inputs relevant to your stack.
Runs PHP, JS, style, i18n, markdown and type-check linters. Enable only the ones you need.
| Input | Type | Default | Description |
|---|---|---|---|
lint-php |
boolean | false |
Run composer test |
lint-js |
boolean | false |
Run npm run lint:js |
lint-styles |
boolean | false |
Run npm run lint:styles |
lint-i18n |
boolean | false |
Run npm run lint:i18n |
lint-md |
boolean | false |
Run npm run lint:md |
lint-types |
boolean | false |
Run npm run lint:types |
runner |
string | ubuntu-latest |
Runner image (e.g. ubuntu-24.04-arm for ARM) |
lint:
uses: StarCitizenTools/mediawiki-ci-workflows/.github/workflows/lint.yml@main
with:
lint-php: true
lint-js: true
lint-styles: true
lint-i18n: true
lint-md: true
lint-types: trueDownloads a MediaWiki branch, installs your project into it, and runs composer phan.
| Input | Type | Default | Description |
|---|---|---|---|
project-type |
string | required | skin or extension |
project-name |
string | required | Directory name (e.g. Citizen, TabberNeue) |
mw-branch |
string | REL1_43 |
MediaWiki branch |
php-version |
string | 8.2 |
PHP version |
extra-extensions |
string | "" |
Space-separated extra extensions for type resolution (e.g. Scribunto) |
skip-cache |
boolean | false |
Skip MW cache (for nightly runs) |
runner |
string | ubuntu-latest |
Runner image (e.g. ubuntu-24.04-arm for ARM) |
analyze-php:
uses: StarCitizenTools/mediawiki-ci-workflows/.github/workflows/analyze-php.yml@main
with:
project-type: skin
project-name: CitizenRuns Vitest with coverage and caches the results for SonarQube.
| Input | Type | Default | Description |
|---|---|---|---|
runner |
string | ubuntu-latest |
Runner image (e.g. ubuntu-24.04-arm for ARM) |
test-js:
uses: StarCitizenTools/mediawiki-ci-workflows/.github/workflows/test-js.yml@mainRuns PHPUnit across a matrix of MediaWiki branches and PHP versions.
| Input | Type | Default | Description |
|---|---|---|---|
project-type |
string | required | skin or extension |
project-name |
string | required | Directory name (e.g. Citizen, TabberNeue) |
matrix |
string | (see below) | JSON array of matrix entries |
extra-extensions |
string | "" |
Space-separated extra extensions to download and load (e.g. Scribunto) |
skip-cache |
boolean | false |
Skip MW cache (for nightly runs) |
runner |
string | ubuntu-latest |
Runner image (e.g. ubuntu-24.04-arm for ARM) |
Default matrix:
[
{"mw": "REL1_43", "php": "8.2", "coverage": "xdebug", "experimental": false},
{"mw": "REL1_44", "php": "8.3", "coverage": "none", "experimental": false},
{"mw": "REL1_45", "php": "8.4", "coverage": "none", "experimental": false},
{"mw": "REL1_46", "php": "8.5", "coverage": "none", "experimental": false},
{"mw": "master", "php": "8.5", "coverage": "none", "experimental": true}
]| Field | Description |
|---|---|
mw |
MediaWiki branch |
php |
PHP version |
coverage |
xdebug to generate coverage, none to skip |
experimental |
true allows failure without failing the workflow |
test-php:
uses: StarCitizenTools/mediawiki-ci-workflows/.github/workflows/test-php.yml@main
with:
project-type: skin
project-name: CitizenTo customize the matrix:
test-php:
uses: StarCitizenTools/mediawiki-ci-workflows/.github/workflows/test-php.yml@main
with:
project-type: extension
project-name: TabberNeue
matrix: >
[
{"mw": "REL1_43", "php": "8.2", "coverage": "xdebug", "experimental": false},
{"mw": "REL1_44", "php": "8.3", "coverage": "none", "experimental": false}
]Runs MediaWiki's parserTests.php against every .txt file in your project's tests/parser/ directory across a matrix of MediaWiki branches and PHP versions. The job is a no-op if the directory has no parser tests, so it's safe to wire up unconditionally.
| Input | Type | Default | Description |
|---|---|---|---|
project-type |
string | required | skin or extension |
project-name |
string | required | Directory name (e.g. Citizen, TabberNeue) |
matrix |
string | (see below) | JSON array of matrix entries |
extra-extensions |
string | "" |
Space-separated extra extensions to download and load (e.g. Scribunto) |
skip-cache |
boolean | false |
Skip MW cache (for nightly runs) |
runner |
string | ubuntu-latest |
Runner image (e.g. ubuntu-24.04-arm for ARM) |
Default matrix:
[
{"mw": "REL1_43", "php": "8.2", "experimental": false},
{"mw": "REL1_44", "php": "8.3", "experimental": false},
{"mw": "REL1_45", "php": "8.4", "experimental": false},
{"mw": "REL1_46", "php": "8.5", "experimental": false},
{"mw": "master", "php": "8.5", "experimental": true}
]| Field | Description |
|---|---|
mw |
MediaWiki branch |
php |
PHP version |
experimental |
true allows failure without failing the workflow |
test-parser:
uses: StarCitizenTools/mediawiki-ci-workflows/.github/workflows/test-parser.yml@main
with:
project-type: extension
project-name: TabberNeueRuns a SonarQube scan with coverage data from test-js and test-php. Coverage is cached per branch, so SonarQube always has data for both JS and PHP even when only one test suite ran.
| Input | Type | Default | Description |
|---|---|---|---|
has-js-coverage |
boolean | false |
Whether JS coverage was generated this run |
has-php-coverage |
boolean | false |
Whether PHP coverage was generated this run |
enabled |
boolean | true |
Set false to skip |
runner |
string | ubuntu-latest |
Runner image (e.g. ubuntu-24.04-arm for ARM) |
Requires a SONAR_TOKEN secret (optional — the scan is skipped automatically if the token is unavailable, e.g. on fork PRs). Set it up at SonarCloud > Your project > Administration > Analysis Method.
Your project also needs a sonar-project.properties file. See SonarCloud docs.
sonarqube:
needs: [test-js, test-php]
if: always() && !failure()
uses: StarCitizenTools/mediawiki-ci-workflows/.github/workflows/sonarqube.yml@main
with:
has-js-coverage: ${{ needs.test-js.result == 'success' }}
has-php-coverage: ${{ needs.test-php.result == 'success' }}
secrets:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}Checks npm for newer versions of the packages pinned in your project's foreign-resources.yaml, regenerates the bundled files with MediaWiki's manageForeignResources maintenance script, and opens a pull request with the result — a scheduled Dependabot-style bot for foreign resources, which Dependabot itself cannot track.
Only components whose src points at registry.npmjs.org are updated; others are skipped with a notice. Comments and formatting in the yaml are preserved.
| Input | Type | Default | Description |
|---|---|---|---|
project-type |
string | extension |
skin or extension |
project-name |
string | required | Directory name (e.g. FloatingUI, AGGrid) |
foreign-resources-dir |
string | modules/lib |
Directory containing foreign-resources.yaml, relative to the project root |
extra-extensions |
string | "" |
Space-separated extra extensions to download and load (e.g. Scribunto), for projects whose extension.json requires them |
mw-branch |
string | REL1_46 |
MediaWiki branch used to run manageForeignResources |
php-version |
string | 8.4 |
PHP version |
pr-branch |
string | update-foreign-resources |
Head branch for the update pull request |
pr-title-prefix |
string | build(deps) |
Conventional-commit prefix for the PR title and commit message; the rest is generated from the bumped packages (e.g. build(deps): bump @floating-ui/dom from 1.7.6 to 1.8.0) |
runner |
string | ubuntu-latest |
Runner image (e.g. ubuntu-24.04-arm for ARM) |
Notes:
- Trigger it from the caller on
scheduleandworkflow_dispatch— it is not meant to run on pushes or pull requests. - Unlike the test workflows, this one does not cache the MediaWiki install, so it takes no
skip-cacheinput. The project is checked out insidemediawiki/, so caching that tree would also cache the project's.gitand carry git state between runs. Reinstalling costs about 35s, which is not worth optimising on a weekly job. - The caller job must grant
contents: writeandpull-requests: writepermissions. - With the default
GITHUB_TOKEN, the repository setting Allow GitHub Actions to create and approve pull requests (Settings → Actions → General) must be enabled, and the created PR will not triggerpull_requestCI workflows. Pass aPR_TOKENsecret (a PAT or GitHub App token) to lift both limitations. - If your project cuts releases from conventional commits (e.g. release-please), override
pr-title-prefixwith a release-triggering type such asfix(deps). - Reference this workflow
@main. The bump script is checked out from this repository's default branch at run time, so pinning the caller to a tag or SHA would still run the latest script — pinning gives no version isolation here.
name: Update foreign resources
on:
schedule:
- cron: "0 6 * * 1"
workflow_dispatch:
jobs:
update:
uses: StarCitizenTools/mediawiki-ci-workflows/.github/workflows/update-foreign-resources.yml@main
permissions:
contents: write
pull-requests: write
with:
project-type: extension
project-name: FloatingUI
# Empty until the org defines PR_TOKEN; the workflow then falls back
# to GITHUB_TOKEN.
secrets:
PR_TOKEN: ${{ secrets.PR_TOKEN }}A complete caller workflow with change detection, conditional jobs, and nightly cache refresh:
Show full example
name: CI
on:
schedule:
- cron: "0 0 * * *"
push:
branches: [main]
pull_request:
branches: ["**"]
concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
cancel-in-progress: true
jobs:
changes:
name: Detect changes
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
php: ${{ steps.filter.outputs.php_any_changed }}
script: ${{ steps.filter.outputs.script_any_changed }}
stylesheet: ${{ steps.filter.outputs.stylesheet_any_changed }}
i18n: ${{ steps.filter.outputs.i18n_any_changed }}
markdown: ${{ steps.filter.outputs.markdown_any_changed }}
steps:
- uses: actions/checkout@v6
- uses: tj-actions/changed-files@v47
id: filter
with:
files_yaml: |
php:
- includes/**/*.php
- tests/**/*.php
- skin.json # or extension.json
- composer.json
- composer.lock
- .phan/config.php
- .github/workflows/ci.yml
script:
- resources/**/*.js
- tests/vitest/**
- vitest.config.js
- package.json
- package-lock.json
- .eslintrc.json
- .github/workflows/ci.yml
stylesheet:
- resources/**/*.css
- resources/**/*.less
- skinStyles/**/*.css
- skinStyles/**/*.less
- package.json
- package-lock.json
- .github/workflows/ci.yml
i18n:
- i18n/*.json
- package.json
- package-lock.json
- .github/workflows/ci.yml
markdown:
- "*.md"
- .markdownlint.json
- .github/workflows/ci.yml
lint:
needs: changes
if: >-
needs.changes.outputs.php == 'true' ||
needs.changes.outputs.script == 'true' ||
needs.changes.outputs.stylesheet == 'true' ||
needs.changes.outputs.i18n == 'true' ||
needs.changes.outputs.markdown == 'true' ||
github.event_name == 'schedule'
uses: StarCitizenTools/mediawiki-ci-workflows/.github/workflows/lint.yml@main
with:
lint-php: ${{ needs.changes.outputs.php == 'true' || github.event_name == 'schedule' }}
lint-js: ${{ needs.changes.outputs.script == 'true' || github.event_name == 'schedule' }}
lint-styles: ${{ needs.changes.outputs.stylesheet == 'true' || github.event_name == 'schedule' }}
lint-i18n: ${{ needs.changes.outputs.i18n == 'true' || github.event_name == 'schedule' }}
lint-md: ${{ needs.changes.outputs.markdown == 'true' || github.event_name == 'schedule' }}
analyze-php:
needs: changes
if: needs.changes.outputs.php == 'true' || github.event_name == 'schedule'
uses: StarCitizenTools/mediawiki-ci-workflows/.github/workflows/analyze-php.yml@main
with:
project-type: skin # or 'extension'
project-name: MySkin # your project's directory name
skip-cache: ${{ github.event_name == 'schedule' }}
test-js:
needs: changes
if: needs.changes.outputs.script == 'true' || github.event_name == 'schedule'
uses: StarCitizenTools/mediawiki-ci-workflows/.github/workflows/test-js.yml@main
test-php:
needs: changes
if: needs.changes.outputs.php == 'true' || github.event_name == 'schedule'
uses: StarCitizenTools/mediawiki-ci-workflows/.github/workflows/test-php.yml@main
with:
project-type: skin # or 'extension'
project-name: MySkin # your project's directory name
skip-cache: ${{ github.event_name == 'schedule' }}
sonarqube:
needs: [test-js, test-php]
if: always() && !failure()
uses: StarCitizenTools/mediawiki-ci-workflows/.github/workflows/sonarqube.yml@main
with:
has-js-coverage: ${{ needs.test-js.result == 'success' }}
has-php-coverage: ${{ needs.test-php.result == 'success' }}
secrets:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}| What | Cache key | Shared across | Refreshed by |
|---|---|---|---|
| MediaWiki installation | mw-<branch>-php<version> |
test-php, test-parser, and analyze-php |
skip-cache: true (nightly) |
| Composer packages | composer-php<version> |
All MW branches | Automatic (Composer) |
| JS coverage | coverage-js-<branch> |
SonarQube runs | Each test-js run |
| PHP coverage | coverage-php-<branch> |
SonarQube runs | Each test-php run |
Requires mise. Install dev tools and enable the pre-commit hook:
mise install
mise run setupThis installs actionlint, yamlfmt, lefthook, and Go (for the scripts/ tooling, checked with gofmt and go vet), then configures lefthook to run the checks automatically before each commit.