mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-14 22:33:12 -06:00
Some checks are pending
CI / Unit tests (node --test) (push) Waiting to run
CI / OpenAPI spec lint (push) Waiting to run
CI / Android unit tests (Kotlin schedule evaluator vectors) (push) Waiting to run
CI / Licence gate + SBOM (production deps) (push) Waiting to run
CI / Boot smoke + version check (push) Waiting to run
The licence audit that found org.json in the APK was run by hand. Nothing stopped the next
transitive dependency arriving the same way, and "we track licences" was a claim rather than
something anyone could check.
TWO GATES, BOTH FAIL CLOSED.
scripts/license-check.js audits the server's npm tree. scripts/android-license-check.js
resolves the real releaseRuntimeClasspath — everything that can enter the APK a customer
installs — and checks it against android/licenses.json, where each entry records the licence
AND the evidence for it. A dependency nobody has recorded fails the build. That is the case
worth catching: org.json reached customers because it arrived transitively and nothing ever
asked what licence it carried.
Denied: AGPL, GPL, SSPL, Commons Clause, BUSL, and the JSON Licence. Weak copyleft (LGPL,
MPL, EPL, CDDL) is reported but does not fail — it is a judgement, and the judgement should
be made by someone who knows they are making it. Anything unrecognised fails; a package whose
licence we cannot identify is not one we ship.
⚠️ THE SERVER GATE INSTALLS --omit=dev, AND THAT IS THE POINT. A developer checkout carries
sharp, whose @img/sharp-wasm32 declares LGPL-3.0-or-later. It is a test fixture generator that
never reaches a server, but a scanner pointed at a dev tree reports LGPL and contradicts the
answer we give customers. Auditing the production install is what makes the answer defensible.
SBOM. Every release now publishes screentinker-sbom-<version>.cdx.json — CycloneDX 1.5, every
production dependency with version, purl and licence, generated from a production install. CI
uploads one on every run too. That is what turns the claim into something a customer or an
underwriter can verify themselves.
Neither script takes a dependency: a gate that needs its own supply chain audited is worth
less than one that does not.
Verified by mutation rather than assumed. Injecting GPL-3.0-or-later, AGPL-3.0, the JSON
Licence, SSPL-1.0, and a package with no licence field each fail the server gate; MIT and
LGPL pass (LGPL reported). Removing the org.json exclusion fails the Android gate by name;
dropping a group from the policy fails it as unrecorded. Both restored, both green.
Found and fixed while building it: npm ls exits non-zero for any tree problem — an extraneous
package is enough — which made the gate abort instead of auditing. It now reads the listing
either way and only aborts on genuinely empty output.
docs/licensing.md records the policy, how to run the gates, and the dev-vs-production trap.
1676/1676 pass.
196 lines
8.1 KiB
YAML
196 lines
8.1 KiB
YAML
name: CI
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
pull_request:
|
|
branches: [main]
|
|
workflow_dispatch:
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
# main gets frequent pushes - cancel an in-flight run when a newer commit
|
|
# (or rerun) supersedes it, per ref.
|
|
concurrency:
|
|
group: ci-${{ github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
jobs:
|
|
test:
|
|
name: Unit tests (node --test)
|
|
runs-on: ubuntu-latest
|
|
defaults:
|
|
run:
|
|
working-directory: server
|
|
steps:
|
|
- uses: actions/checkout@v6
|
|
with:
|
|
# The player-parity baselines judge a claim against the SHIPPED source
|
|
# (`git show <latest tag>:…`), because a baseline describes what an
|
|
# UN-UPDATED display can do. The default shallow checkout has no tags, so
|
|
# the suite silently fell back to the working tree and the biconditionals
|
|
# inverted: fixing a player's payload bug made CI demand a baseline change
|
|
# for displays that cannot possibly have the fix yet. Green locally, red
|
|
# here, for a reason found nowhere in the diff.
|
|
fetch-depth: 0
|
|
- uses: actions/setup-node@v6
|
|
with:
|
|
node-version: '20'
|
|
cache: npm
|
|
cache-dependency-path: server/package-lock.json
|
|
- run: npm ci
|
|
- run: npm test
|
|
|
|
openapi:
|
|
name: OpenAPI spec lint
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v6
|
|
- uses: actions/setup-node@v6
|
|
with:
|
|
node-version: '20'
|
|
- name: Lint the public API spec
|
|
run: npx --yes @redocly/cli@latest lint docs/openapi.yaml
|
|
# Contract integrity: the spec documents ONLY the token-reachable public surface.
|
|
# A JWT-only router (admin/auth/provision/...) appearing here is a security flag,
|
|
# not a convenience - fail loudly. (The runtime partition test is a separate suite
|
|
# that will cross-check the spec against the live mount list.)
|
|
- name: Assert spec is public-only
|
|
run: |
|
|
BAD=$(grep -oE '^ /(admin|auth|workspaces|ai|provision|white-label|status|subscription|stripe|teams|player-debug|contact|tokens)\b' docs/openapi.yaml || true)
|
|
if [ -n "$BAD" ]; then echo "::error::JWT-only path(s) leaked into the public spec:"; echo "$BAD"; exit 1; fi
|
|
if grep -qE 'unassigned|/prune' docs/openapi.yaml; then echo "::error::token-denied endpoint present in public spec"; exit 1; fi
|
|
echo "OK: spec is public-only"
|
|
|
|
android-test:
|
|
name: Android unit tests (Kotlin schedule evaluator vectors)
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v6
|
|
- uses: actions/setup-java@v4
|
|
with:
|
|
distribution: temurin
|
|
java-version: '17'
|
|
# Cache ~/.gradle/caches AND ~/.gradle/wrapper. The wrapper cache holds the
|
|
# Gradle distribution (gradle-8.5-bin.zip); without it every run re-downloaded
|
|
# ~130MB from services.gradle.org, and a transient reset there failed the job
|
|
# before any test ran (org.gradle.wrapper.Install.forceFetch). On a cache hit the
|
|
# distribution is already present, so the download — and its flake — is skipped.
|
|
cache: gradle
|
|
- uses: android-actions/setup-android@v3
|
|
# ScheduleEvalTest reads the SHARED shared/schedule-vectors.json (wired via
|
|
# the test task in app/build.gradle.kts), so a ScheduleEval.kt change that
|
|
# breaks the contract fails here.
|
|
- name: Kotlin evaluator vector conformance
|
|
working-directory: android
|
|
run: ./gradlew :app:testDebugUnitTest --no-daemon
|
|
|
|
# Every artifact that can enter the APK must have a licence on file. This runs here
|
|
# rather than in its own job because the Gradle cache and Android SDK are already warm.
|
|
- name: Licence gate (APK runtime classpath)
|
|
run: node scripts/android-license-check.js
|
|
|
|
licenses:
|
|
name: Licence gate + SBOM (production deps)
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v6
|
|
- uses: actions/setup-node@v6
|
|
with:
|
|
node-version: '20'
|
|
cache: npm
|
|
cache-dependency-path: server/package-lock.json
|
|
|
|
# --omit=dev on purpose, and it is the whole point of the job. A developer checkout
|
|
# carries sharp, whose @img/sharp-wasm32 declares LGPL-3.0-or-later; it is a test
|
|
# fixture generator that never reaches a server. Auditing anything other than a
|
|
# production install would report a licence we do not actually ship.
|
|
- name: Install production dependencies only
|
|
working-directory: server
|
|
run: npm ci --omit=dev
|
|
|
|
- name: Licence gate
|
|
run: node scripts/license-check.js --sbom sbom/screentinker-server.cdx.json
|
|
|
|
- uses: actions/upload-artifact@v4
|
|
with:
|
|
name: sbom
|
|
path: sbom/
|
|
if-no-files-found: error
|
|
|
|
smoke:
|
|
name: Boot smoke + version check
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v6
|
|
- uses: actions/setup-node@v6
|
|
with:
|
|
node-version: '20'
|
|
cache: npm
|
|
cache-dependency-path: server/package-lock.json
|
|
|
|
- name: Install deps
|
|
working-directory: server
|
|
run: npm ci
|
|
|
|
# Boot against a fresh SQLite db (clean checkout = no db yet). SELF_HOSTED
|
|
# makes the first user an admin with no billing. No certs present, so the
|
|
# server listens on plain HTTP at :3001. Background it and wait until it
|
|
# answers.
|
|
- name: Boot server
|
|
working-directory: server
|
|
env:
|
|
SELF_HOSTED: 'true'
|
|
# Boot WITH the collector on. This block is config-gated and only the
|
|
# statistics-collecting deployment sets the flag, so it had never executed in CI,
|
|
# on alpha, or in any test - and a load-time crash inside it took production down
|
|
# while every check was green. Code only one deployment runs is exactly the code
|
|
# CI has to execute.
|
|
TELEMETRY_COLLECTOR: '1'
|
|
run: |
|
|
node server.js > "$RUNNER_TEMP/server.log" 2>&1 &
|
|
echo $! > "$RUNNER_TEMP/server.pid"
|
|
for i in $(seq 1 30); do
|
|
curl -sf http://localhost:3001/api/status >/dev/null && exit 0
|
|
sleep 1
|
|
done
|
|
echo "server did not come up within 30s:"; cat "$RUNNER_TEMP/server.log"; exit 1
|
|
|
|
# Assert the public status endpoint is healthy and reports exactly the
|
|
# VERSION file - this is what proves the single-source-of-truth wiring.
|
|
- name: Assert /api/status ok and version matches VERSION
|
|
run: |
|
|
STATUS="$(curl -sf http://localhost:3001/api/status)"
|
|
echo "status: $STATUS"
|
|
EXPECTED="$(cat VERSION)"
|
|
REPORTED="$(echo "$STATUS" | jq -r .version)"
|
|
echo "VERSION file: $EXPECTED reported: $REPORTED"
|
|
test "$(echo "$STATUS" | jq -r .status)" = "ok"
|
|
test "$REPORTED" = "$EXPECTED"
|
|
echo "OK: status ok, version $REPORTED matches VERSION"
|
|
|
|
# Booting is not enough on its own - the collector could be mounted and broken. Prove
|
|
# the routes it adds actually answer, so a fault inside that block fails here rather
|
|
# than on the single deployment that turns it on.
|
|
- name: Assert the collector routes answer when enabled
|
|
run: |
|
|
STATS="$(curl -sf http://localhost:3001/api/public/stats)"
|
|
echo "stats: $STATS"
|
|
test "$(echo "$STATS" | jq -r 'has("screens") and has("installs")')" = "true"
|
|
REPORT="$(curl -s -o /dev/null -w '%{http_code}' -X POST \
|
|
-H 'Content-Type: application/json' -d '{"bad":1}' \
|
|
http://localhost:3001/api/telemetry/report)"
|
|
echo "malformed report -> HTTP $REPORT"
|
|
test "$REPORT" = "400"
|
|
echo "OK: collector mounted and answering"
|
|
|
|
- name: Stop server
|
|
if: always()
|
|
run: kill "$(cat "$RUNNER_TEMP/server.pid")" 2>/dev/null || true
|
|
|
|
# TODO (deferred - needs a tag earlier than HEAD, so meaningful from v1.8.0 on):
|
|
# upgrade-path job. Restore a db created by the previous tagged release, boot
|
|
# the current code against it, and assert migrations complete and /api/status
|
|
# is healthy. Add once a prior release tag exists.
|