feat(config): shiftEnterNewline option + client config injection #600
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |