Skip to content

feat(config): shiftEnterNewline option + client config injection #600

feat(config): shiftEnterNewline option + client config injection

feat(config): shiftEnterNewline option + client config injection #600

Workflow file for this run

name: CI
on:
push:
branches: [ main, master, develop, feat/**, release-please--branches--** ]
pull_request:
branches: [ '**' ]
workflow_dispatch:
inputs:
bundle_verify_outage_override:
description: >-
Bypass client-bundle verification ONLY for a confirmed GitHub
release / Attestations / Rekor outage. Tamper failures still block.
The bypass is recorded in this run's logs and actor.
type: boolean
default: false
jobs:
build-lint-test:
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
steps:
- name: Checkout
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
with:
fetch-depth: 1
- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 22
cache: 'npm'
- name: Install dependencies
# Skip lifecycle scripts so a compromised dependency cannot execute
# arbitrary code in CI; the scripts this pipeline actually needs are
# re-run explicitly in the next step.
run: npm ci --ignore-scripts
- name: Restore native/runtime dependencies
# better-sqlite3 is the only runtime-critical native module (host-key
# store); its prebuilt binding is fetched by its install script.
# prepare:runtime is this repo's own postinstall, run explicitly.
run: |
npm rebuild better-sqlite3
npm run prepare:runtime
- name: Lint
run: npm run lint
- name: Typecheck
run: npm run typecheck
- name: Build
run: npm run build
- name: Unit/Integration tests
run: npm run test
- name: Security audit
run: npm audit --audit-level=high
- name: Trivy vulnerability scan
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
with:
scan-type: 'fs'
scan-ref: '.'
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
ignore-unfixed: true
- name: Upload Trivy scan results
uses: github/codeql-action/upload-sarif@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
with:
sarif_file: 'trivy-results.sarif'
verify-client-bundle:
# Verify the webssh2_client bundle's integrity + provenance (issue #547).
# Network- and token-dependent, so it is skipped on fork/dependabot PRs
# (read-only token / withheld secrets); the same-repo and main-branch runs
# are the gate of record.
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Determine trust gate
id: trust
env:
EVENT_NAME: ${{ github.event_name }}
HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }}
BASE_REPO: ${{ github.repository }}
ACTOR: ${{ github.actor }}
run: |
set -euo pipefail
if [ "$EVENT_NAME" = 'pull_request' ] && [ "$HEAD_REPO" != "$BASE_REPO" ]; then
echo 'run=false' >> "$GITHUB_OUTPUT"
echo 'Fork PR; bundle verification skipped (advisory; same-repo/main run is the gate).'
elif [ "$ACTOR" = 'dependabot[bot]' ]; then
echo 'run=false' >> "$GITHUB_OUTPUT"
echo 'Dependabot PR; bundle verification skipped (secrets withheld; main run is the gate).'
else
echo 'run=true' >> "$GITHUB_OUTPUT"
fi
- name: Checkout
if: steps.trust.outputs.run == 'true'
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
with:
fetch-depth: 1
- name: Setup Node.js
if: steps.trust.outputs.run == 'true'
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 22
cache: 'npm'
- name: Install dependencies
if: steps.trust.outputs.run == 'true'
# Bundle verification only needs tsx + the (pre-built) webssh2_client
# package, so skip lifecycle scripts — no native builds required here.
run: npm ci --ignore-scripts
- name: Assert gh CLI supports pinned attestation verification
if: steps.trust.outputs.run == 'true'
run: |
set -euo pipefail
gh --version
ver=$(gh --version | sed -n 's/^gh version \([0-9.]*\).*/\1/p')
# gh attestation verify with --cert-identity is GA since 2.49.0.
required=2.49.0
lowest=$(printf '%s\n%s\n' "$ver" "$required" | sort -V | head -n1)
if [ "$lowest" != "$required" ]; then
echo "::error::gh $ver is older than required $required for attestation verify"
exit 1
fi
- name: Negative control — wrong signer identity must be rejected
if: steps.trust.outputs.run == 'true'
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
ver=$(node -p "require('./package-lock.json').packages['node_modules/webssh2_client'].version")
curl -fsSL --proto '=https' --tlsv1.2 --max-filesize 1048576 -o /tmp/cs-negative.txt \
"https://github.com/billchurch/webssh2_client/releases/download/v${ver}/checksums.txt"
if gh attestation verify /tmp/cs-negative.txt \
--repo billchurch/webssh2_client \
--cert-identity 'https://github.com/billchurch/webssh2_client/.github/workflows/evil.yml@refs/heads/main' \
>/dev/null 2>&1; then
echo '::error::Negative control PASSED verification — gh identity pinning is not effective'
exit 1
fi
echo 'Negative control OK: a wrong signer identity is rejected.'
- name: Verify client bundle integrity + provenance
if: steps.trust.outputs.run == 'true'
env:
GH_TOKEN: ${{ github.token }}
WEBSSH2_BUNDLE_VERIFY_OUTAGE_OVERRIDE: ${{ github.event_name == 'workflow_dispatch' && inputs.bundle_verify_outage_override && 'true' || 'false' }}
run: npm run security:verify-bundle
- name: Verify registry signatures + provenance attestations
if: steps.trust.outputs.run == 'true'
run: npm audit signatures
dependency-review:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
permissions:
contents: read
pull-requests: write
steps:
- name: Checkout
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
- name: Dependency Review
uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0
with:
fail-on-severity: high
comment-summary-in-pr: on-failure
docker-image-scan:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
concurrency:
group: docker-image-scan-${{ github.event.pull_request.number }}
cancel-in-progress: true
permissions:
contents: read
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
- id: filter
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
with:
filters: |
image:
- 'Dockerfile'
- 'package-lock.json'
- 'package.json'
- '.github/workflows/docker-publish.yml'
- name: Note when nothing image-relevant changed
if: steps.filter.outputs.image != 'true'
run: echo "No image-relevant changes; passing required check as a no-op."
- name: Note fork PR skip
if: >-
steps.filter.outputs.image == 'true' &&
github.event.pull_request.head.repo.full_name != github.repository
run: echo "Fork PR; image scan skipped (advisory; main-branch scan is the gate)."
- name: Note dependabot PR skip
if: >-
steps.filter.outputs.image == 'true' &&
github.event.pull_request.head.repo.full_name == github.repository &&
github.actor == 'dependabot[bot]'
run: echo "Dependabot PR; image scan skipped (advisory; main-branch scan is the gate). GitHub does not expose repo secrets to dependabot-triggered workflows by default, so the Docker Hub login required here cannot succeed."
- name: Login to Docker Hub (raise anon rate-limit)
if: >-
steps.filter.outputs.image == 'true' &&
github.event.pull_request.head.repo.full_name == github.repository &&
github.actor != 'dependabot[bot]'
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Set up buildx
if: >-
steps.filter.outputs.image == 'true' &&
github.event.pull_request.head.repo.full_name == github.repository &&
github.actor != 'dependabot[bot]'
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Build amd64 image for scanning
if: >-
steps.filter.outputs.image == 'true' &&
github.event.pull_request.head.repo.full_name == github.repository &&
github.actor != 'dependabot[bot]'
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: .
load: true
tags: webssh2:pr-${{ github.event.pull_request.number }}
platforms: linux/amd64
cache-from: type=gha,scope=publish-refs/heads/main
- name: Trivy image scan
if: >-
steps.filter.outputs.image == 'true' &&
github.event.pull_request.head.repo.full_name == github.repository &&
github.actor != 'dependabot[bot]'
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
with:
image-ref: webssh2:pr-${{ github.event.pull_request.number }}
format: table
severity: CRITICAL,HIGH
ignore-unfixed: true
exit-code: '1'
cache: true
trivyignores: .trivyignore