From 09562061a4ca39d6629cf07841c676355580e9b5 Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Sun, 13 Sep 2026 00:56:23 -0400 Subject: [PATCH] ci: lockfile check, gitleaks, docker smoke job, renovate, shipped nginx security headers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - "Verify lockfile is in sync" (git diff --exit-code package-lock.json after npm ci) replaces the deleted GitHub lockfile workflow (#99). - gitleaks 8.30.1 binary scan on push + PR with a small allowlist for the public homeserver/registry URLs (#95). - docker job builds the image, runs it and asserts 200 + the security headers; continue-on-error until the runner is confirmed to have a Docker daemon (#93). .dockerignore keeps the context small. - docker-nginx.conf now sends a CSP (frame-src allowlist matching videoEmbed.ts), frame-ancestors 'none', Referrer-Policy and nosniff — shipped config, verify against the live chat.lotusguild.org headers before adopting in prod nginx (#95, #44 shipped-config half). - renovate.json + weekly renovate workflow for cinny and element-call; needs a RENOVATE_TOKEN secret (names starting GITEA_ are reserved) and stays continue-on-error until it exists (#94). - e2e job appended for the Playwright smoke test (#90), continue-on-error until green on the runner. Fixes #93 Fixes #94 Fixes #95 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA --- .dockerignore | 4 + .gitea/workflows/ci.yml | 147 ++++++++++++++++++++++++++++++++++ .gitea/workflows/renovate.yml | 28 +++++++ .gitleaks.toml | 19 +++++ docker-nginx.conf | 36 +++++++++ renovate.json | 25 ++++++ 6 files changed, 259 insertions(+) create mode 100644 .gitea/workflows/renovate.yml create mode 100644 .gitleaks.toml create mode 100644 renovate.json diff --git a/.dockerignore b/.dockerignore index be0443aea..891b4be32 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,2 +1,6 @@ node_modules/ .git/ +dist/ +experiment/ +*.md +!README.md diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 94f7ac03a..da7d5f073 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -57,6 +57,16 @@ jobs: sleep $((attempt * 15)) done + # ── #99 — lockfile.yml (GitHub-only, deleted) commented on lockfile + # diffs after the fact; this is the CI-native equivalent, ported as a + # hard gate. `npm ci` already refuses to run on an out-of-range + # mismatch, but it can silently normalize lesser lockfile drift (e.g. + # metadata/resolved fields behind a stale-but-satisfiable range) + # without failing — so assert zero diff afterward, which is the + # cheapest way to also catch that class of drift. + - name: Verify lockfile is in sync + run: git diff --exit-code package-lock.json + # ── Quality gates run BEFORE the slow build so a format/lint/type/test # error fails in seconds instead of after the ~minutes-long build. All are # hard gates — any failure fails the job and blocks the deploy. The tree is @@ -142,3 +152,140 @@ jobs: git push origin main echo "Pushed — cinny-desktop release.yml will start via on:push trigger" fi + + # ── #95 — secret scanning ──────────────────────────────────────────────── + # zricethezav/gitleaks-action is GitHub-Actions-only; on the Gitea act_runner + # we can't assume the host has a Docker daemon reachable from job containers + # (see the `docker` job below), so this downloads the pinned linux/amd64 + # binary release directly instead. `--no-git` scans the checked-out tree as + # plain files (a point-in-time content scan) rather than walking history, + # since this runs on both push and pull_request and a PR's shallow checkout + # doesn't carry full history anyway. + gitleaks: + name: Secret scan (gitleaks) + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install gitleaks 8.30.1 + run: | + curl -fsSL -o gitleaks.tar.gz \ + https://github.com/gitleaks/gitleaks/releases/download/v8.30.1/gitleaks_8.30.1_linux_x64.tar.gz + tar -xzf gitleaks.tar.gz gitleaks + chmod +x gitleaks + + - name: Scan for secrets + run: ./gitleaks detect --no-git -v --redact --source . --config .gitleaks.toml + + # ── #93 — the image was never actually built in CI, so a Dockerfile break + # (or a header regression, once #95's nginx CSP shipped) could sit unnoticed + # until a manual `docker build` on deploy infra caught it. This builds the + # real image, boots it, and asserts both a 200 and the security headers + # added to docker-nginx.conf for #95. + # + # `continue-on-error: true` — informational for now. The shared act_runner + # may not expose a Docker daemon to job containers (same class of problem + # as the unreachable cache server noted above); flip this off once it's + # confirmed the runner can actually run `docker build`/`docker run` here. + docker: + name: Docker image build & smoke test + needs: build + runs-on: ubuntu-latest + continue-on-error: true + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Build image + run: docker build -t cinny-ci . + + - name: Run container + run: docker run -d --name cinny-ci -p 8095:80 cinny-ci + + - name: Wait for container to be ready + run: | + for i in $(seq 1 30); do + curl -fsS -o /dev/null http://localhost:8095/ && exit 0 + sleep 1 + done + echo "container never became ready" >&2 + exit 1 + + - name: Check response and security headers + run: | + headers="$(curl -fsSI http://localhost:8095/)" + echo "$headers" + echo "$headers" | grep -qi '^HTTP/[0-9.]* 200' || { echo "expected HTTP 200"; exit 1; } + echo "$headers" | grep -qi '^content-security-policy:' || { echo "missing Content-Security-Policy header"; exit 1; } + echo "$headers" | grep -qi "frame-ancestors 'none'" || { echo "CSP missing frame-ancestors 'none'"; exit 1; } + echo "$headers" | grep -qi '^referrer-policy: *no-referrer' || { echo "missing Referrer-Policy header"; exit 1; } + echo "$headers" | grep -qi '^x-content-type-options: *nosniff' || { echo "missing X-Content-Type-Options header"; exit 1; } + + - name: Stop container + if: always() + run: docker rm -f cinny-ci || true + + # ── #90 — Playwright smoke test ────────────────────────────────────────── + # Boots the built client in a real (headless) Chromium and drives it. Two + # tiers live under e2e/ (see LOTUS_TESTING.md → "Playwright smoke test"): + # boot tier — always runs: login page renders with no console/page + # errors, sw.js is served + registers, bundled Element Call + # mounts in a frame. + # E2EE tier — password login, create a private encrypted room, send text + # + a compressed image, assert every `PUT …/send/*` went out + # as m.room.encrypted. Skips itself unless the E2E_* secrets + # below are set (create them under repo → Settings → Actions + # → Secrets; they are empty until then). + # dist/ is rebuilt in-job because actions/upload-artifact@v4 does not work + # on this Gitea runner (LOTUS_TODO), so `needs: build` only gates on the + # main job having passed, not on its artifact. + # continue-on-error: `playwright install --with-deps` needs apt on the + # runner image; promote to hard once green on the runner. + e2e: + name: Playwright smoke (e2e) + needs: build + runs-on: ubuntu-latest + continue-on-error: true + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: '.node-version' + + - name: Install dependencies + run: | + npm config set fetch-retries 5 + npm config set fetch-retry-mintimeout 20000 + npm config set fetch-retry-maxtimeout 120000 + npm config set fetch-timeout 600000 + for attempt in 1 2 3; do + echo "npm ci attempt $attempt…" + npm ci && break + if [ "$attempt" = "3" ]; then + echo "npm ci failed after 3 attempts" >&2 + exit 1 + fi + echo "npm ci failed; retrying in $((attempt * 15))s…" >&2 + sleep $((attempt * 15)) + done + + - name: Install Playwright Chromium + run: npx playwright install --with-deps chromium + + - name: Build + run: npm run build + env: + NODE_OPTIONS: '--max_old_space_size=6144' + VITE_APP_VERSION: ${{ github.sha }} + + - name: Playwright smoke test + run: npm run test:e2e + env: + CI: 'true' + E2E_HOMESERVER: ${{ secrets.E2E_HOMESERVER }} + E2E_USER: ${{ secrets.E2E_USER }} + E2E_PASSWORD: ${{ secrets.E2E_PASSWORD }} diff --git a/.gitea/workflows/renovate.yml b/.gitea/workflows/renovate.yml new file mode 100644 index 000000000..570cd2f08 --- /dev/null +++ b/.gitea/workflows/renovate.yml @@ -0,0 +1,28 @@ +name: Renovate + +# Gitea #94 — no dependency update automation existed at all. Runs the +# official renovate/renovate Docker image against this Gitea instance. +# +# Requires a `RENOVATE_TOKEN` repo/org secret: a Gitea access token with +# read/write on LotusGuild/cinny and LotusGuild/element-call, created by a +# maintainer — this workflow does not (and cannot) create one for you. +# Note: Gitea reserves the `GITEA_` secret-name prefix, so the token cannot +# be named e.g. `GITEA_TOKEN` — hence `RENOVATE_TOKEN`. +on: + schedule: + - cron: '0 4 * * 1' # weekly, Monday 04:00 UTC + workflow_dispatch: {} + +jobs: + renovate: + name: Renovate + runs-on: ubuntu-latest + continue-on-error: true # informational until RENOVATE_TOKEN is confirmed present + steps: + - name: Run Renovate + uses: docker://renovate/renovate:44 + env: + RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }} + RENOVATE_PLATFORM: gitea + RENOVATE_ENDPOINT: https://code.lotusguild.org/api/v1 + RENOVATE_REPOSITORIES: LotusGuild/cinny,LotusGuild/element-call diff --git a/.gitleaks.toml b/.gitleaks.toml new file mode 100644 index 000000000..9f4c6e985 --- /dev/null +++ b/.gitleaks.toml @@ -0,0 +1,19 @@ +title = "gitleaks config for Lotus Chat (cinny fork)" + +# Gitea #95 — secret scanning was entirely absent. Extend gitleaks' built-in +# ruleset (don't replace it) and allowlist the known-public infrastructure +# URLs that show up in tracked config, which are hostnames, not secrets. +[extend] +useDefault = true + +[allowlist] +description = "Known-public Lotus/Matrix homeserver + npm registry URLs — not secrets" +regexes = [ + '''https?://matrix\.lotusguild\.org''', + '''https?://code\.lotusguild\.org/api/packages/LotusGuild/npm/''', + '''matrix\.lotusguild\.org''', +] +paths = [ + '''config\.json''', + '''\.npmrc''', +] diff --git a/docker-nginx.conf b/docker-nginx.conf index a2dbeba03..2dabd8f00 100644 --- a/docker-nginx.conf +++ b/docker-nginx.conf @@ -2,6 +2,42 @@ server { listen 80; listen [::]:80; + # ── Gitea #95 / #44 — shipped image had no security headers at all. + # `always` so these are sent on error responses too, not just 200s. + # + # Content-Security-Policy, directive by directive: + # default-src 'self' baseline: same-origin unless a directive below opens it up + # script-src 'self' 'wasm-unsafe-eval' + # app code is same-origin only; 'wasm-unsafe-eval' is required + # for the wasm modules used for E2EE crypto and audio denoise + # style-src 'self' 'unsafe-inline' + # vanilla-extract (the app's CSS-in-JS) emits inline