mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-07-26 11:49:16 +00:00
efcc06d50b
* ci: harden CI and supply-chain pipeline * Add frontend Vitest step to ci.yml so the 241 existing frontend tests run on every PR (mirrors the backend build/test/lint/audit order). * Pin Node 26 as a single source of truth: new .node-version, node-version-file on all setup-node calls, engines.node ">=26.0.0" in all three package.json files. Matches the Dockerfile's node:26-alpine. * SHA-pin remaining mutable actions in the start-app composite (actions/setup-node v6, actions/cache v4.3.0). * Pin Dockerfile supply-chain inputs: golang:1.26.3-alpine by sha256 digest in both builder stages; replace mutable-tag git clone with commit-SHA fetch for docker/cli (v29.4.1) and docker/compose (v5.1.3). LDFLAGS version strings and otel patch preserved unchanged. * Ref-scope docker-publish concurrency so two different release tags cannot cancel each other; same-ref reruns still cancel as before. * Harden CLA workflow: drop actions:write from permissions; tighten the issue_comment trigger to PRs only (github.event.issue.pull_request != null) matching the two documented CLA phrases. No PR code is checked out. * Drop trivy-version: latest from both Trivy scans so the SHA-pinned aquasecurity/trivy-action governs the bundled binary version. The HIGH/CRITICAL gate, severity filter, and trivy.yaml (OpenVEX) are unchanged. * Restructure Dependabot: add applies-to: security-updates groups for npm (root/backend/frontend), docker, and github-actions; switch github-actions to directories so the local composite action is monitored alongside the top-level workflows. * Add a daily scheduled SARIF security scan (security-scan.yml): two parallel jobs scanning saelix/sencho:latest and a fresh main HEAD build, uploading to GitHub code scanning. Least-privilege (contents: read, security-events: write). Visibility only; existing PR-blocking and release-blocking Trivy gates are not weakened. Validation: backend tsc clean; frontend tsc clean; frontend npm test 27 files / 241 tests pass; npm audit --audit-level=high passes at root, backend, and frontend; docker buildx build --check passes with no warnings (all pinned digests resolve from the registry). * ci(frontend): set explicit jsdom URL so localStorage initializes in CI jsdom does not instantiate window.localStorage / sessionStorage when the document has the opaque about:blank origin. Five frontend test files that call localStorage.clear() in beforeEach started failing once the new frontend Vitest step in this PR began running them on Linux CI runners. Configuring environmentOptions.jsdom.url with a real same-origin URL is the documented Vitest 4.x workaround and is a config-only change. All 27 test files (241 tests) pass locally with the fix applied.
301 lines
11 KiB
YAML
301 lines
11 KiB
YAML
name: Sencho CI
|
|
|
|
on:
|
|
pull_request:
|
|
branches: [main]
|
|
push:
|
|
branches: [main]
|
|
|
|
# Cancel previous runs on the same branch/PR
|
|
concurrency:
|
|
group: ci-${{ github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
# Least-privilege default for every job.
|
|
permissions:
|
|
contents: read
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Build & Validation (PRs only, skipped for release-please PRs)
|
|
# ---------------------------------------------------------------------------
|
|
jobs:
|
|
backend:
|
|
name: Backend (Build, Test, Lint)
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 10
|
|
# Skip on the release-please bot PR. It only touches version metadata,
|
|
# so the full build+test+lint+audit stack is pure action-minute waste.
|
|
if: >-
|
|
github.event_name == 'pull_request'
|
|
&& github.head_ref != 'release-please--branches--main--components--sencho'
|
|
steps:
|
|
- name: Checkout Code
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
|
with:
|
|
node-version-file: '.node-version'
|
|
cache: 'npm'
|
|
cache-dependency-path: backend/package-lock.json
|
|
|
|
- name: Install Dependencies
|
|
working-directory: ./backend
|
|
run: npm ci
|
|
|
|
- name: Build (TypeScript)
|
|
working-directory: ./backend
|
|
run: npm run build
|
|
|
|
- name: Unit Tests (Vitest)
|
|
working-directory: ./backend
|
|
run: npm test
|
|
|
|
- name: Lint (ESLint)
|
|
working-directory: ./backend
|
|
run: npm run lint
|
|
|
|
- name: Audit Dependencies
|
|
working-directory: ./backend
|
|
run: npm audit --audit-level=high
|
|
|
|
# Hand the freshly-verified dist/ to the E2E job so it does not have to
|
|
# re-run `tsc` on the same source. retention-days is the minimum; this
|
|
# artifact is only needed for the downstream e2e job in the same run.
|
|
- name: Upload backend dist
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
|
with:
|
|
name: backend-dist
|
|
path: backend/dist
|
|
retention-days: 1
|
|
if-no-files-found: error
|
|
|
|
frontend:
|
|
name: Frontend (Build, Lint)
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 10
|
|
# See backend job rationale: skip bot PRs that cannot affect the frontend.
|
|
if: >-
|
|
github.event_name == 'pull_request'
|
|
&& github.head_ref != 'release-please--branches--main--components--sencho'
|
|
steps:
|
|
- name: Checkout Code
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
|
with:
|
|
node-version-file: '.node-version'
|
|
cache: 'npm'
|
|
cache-dependency-path: frontend/package-lock.json
|
|
|
|
- name: Install Dependencies
|
|
working-directory: ./frontend
|
|
run: npm ci
|
|
|
|
- name: Build (Vite/React)
|
|
working-directory: ./frontend
|
|
run: npm run build
|
|
|
|
- name: Unit Tests (Vitest)
|
|
working-directory: ./frontend
|
|
run: npm test
|
|
|
|
- name: Lint (ESLint)
|
|
working-directory: ./frontend
|
|
run: npm run lint
|
|
|
|
- name: Audit Dependencies
|
|
working-directory: ./frontend
|
|
run: npm audit --audit-level=high
|
|
|
|
docker-validate:
|
|
name: Docker Build & Scan
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 15
|
|
# See backend job rationale.
|
|
if: >-
|
|
github.event_name == 'pull_request'
|
|
&& github.head_ref != 'release-please--branches--main--components--sencho'
|
|
steps:
|
|
- name: Checkout Code
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
|
|
- name: Set up Docker Buildx
|
|
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
|
|
|
|
# Computed once per job run. Feeds Dockerfile's APK_CACHE_BUST arg so
|
|
# the `apk upgrade` layer rebuilds at least once per calendar day, even
|
|
# when every other input to the layer is cached. See Dockerfile:115-122
|
|
# for the rationale.
|
|
- name: Compute daily apk cache bust value
|
|
id: apk-bust
|
|
run: echo "date=$(date -u +%Y-%m-%d)" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Build Docker image (validation only)
|
|
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7
|
|
with:
|
|
context: .
|
|
push: false
|
|
load: true
|
|
tags: sencho:pr-test
|
|
cache-from: type=gha
|
|
cache-to: type=gha,mode=max
|
|
build-args: |
|
|
APK_CACHE_BUST=${{ steps.apk-bust.outputs.date }}
|
|
|
|
- name: Scan image for vulnerabilities (Trivy)
|
|
# Hard-fails the PR on any HIGH or CRITICAL finding not suppressed by
|
|
# the OpenVEX document at security/vex/sencho.openvex.json (loaded via
|
|
# trivy.yaml). The VEX document is the canonical triage record and is
|
|
# also attached as a cosign attestation on the published image.
|
|
# If a new CVE must be triaged, add a VEX statement with a
|
|
# justification — do NOT recreate .trivyignore.
|
|
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
|
|
with:
|
|
image-ref: sencho:pr-test
|
|
exit-code: '1'
|
|
severity: 'CRITICAL,HIGH'
|
|
format: 'table'
|
|
trivy-config: trivy.yaml
|
|
|
|
# Mirror of the release-time pre-publish smoke gate in docker-publish.yml.
|
|
# Catches the class of failure where the source-built Docker CLI or
|
|
# Compose plugin compiles cleanly but does not exec at runtime (e.g. -o
|
|
# pointed at a non-main Go package, ABI breakage in a base image bump).
|
|
# The release workflow additionally polls /api/health; PR CI keeps just
|
|
# the binary check to bound job duration without losing the gate.
|
|
- name: Smoke test built image (binaries)
|
|
run: |
|
|
set -euo pipefail
|
|
docker run --rm --entrypoint sh sencho:pr-test -c \
|
|
'docker --version && docker compose version'
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# E2E Tests (PRs only, skipped for release-please PRs)
|
|
# ---------------------------------------------------------------------------
|
|
e2e:
|
|
name: E2E Tests (Playwright)
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 15
|
|
needs: [backend, frontend]
|
|
# See backend job rationale.
|
|
if: >-
|
|
github.event_name == 'pull_request'
|
|
&& github.head_ref != 'release-please--branches--main--components--sencho'
|
|
steps:
|
|
- name: Checkout Code
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
|
|
# Reuse the dist/ that the `backend` job produced and verified, instead
|
|
# of running `tsc` a second time against the same source tree. The
|
|
# composite action's `skip-backend-build` input short-circuits its build
|
|
# step when this artifact is already in place.
|
|
- name: Download backend dist
|
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
|
with:
|
|
name: backend-dist
|
|
path: backend/dist
|
|
|
|
- name: Start app & install Playwright
|
|
uses: ./.github/actions/start-app
|
|
with:
|
|
skip-backend-build: 'true'
|
|
|
|
- name: Run E2E tests
|
|
# `--project=chromium` is explicit because playwright.config.ts also
|
|
# defines a `screenshots` project that captures docs/images/ for
|
|
# manual review. That project must not run in CI: its output would
|
|
# be discarded after the run, wasting minutes per PR. Keep this
|
|
# flag; do not "simplify" back to `npx playwright test`.
|
|
run: npx playwright test --project=chromium
|
|
|
|
- name: Upload E2E report and service logs
|
|
if: failure()
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
|
with:
|
|
name: playwright-report
|
|
path: |
|
|
e2e/report/
|
|
test-results/
|
|
ci-logs/
|
|
retention-days: 7
|
|
|
|
sync-docs:
|
|
name: Sync Docs to sencho-docs
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 5
|
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
|
steps:
|
|
# This job mirrors docs/ into the sibling sencho-docs repo. The token
|
|
# must therefore be scoped to `sencho-docs`, not the current repo, so
|
|
# we mint an installation token explicitly targeting that repo.
|
|
# Minted unconditionally so the step graph stays simple; the token
|
|
# auto-revokes on post-step even when the gate below is false.
|
|
- name: Generate GitHub App installation token
|
|
id: app-token
|
|
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
|
with:
|
|
app-id: ${{ secrets.APP_ID }}
|
|
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
|
owner: ${{ github.repository_owner }}
|
|
repositories: sencho-docs
|
|
permission-contents: write
|
|
|
|
- name: Checkout Sencho repo
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
with:
|
|
path: sencho
|
|
# Need the previous commit to diff against for the docs-change gate.
|
|
fetch-depth: 2
|
|
|
|
# Skip the clone/rsync/commit work entirely when the push doesn't touch
|
|
# docs/. rsync --delete is a no-op in that case anyway, but spinning up
|
|
# the runner + cloning sencho-docs + computing the diff still burns
|
|
# ~30s of action minutes per non-docs push to main. With multiple
|
|
# non-docs merges per day this adds up fast.
|
|
- name: Detect docs changes
|
|
id: detect
|
|
working-directory: sencho
|
|
run: |
|
|
if git diff --name-only HEAD~1 HEAD -- docs/ | grep -q .; then
|
|
echo "changed=true" >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "changed=false" >> "$GITHUB_OUTPUT"
|
|
echo "No changes under docs/ in this commit; skipping sync."
|
|
fi
|
|
|
|
- name: Clone or init sencho-docs
|
|
if: steps.detect.outputs.changed == 'true'
|
|
env:
|
|
DOCS_TOKEN: ${{ steps.app-token.outputs.token }}
|
|
run: |
|
|
rm -rf sencho-docs
|
|
|
|
REMOTE="https://x-access-token:${DOCS_TOKEN}@github.com/studio-saelix/sencho-docs.git"
|
|
|
|
if git clone "${REMOTE}" sencho-docs 2>/dev/null; then
|
|
echo "Cloned existing sencho-docs."
|
|
else
|
|
echo "Clone failed (repo is empty or has no default branch) - initializing."
|
|
mkdir -p sencho-docs
|
|
cd sencho-docs
|
|
git init
|
|
git checkout -b main
|
|
git remote add origin "${REMOTE}"
|
|
fi
|
|
|
|
- name: Copy /docs into sencho-docs root
|
|
if: steps.detect.outputs.changed == 'true'
|
|
run: rsync -av --delete --exclude='.git' sencho/docs/ sencho-docs/
|
|
|
|
- name: Commit and push to sencho-docs
|
|
if: steps.detect.outputs.changed == 'true'
|
|
working-directory: sencho-docs
|
|
run: |
|
|
git config user.email "docs-bot@sencho.io"
|
|
git config user.name "Sencho Docs Bot"
|
|
git add -A
|
|
git commit -m "docs: sync from main@${{ github.sha }}" || echo "No changes to commit"
|
|
git push origin HEAD:main
|