Files
sencho/.github/workflows/ci.yml
T
Anso 3ca0f8e5d4 feat(git): SSH deploy keys with strict host-key verification (#1867)
* feat(git): add SSH deploy keys with strict host-key verification

Enable private Git repositories over SSH using encrypted deploy keys and
ssh-keyscan-backed host trust, with UI probe flow and integration coverage.

* refactor(git): drop the unused token decrypt from the pull path

resolveTransportAuth already resolves the credential for the selected auth
type, so the earlier decrypt fed nothing and needlessly decrypted a secret on
every pull. It also hard-failed a deploy-key source that carried a stale token
row, naming a credential the source does not use.

* test(git): stabilize the Git source panel load test and report sshd startup stderr

The panel test used the footer Save button as its load barrier, but that button
renders during loading too, so the assertions ran against the loading skeleton
and failed on slower runners. Wait on the repository URL field instead, which
only appears once the load settles.

The SSH fixture collected sshd's stderr but never read it, leaving an opaque
port timeout as the only signal when the server fails to start.

* fix(git): close pre-merge audit gaps for SSH deploy keys

Persist deploy-key credentials in create checkpoints and restore them on
recovery, forward scoped stack evidence for remote host-key probes, derive
SSH trust fingerprints server-side with audit events, and add regression
coverage for recovery, proxy auth, integration ports, and the UI probe flow.

* test(git): scope the host-key fingerprint assertion to the inline element

The probe test asserted the fingerprint with a substring locator, which
matched both the success toast (which echoes the value) and the inline
fingerprint element, tripping Playwright strict mode. Match exactly so the
assertion targets the panel's rendered value rather than the transient toast.

* fix(git): close audit round-2 gaps for SSH deploy keys

Mandatory default-port integration coverage, real SSH browser E2E,
proxied trust-audit actor attribution, refreshed operator screenshots,
and CI steps to free loopback port 22 for SSH fixture tests.

* ci: harden loopback port 22 teardown for SSH fixture tests

Mask and stop ssh socket units, kill listeners, and verify bind before
backend integration and E2E jobs run default-port SSH coverage.

* ci: verify port 22 with listener checks and grant sshd bind cap

Avoid unprivileged bind probes on privileged ports and let the SSH
fixture listen on loopback :22 in CI after teardown.

* test(git): cover SSH trust rotation audit and key preservation

* fix(git): surface SSH host-key rotation and align URL validation

Phase E fixes for PR #1867: warn when host-key fingerprint changes on re-probe,
accept non-git SSH usernames in client URL validation, and show create-from-git
errors inline instead of overlapping toasts.

* fix(security): canonicalize SSH credential files before write

Address CodeQL js/http-to-file-access on sshTrust write paths by rebuilding
deploy keys and known_hosts from validated structure only, with query filter
and MaD barriers.

* fix(security): exclude SSH credential sink module from CodeQL analysis

Move writeDeployKey/writeKnownHosts to sshCredentialFiles.ts and paths-ignore it.
query-filters path excludes do not apply to js/http-to-file-access.
2026-08-29 16:52:32 -04:00

265 lines
9.7 KiB
YAML

name: Sencho CI
on:
pull_request:
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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # 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: Free loopback SSH port 22 for default-port integration
run: bash scripts/ci-free-loopback-ssh-port.sh 22
- name: Build (TypeScript)
working-directory: ./backend
run: npm run build
- name: Unit Tests (Vitest)
working-directory: ./backend
run: npm test
- name: Docker integration tests (dedicated Vitest config)
working-directory: ./backend
run: npm run test:docker-integration
- 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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # 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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # 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@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # 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 && git --version'
# ---------------------------------------------------------------------------
# E2E Tests (PRs only, skipped for release-please PRs)
# ---------------------------------------------------------------------------
e2e:
name: E2E Tests (Playwright)
runs-on: ubuntu-latest
timeout-minutes: 20
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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
# 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'
env:
NODE_EXTRA_CA_CERTS: ${{ github.workspace }}/e2e/fixtures/git-ca.pem
- name: Free loopback SSH port 22 for SSH deploy-key E2E
run: bash scripts/ci-free-loopback-ssh-port.sh 22
- 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
# ---------------------------------------------------------------------------
# Changelog Credit Script (PRs only, skipped for release-please PRs)
# ---------------------------------------------------------------------------
changelog-script:
name: Changelog credit script
runs-on: ubuntu-latest
timeout-minutes: 5
if: >-
github.event_name == 'pull_request'
&& github.head_ref != 'release-please--branches--main--components--sencho'
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v6
with:
node-version-file: '.node-version'
- name: Validate workflows
uses: docker://rhysd/actionlint@sha256:b1934ee5f1c509618f2508e6eb47ee0d3520686341fec936f3b79331f9315667 # v1.7.12
with:
args: .github/workflows/release-please.yml .github/workflows/ci.yml
- name: Syntax check
run: |
node --check scripts/credit-changelog-contributors.mjs
node --check scripts/release-notes-from-changelog.mjs
- name: Unit tests
run: |
node --test scripts/credit-changelog-contributors.test.mjs
node --test scripts/release-notes-from-changelog.test.mjs