ci: harden CI and supply-chain pipeline (#1169)

* 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.
This commit is contained in:
Anso
2026-05-23 02:37:29 -04:00
committed by GitHub
parent 0947da13ae
commit efcc06d50b
12 changed files with 208 additions and 16 deletions
+3 -3
View File
@@ -28,9 +28,9 @@ runs:
using: composite using: composite
steps: steps:
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v6 uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with: with:
node-version: '20' node-version-file: '.node-version'
cache: 'npm' cache: 'npm'
cache-dependency-path: | cache-dependency-path: |
package-lock.json package-lock.json
@@ -101,7 +101,7 @@ runs:
- name: Cache Playwright browsers - name: Cache Playwright browsers
id: playwright-cache id: playwright-cache
uses: actions/cache@v4 uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with: with:
path: ~/.cache/ms-playwright path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ steps.playwright-version.outputs.version }} key: playwright-${{ runner.os }}-${{ steps.playwright-version.outputs.version }}
+33 -1
View File
@@ -1,5 +1,8 @@
version: 2 version: 2
updates: updates:
# npm (root) - playwright/husky/commitlint
# Security advisories batch into their own PR (labeled "security") so they
# can be reviewed and merged ahead of the noisier weekly version-update PR.
- package-ecosystem: "npm" - package-ecosystem: "npm"
directory: "/" directory: "/"
schedule: schedule:
@@ -9,6 +12,10 @@ updates:
open-pull-requests-limit: 10 open-pull-requests-limit: 10
groups: groups:
all-npm-root: all-npm-root:
applies-to: version-updates
patterns: ["*"]
all-npm-root-security:
applies-to: security-updates
patterns: ["*"] patterns: ["*"]
- package-ecosystem: "npm" - package-ecosystem: "npm"
@@ -20,6 +27,10 @@ updates:
open-pull-requests-limit: 10 open-pull-requests-limit: 10
groups: groups:
all-npm-backend: all-npm-backend:
applies-to: version-updates
patterns: ["*"]
all-npm-backend-security:
applies-to: security-updates
patterns: ["*"] patterns: ["*"]
- package-ecosystem: "npm" - package-ecosystem: "npm"
@@ -31,21 +42,42 @@ updates:
open-pull-requests-limit: 10 open-pull-requests-limit: 10
groups: groups:
all-npm-frontend: all-npm-frontend:
applies-to: version-updates
patterns: ["*"]
all-npm-frontend-security:
applies-to: security-updates
patterns: ["*"] patterns: ["*"]
# Docker base images. No version-update group: keep base-image bumps as
# individual PRs so each digest change gets its own review. Security
# updates still batch into one PR for quick triage.
- package-ecosystem: "docker" - package-ecosystem: "docker"
directory: "/" directory: "/"
schedule: schedule:
interval: "weekly" interval: "weekly"
labels: labels:
- "dependencies" - "dependencies"
groups:
docker-security:
applies-to: security-updates
patterns: ["*"]
# github-actions: scan both top-level workflows (.github/workflows/) and the
# local composite action (.github/actions/start-app/action.yml). Dependabot
# only walks each directory it is given, so the composite must be listed
# explicitly for its uses: pins to be tracked.
- package-ecosystem: "github-actions" - package-ecosystem: "github-actions"
directory: "/" directories:
- "/"
- "/.github/actions/start-app"
schedule: schedule:
interval: "weekly" interval: "weekly"
labels: labels:
- "ci" - "ci"
groups: groups:
all-actions: all-actions:
applies-to: version-updates
patterns: ["*"]
all-actions-security:
applies-to: security-updates
patterns: ["*"] patterns: ["*"]
+6 -3
View File
@@ -35,7 +35,7 @@ jobs:
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with: with:
node-version: '20' node-version-file: '.node-version'
cache: 'npm' cache: 'npm'
cache-dependency-path: backend/package-lock.json cache-dependency-path: backend/package-lock.json
@@ -85,7 +85,7 @@ jobs:
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with: with:
node-version: '20' node-version-file: '.node-version'
cache: 'npm' cache: 'npm'
cache-dependency-path: frontend/package-lock.json cache-dependency-path: frontend/package-lock.json
@@ -97,6 +97,10 @@ jobs:
working-directory: ./frontend working-directory: ./frontend
run: npm run build run: npm run build
- name: Unit Tests (Vitest)
working-directory: ./frontend
run: npm test
- name: Lint (ESLint) - name: Lint (ESLint)
working-directory: ./frontend working-directory: ./frontend
run: npm run lint run: npm run lint
@@ -153,7 +157,6 @@ jobs:
exit-code: '1' exit-code: '1'
severity: 'CRITICAL,HIGH' severity: 'CRITICAL,HIGH'
format: 'table' format: 'table'
trivy-version: 'latest'
trivy-config: trivy.yaml trivy-config: trivy.yaml
# Mirror of the release-time pre-publish smoke gate in docker-publish.yml. # Mirror of the release-time pre-publish smoke gate in docker-publish.yml.
+16 -2
View File
@@ -5,8 +5,13 @@ on:
pull_request_target: pull_request_target:
types: [opened,closed,synchronize] types: [opened,closed,synchronize]
# Minimum scopes contributor-assistant/github-action needs to operate:
# contents: write - commit signatures to signatures/version1/cla.json
# pull-requests: write - comment on the PR with the CLA prompt
# statuses: write - set the commit status (success / pending)
# actions: write is intentionally NOT granted; this workflow does not manage
# other workflow runs.
permissions: permissions:
actions: write
contents: write contents: write
pull-requests: write pull-requests: write
statuses: write statuses: write
@@ -16,7 +21,16 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: "CLA Assistant" - name: "CLA Assistant"
if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target' # Only run on pull_request_target events, or on issue_comment events
# that are (a) on a PR (not a plain Issue) and (b) match one of the two
# documented CLA phrases. github.event.issue.pull_request is truthy
# only when the issue is a PR, so plain-Issue comments are ignored.
if: |
github.event_name == 'pull_request_target' ||
(github.event_name == 'issue_comment' &&
github.event.issue.pull_request != null &&
(github.event.comment.body == 'recheck' ||
github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA'))
uses: contributor-assistant/github-action@ca4a40a7d1004f18d9960b404b97e5f30a505a08 # v2.6.1 uses: contributor-assistant/github-action@ca4a40a7d1004f18d9960b404b97e5f30a505a08 # v2.6.1
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+4 -2
View File
@@ -7,7 +7,10 @@ on:
workflow_dispatch: workflow_dispatch:
concurrency: concurrency:
group: docker-publish # Ref-scoped so two different release tags (e.g. v0.86.4 and v0.86.5) cannot
# cancel each other. Reruns of the exact same ref still cancel the prior
# run for that ref, which is the cancel behavior we actually want.
group: docker-publish-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true
jobs: jobs:
@@ -132,7 +135,6 @@ jobs:
exit-code: '1' exit-code: '1'
severity: 'CRITICAL,HIGH' severity: 'CRITICAL,HIGH'
format: 'table' format: 'table'
trivy-version: 'latest'
trivy-config: trivy.yaml trivy-config: trivy.yaml
# Start the scanned image headless on the runner and poll /api/health # Start the scanned image headless on the runner and poll /api/health
+104
View File
@@ -0,0 +1,104 @@
name: Scheduled Security Scan
# Visibility-only re-scan of the published image and a fresh build of main.
# Surfaces CVEs disclosed between releases by uploading SARIF to GitHub Code
# Scanning. The PR-blocking gate in ci.yml and the release-blocking gate in
# docker-publish.yml are unchanged; this workflow only writes findings.
on:
schedule:
- cron: '0 0 * * *' # Daily 00:00 UTC
workflow_dispatch:
# Prevent overlap when a manual workflow_dispatch fires while the daily cron
# run is still in flight. cancel-in-progress is safe here: the workflow only
# uploads SARIF for visibility and has no side effects on the registry.
concurrency:
group: security-scan
cancel-in-progress: true
permissions:
contents: read
jobs:
scan-published:
name: Trivy SARIF (published :latest)
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
security-events: write
steps:
- name: Checkout (trivy.yaml + VEX)
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
# Same SHA-pinned action used by the PR-blocking and release-blocking
# scans. The trivy binary version is whatever this action SHA bundles;
# Dependabot's all-actions group moves it forward when the action is
# bumped, keeping a single source of truth.
- name: Trivy scan of saelix/sencho:latest
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
with:
image-ref: saelix/sencho:latest
format: sarif
output: trivy-published.sarif
severity: 'CRITICAL,HIGH,MEDIUM'
trivy-config: trivy.yaml
ignore-unfixed: false
# if: always() so findings still upload when trivy itself failed mid-run
# (e.g. CVE DB pull timeout). The categorical separation from the
# scan-main job below keeps the two result sets distinct in the UI.
- name: Upload SARIF to code scanning
if: always()
uses: github/codeql-action/upload-sarif@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
with:
sarif_file: trivy-published.sarif
category: trivy-published-image
scan-main:
name: Trivy SARIF (main HEAD build)
runs-on: ubuntu-latest
timeout-minutes: 25
permissions:
contents: read
security-events: write
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
# Mirrors the daily-cache-bust logic from ci.yml / docker-publish.yml so
# the apk upgrade layer rebuilds at least once per calendar day.
- name: Compute daily apk cache bust value
id: apk-bust
run: echo "date=$(date -u +%Y-%m-%d)" >> "$GITHUB_OUTPUT"
- name: Build image from main HEAD
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7
with:
context: .
push: false
load: true
tags: localhost/sencho:scheduled-scan
cache-from: type=gha
build-args: |
APK_CACHE_BUST=${{ steps.apk-bust.outputs.date }}
- name: Trivy scan of fresh main build
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
with:
image-ref: localhost/sencho:scheduled-scan
format: sarif
output: trivy-main.sarif
severity: 'CRITICAL,HIGH,MEDIUM'
trivy-config: trivy.yaml
ignore-unfixed: false
- name: Upload SARIF to code scanning
if: always()
uses: github/codeql-action/upload-sarif@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
with:
sarif_file: trivy-main.sarif
category: trivy-main-head
+1
View File
@@ -0,0 +1 @@
26
+23 -5
View File
@@ -99,17 +99,26 @@ RUN if [ "$TARGETARCH" = "$BUILDARCH" ]; then \
# static binary. # static binary.
# #
# Runs on the BUILD platform; GOARCH cross-compiles the static binary for TARGET. # Runs on the BUILD platform; GOARCH cross-compiles the static binary for TARGET.
# The --depth 1 clone fetches only the v29.4.1 tag commit, minimising transfer size. # The fetch pulls only the v29.4.1 commit, minimising transfer size.
# docker/cli uses CalVer and ships vendor.mod instead of go.mod to avoid SemVer # docker/cli uses CalVer and ships vendor.mod instead of go.mod to avoid SemVer
# compliance requirements. We copy vendor.mod -> go.mod and build with -mod=vendor # compliance requirements. We copy vendor.mod -> go.mod and build with -mod=vendor
# so all deps come from the vendored tree (no network access needed). # so all deps come from the vendored tree (no network access needed).
FROM --platform=$BUILDPLATFORM golang:1.26.3-alpine AS cli-builder # Base image pinned by digest so the Go toolchain that compiles the static
# Docker CLI binary cannot change without an explicit Dependabot bump.
FROM --platform=$BUILDPLATFORM golang:1.26.3-alpine@sha256:91eda9776261207ea25fd06b5b7fed8d397dd2c0a283e77f2ab6e91bfa71079d AS cli-builder
ARG TARGETARCH ARG TARGETARCH
RUN apk add --no-cache git RUN apk add --no-cache git
RUN git clone --depth 1 --branch v29.4.1 https://github.com/docker/cli.git /src/docker-cli # Fetch by commit SHA rather than by mutable tag. The SHA below resolves to
# the docker/cli v29.4.1 release; recorded here for traceability since the
# raw SHA does not carry semantic information.
RUN git init /src/docker-cli && \
cd /src/docker-cli && \
git remote add origin https://github.com/docker/cli.git && \
git fetch --depth=1 origin 407f3428e5c5a3a4088f9268bc7159f5e0f95bea && \
git checkout FETCH_HEAD
WORKDIR /src/docker-cli WORKDIR /src/docker-cli
@@ -135,13 +144,22 @@ RUN cp vendor.mod go.mod && cp vendor.sum go.sum && \
# v0.29.0. The go get step below bumps otel to v1.43.0 to resolve # v0.29.0. The go get step below bumps otel to v1.43.0 to resolve
# CVE-2026-39883 (BSD kenv) and CVE-2026-39882 (OTLP response OOM) so that # CVE-2026-39883 (BSD kenv) and CVE-2026-39882 (OTLP response OOM) so that
# the compose binary scans completely clean. # the compose binary scans completely clean.
FROM --platform=$BUILDPLATFORM golang:1.26.3-alpine AS compose-builder # Base image pinned by digest (same image as cli-builder above) so both
# source builds share an identical, immutable Go toolchain.
FROM --platform=$BUILDPLATFORM golang:1.26.3-alpine@sha256:91eda9776261207ea25fd06b5b7fed8d397dd2c0a283e77f2ab6e91bfa71079d AS compose-builder
ARG TARGETARCH ARG TARGETARCH
RUN apk add --no-cache git RUN apk add --no-cache git
RUN git clone --depth 1 --branch v5.1.3 https://github.com/docker/compose.git /src/docker-compose # Fetch by commit SHA. The SHA below resolves to the docker/compose v5.1.3
# release; recorded here for traceability since the raw SHA does not carry
# semantic information.
RUN git init /src/docker-compose && \
cd /src/docker-compose && \
git remote add origin https://github.com/docker/compose.git && \
git fetch --depth=1 origin 5b2badbda44f3410b2a6c58dff79def21fe8b13e && \
git checkout FETCH_HEAD
WORKDIR /src/docker-compose WORKDIR /src/docker-compose
+3
View File
@@ -18,6 +18,9 @@
"author": "", "author": "",
"license": "SEE LICENSE IN LICENSE", "license": "SEE LICENSE IN LICENSE",
"type": "commonjs", "type": "commonjs",
"engines": {
"node": ">=26.0.0"
},
"devDependencies": { "devDependencies": {
"@eslint/js": "^10.0.1", "@eslint/js": "^10.0.1",
"@types/bcrypt": "^6.0.0", "@types/bcrypt": "^6.0.0",
+3
View File
@@ -4,6 +4,9 @@
"license": "SEE LICENSE IN LICENSE", "license": "SEE LICENSE IN LICENSE",
"version": "0.0.0", "version": "0.0.0",
"type": "module", "type": "module",
"engines": {
"node": ">=26.0.0"
},
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "tsc -b && vite build", "build": "tsc -b && vite build",
+9
View File
@@ -11,6 +11,15 @@ export default defineConfig({
}, },
test: { test: {
environment: 'jsdom', environment: 'jsdom',
// jsdom only instantiates window.localStorage / sessionStorage when the
// document has a real same-origin URL. The default about:blank origin is
// opaque and can leave both APIs undefined depending on the jsdom build,
// causing any test that touches localStorage in beforeEach to throw.
// Setting an explicit URL is the documented jsdom workaround and changes
// no test or app code.
environmentOptions: {
jsdom: { url: 'http://localhost' },
},
globals: true, globals: true,
setupFiles: ['./src/__tests__/setup.ts'], setupFiles: ['./src/__tests__/setup.ts'],
include: ['src/**/*.{test,spec}.{ts,tsx}'], include: ['src/**/*.{test,spec}.{ts,tsx}'],
+3
View File
@@ -17,6 +17,9 @@
"author": "Studio Saelix", "author": "Studio Saelix",
"license": "SEE LICENSE IN LICENSE", "license": "SEE LICENSE IN LICENSE",
"type": "commonjs", "type": "commonjs",
"engines": {
"node": ">=26.0.0"
},
"bugs": { "bugs": {
"url": "https://github.com/studio-saelix/sencho/issues" "url": "https://github.com/studio-saelix/sencho/issues"
}, },