waynestate / rollcall
WELD's fleet scanner: every app answers present with its PHP image, Laravel and auth-package versions and its PROCTOR result, checked against CREED's policy.
Requires
- php: ^8.3
- composer/semver: ^3.4
- nyholm/psr7: ^1.8
- promphp/prometheus_client_php: ^2.14
- psr/clock: ^1.0
- psr/http-client: ^1.0
- psr/http-factory: ^1.0
- psr/http-message: ^2.0
- symfony/console: ^7.1
- symfony/http-client: ^7.4
Requires (Dev)
- laravel/pint: ^1.32
- orchestra/testbench: ^10.0 || ^11.0
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^11.5
Suggests
- laravel/framework: For the Laravel adapter: the dashboard page and the scheduled scan
- waynestate/wayne-ui: Styles the Laravel dashboard page; until it is on Packagist, link its stylesheet with ROLLCALL_WAYNEUI_CSS
Provides
None
Conflicts
None
Replaces
None
README
ROLLCALL
Checks: composer ci (the same command runs on GitLab and GitHub).
Every app answers "present". The ones on PHP 7.4 answer from the back row.
Front page: site/index.html (static, no build step; see site/README.md).
On Citadel it is served by the raw-PHP deploy layer in deploy/: a SQUIRE kit whose front controller serves site/ behind HELMET's security headers. For local development, keep serving site/ directly.
ROLLCALL is the fleet scanner. For every app it answers one question: what is it running, and where does that stand
against the policy? It reports the PHP image, the declared PHP floor, the Laravel version, the auth package and its
version, and the PROCTOR result, then checks each version against CREED's policy.json by date. It keeps one snapshot
per scan, so it can show drift and an EOL burn-down over time, and prunes the old ones after the retention window
(default 90 days; the newest snapshot always stays).
ROLLCALL enforces nothing. It never opens an issue, comments, or deploys, and it exits 0 however the fleet looks.
The public report is counts and progress only: no app names, no owners, no ranking. The per-app list is internal.
(Like a good attendance sheet, it is for noticing who is missing, not for shaming them.)
One page of the roll book, called name by name. Sample data: the apps are fictional, and a real scan's per-app list stays internal.
Where it sits in WELD
WELD is Wayne State's application stack. ROLLCALL is the roll book for the whole party:
| Part | Job |
|---|---|
| WayneUI (project 0) | The interface layer: tokens, components, four consumer paths |
| CREED | The standards and policy.json, the dates ROLLCALL reads |
| PROCTOR | Measures one app against the policy; ROLLCALL reads its JSON report and never re-runs a rule |
| PREFLIGHT | CI templates |
| SESAME | Protocol-neutral auth; ROLLCALL reports which auth package each app uses |
| HELMET | Baseline runtime and its own optional /metrics |
| Pinstripe | The coding standard this repo includes for its Pint and PHPStan settings |
| Sourdough | Starters |
| ROLLCALL | The fleet census and EOL burn-down |
How it is built
A framework-free core (src/) plus thin entry points that hold no rules:
bin/rollcall: the CLI (scan,report,drift,prune). One PHP command, so it works the same on Windows, macOS and Linux.public/index.php: a raw-PHP front controller forGET /metricsandGET /report.json.src/Laravel/: a Laravel adapter with a dashboard page, a/metricsroute andphp artisan rollcall:scanandrollcall:prune.
CREED's policy reader is carried as a generated copy under src/Creed/ (namespace Waynestate\Rollcall\Creed),
made by CREED's bin/export.php. CREED is on Packagist now, so the copy can become a require of
waynestate/creed; until ROLLCALL switches, the copy stays. Refresh it from a CREED checkout with
php <creed>/bin/export.php --namespace='Waynestate\Rollcall\Creed' --to=src/Creed, and prove the copy has not
drifted with composer creed-copy:check (it reads the checkout from CREED_PATH). A hand edit to src/Creed/
fails that check.
Data comes in through three interfaces, each with a fixture adapter, so nothing here needs a live host to try:
| Source | Interface | 0.1 adapters |
|---|---|---|
Repositories (composer.json, composer.lock) |
RepositorySource |
a directory of fixtures; GitLab over PSR-18, tested against recorded responses |
| Runtime image per app | ImageSource |
a JSON export of the app list |
| PROCTOR result | ProctorReportSource |
a directory of PROCTOR --format=json reports |
Everything is read-only toward every source. The only thing ROLLCALL writes is its own snapshot directory.
Install
composer require waynestate/rollcall
ROLLCALL 0.1.0 is on Packagist. No extra repository needed. Requires PHP ^8.3. The CLI is vendor/bin/rollcall.
Quick start
From a checkout, with the bundled fixtures (no network, no token). Install dependencies once with
composer install (or the Docker line under "Run the checks").
Linux and macOS (bash):
export ROLLCALL_POLICY_FILE=tests/fixtures/policy.json
export ROLLCALL_REPOS_DIR=tests/fixtures/repos
export ROLLCALL_IMAGES_FILE=tests/fixtures/images.json
export ROLLCALL_PROCTOR_DIR=tests/fixtures/proctor
export ROLLCALL_STORE_DIR=var/snapshots
php bin/rollcall scan
php bin/rollcall report
Windows (PowerShell, no bash or WSL needed):
$env:ROLLCALL_POLICY_FILE = "tests/fixtures/policy.json"
$env:ROLLCALL_REPOS_DIR = "tests/fixtures/repos"
$env:ROLLCALL_IMAGES_FILE = "tests/fixtures/images.json"
$env:ROLLCALL_PROCTOR_DIR = "tests/fixtures/proctor"
$env:ROLLCALL_STORE_DIR = "var/snapshots"
php bin/rollcall scan
php bin/rollcall report
policy.json is CREED's file; the copy under tests/fixtures/ is a snapshot for tests. Use the released one in real
runs.
Commands
rollcall scan [--today=YYYY-MM-DD] [--prune[=DAYS]]
rollcall report [--format=text|json|prometheus] [--internal] [--today=YYYY-MM-DD]
rollcall drift --feed=<file> [--component=php] [--today=YYYY-MM-DD]
rollcall prune [--days=N] [--dry-run] [--today=YYYY-MM-DD]
scancalls the roll once and stores<ROLLCALL_STORE_DIR>/<date>.json. Scanning twice in a day replaces that day's snapshot. A gap in one source (a missing report, a bad image tag) becomes a note on that app, not a failed scan. A refused GitLab token stops the scan loudly instead of producing a flattering partial answer.--prune[=DAYS]deletes snapshots older than that many days after saving (default:ROLLCALL_RETENTION_DAYS, 90; 0 keeps everything).reportprints the latest snapshot against the policy as of today.textis a terminal table,jsonis the machine-readable form,prometheusis the metrics text (refused unlessROLLCALL_METRICS_ENABLED=true).--internaladds the per-app list; keep that output inside the project.driftcomparespolicy.jsonwith a saved endoflife.date v1 product page (for examplehttps://endoflife.date/api/v1/products/php/saved to a file). A difference is a flag for a person to look at; the policy stays the source of truth. ROLLCALL does not fetch that page itself.prunedeletes snapshots older than--days(default:ROLLCALL_RETENTION_DAYS, 90; 0 keeps everything). The newest snapshot is never deleted, even if it is older than the cut-off, so the dashboard always has data.--dry-runlists what would go without deleting anything.
Exit codes: 0 ok, 2 tool error (bad option, unreadable policy, a source that refused). There is no 1: ROLLCALL
does not gate anything.
What the report says
- PHP image, declared PHP floor, Laravel: apps per version, the state from the policy (
active,security_only,eol,unknown), the date security support ends and the days left (negative once past). - Auth package and PROCTOR: apps per auth package, and per PROCTOR result (
pass,fail,none). - Drift, counted per kind:
image_eol,image_unknown,image_below_floor,floor_allows_eol,floor_unbounded,eol_past_grace(past the policy's grace days),laravel_eol,proctor_missing,proctor_failing,override_expired. - Progress: apps that moved since the previous scan and apps that went from an EOL image to a supported one. In the internal view each upgraded app carries the day it landed.
- EOL burn-down: for each scan day, how many apps ran an EOL image on that day.
State is never decided in code: it is derived from the dates in policy.json by CREED's reader. A version the
policy does not list is unknown.
Configuration
Every setting is an environment variable. .env.example holds placeholders; never commit a real token.
| Variable | Meaning | Default |
|---|---|---|
ROLLCALL_POLICY_FILE |
Path to CREED's policy.json |
none (required for report, drift) |
ROLLCALL_STORE_DIR |
Snapshot directory, the only place ROLLCALL writes | var/snapshots |
ROLLCALL_SOURCE |
directory (fixtures) or gitlab |
directory |
ROLLCALL_REPOS_DIR |
Fixture repositories as <group>/<project>/composer.{json,lock} |
none |
ROLLCALL_GITLAB_URL |
https:// base URL of the GitLab instance |
none |
ROLLCALL_GITLAB_TOKEN |
A read_api token on a group or service account (never api). Never logged, never printed |
none |
ROLLCALL_GITLAB_GROUPS |
Comma-separated group ids or paths; subgroups are included | none |
ROLLCALL_IMAGES_FILE |
Export of the app list: {"apps": [{"project": "group/project", "image": "php:8.4-apache"}]} |
none |
ROLLCALL_PROCTOR_DIR |
PROCTOR JSON reports, one per app: group/project is read from group__project.json |
none |
ROLLCALL_AUTH_PACKAGES |
Comma-separated Composer packages that count as an auth package | the built-in list |
ROLLCALL_RETENTION_DAYS |
How many days of snapshots to keep; the newest is never deleted | 90 (0 keeps everything) |
ROLLCALL_METRICS_ENABLED |
Turn metrics on (CLI report --format=prometheus and /metrics) |
false |
ROLLCALL_METRICS_TOKEN |
Bearer token a scraper must present | none |
ROLLCALL_METRICS_ALLOW |
Comma-separated scraper IP addresses (exact match) | none |
ROLLCALL_PUBLIC_REPORT_ENABLED |
Serve GET /report.json (public counts only) |
false |
ROLLCALL_DASHBOARD_ENABLED |
Laravel: serve the dashboard page | false |
ROLLCALL_DASHBOARD_PATH |
Laravel: dashboard URL path | rollcall |
ROLLCALL_INTERNAL_ENABLED |
Laravel: serve the per-app page at <path>/apps (needs your sign-in middleware) |
false |
ROLLCALL_WAYNEUI_CSS |
Laravel: https:// or absolute URL of WayneUI's stylesheet for the dashboard |
none |
Scanning GitLab (once a person gives you a token and a group): set ROLLCALL_SOURCE=gitlab and the three GitLab
variables. ROLLCALL sends only GET and HEAD, fetches the two Composer files without cloning, skips a file whose blob
id has not changed, follows Link pagination only to the configured host, and waits out 429 Retry-After.
Metrics
Off by default (the stack's metrics rule). With ROLLCALL_METRICS_ENABLED=true, /metrics serves Prometheus text built from the latest
snapshot (a request never triggers a scan). It needs ROLLCALL_METRICS_TOKEN as a bearer token or an address in
ROLLCALL_METRICS_ALLOW; with neither set, nobody gets in. Labels are version numbers, state names, drift kinds and
public package names: no app names or personal data. The dashboard JSON is in grafana/rollcall-burn-down.json; import
it into Grafana once a Prometheus server scrapes the endpoint. Run rollcall scan
on a schedule so the series moves.
Laravel
The package registers itself (extra.laravel.providers). Publish the config with
php artisan vendor:publish --tag=rollcall-config, set the variables above, and schedule rollcall:scan. The
dashboard shows counts and progress; the per-app page is a second route that stays off until you enable it, and it
runs behind rollcall.dashboard.internal_middleware (default web and auth.sesame), so an app with no such
middleware fails closed. The page uses WayneUI's class names and takes its stylesheet from ROLLCALL_WAYNEUI_CSS.
The /metrics route is registered on its own path; if your app already serves /metrics (HELMET does), run ROLLCALL's
on a separate app or leave metrics off.
Run the checks
Maintainers: how to build, test and release this project, and where its data comes from, is in MAINTAINING.md.
composer ci is the contract: manifest, tests, Pint, PHPStan and composer audit. PHP is not needed on the host:
docker run --rm -v "${PWD}:/app" -w /app composer:2.10.3 sh -c "composer install --no-interaction && composer ci"
(On Windows PowerShell ${PWD} works as written; in Git Bash use $(pwd -W) and set MSYS_NO_PATHCONV=1.)
The GitLab pipeline (.gitlab-ci.yml) and the GitHub workflow (.github/workflows/ci.yml) both wrap composer ci,
on PHP 8.3, 8.4 and 8.5. git.wayne.edu has no runners yet; an instance runner there is strongly recommended so this
check can run beside the code it protects.
Security notes
- Read-only toward GitLab and everything else: one gate in the GitLab adapter allows only
GETandHEAD, and a test fails if a write verb appears insrc/. - The GitLab token comes from the environment, is sent only to the configured
httpshost, and is hidden from debug output and error messages. Use aread_apitoken with an expiry and a named rotation owner. - Composer files are parsed as data. No
composer install, script or plugin from a scanned repository ever runs. - PROCTOR reports, image tags and app ids are outside input: shapes are validated, text is bounded, and terminal output strips control characters.
Watch the back row empty (the burn-down, sample data)
The same line Grafana draws from /metrics. Made-up apps, real arithmetic.
Contributing
This repository is published from Wayne State University's internal GitLab and mirrored to GitHub. Changes land there first; Wayne State developers open issues and merge requests on the internal project.
Licence: MIT, Copyright Wayne State University.
The whole classWayneUI and the stack. Everyone is here today; the ones in the back row know who they are.
scripts/readme-art. Present.
