diff --git a/.claude/settings.json b/.claude/settings.json index 4e1c91b02..122fe626c 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -10,5 +10,9 @@ }, "worktree": { "bgIsolation": "none" + }, + "attribution": { + "commit": "", + "pr": "" } } \ No newline at end of file diff --git a/.dockerignore b/.dockerignore index 0b7988b01..fd65d4dc9 100644 --- a/.dockerignore +++ b/.dockerignore @@ -10,7 +10,6 @@ .gitignore .github/ codecov.yml -.goreleaser.yaml .sourcery.yml .claude/ @@ -155,7 +154,7 @@ docker-compose*.yml docs/implementation/ # ----------------------------------------------------------------------------- -# GoReleaser & dist artifacts +# Build output directories (e.g. frontend/dist/) # ----------------------------------------------------------------------------- dist/ diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml deleted file mode 100644 index 85ff1f0f6..000000000 --- a/.github/release-drafter.yml +++ /dev/null @@ -1,26 +0,0 @@ -name-template: 'v$NEXT_PATCH_VERSION' -tag-template: 'v$NEXT_PATCH_VERSION' -categories: - - title: '๐Ÿš€ Features' - labels: - - 'feature' - - 'feat' - - title: '๐Ÿ› Fixes' - labels: - - 'bug' - - 'fix' - - title: '๐Ÿงฐ Maintenance' - labels: - - 'chore' - - title: '๐Ÿงช Tests' - labels: - - 'test' -change-template: '- $TITLE @$AUTHOR (#$NUMBER)' -template: | - ## What's Changed - - $CHANGES - - ---- - - Full Changelog: https://github.com/${{ github.repository }}/compare/$FROM_TAG...$TO_TAG diff --git a/.github/renovate.json b/.github/renovate.json index 1a5a68d8d..115c14938 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -244,6 +244,19 @@ "datasourceTemplate": "golang-version", "versioningTemplate": "semver" }, + { + "customType": "regex", + "description": "Track pinned Semgrep container image digest in the Semgrep CI workflow", + "managerFilePatterns": [ + "/^\\.github/workflows/semgrep\\.yml$/" + ], + "matchStrings": [ + "#\\s*renovate:\\s*datasource=docker\\s+depName=semgrep/semgrep\\s*\\n\\s*image:\\s*semgrep/semgrep:(?[^@\\s]+)@(?sha256:[a-f0-9]+)" + ], + "depNameTemplate": "semgrep/semgrep", + "datasourceTemplate": "docker", + "versioningTemplate": "docker" + }, { "customType": "regex", "description": "Track NODE_VERSION in Actions workflows", @@ -275,11 +288,15 @@ "description": "Track Syft version in workflows and scripts", "managerFilePatterns": [ "/^\\.github/workflows/nightly-build\\.yml$/", + "/^\\.github/workflows/docker-build\\.yml$/", + "/^\\.github/workflows/supply-chain-pr\\.yml$/", + "/^\\.github/workflows/supply-chain-verify\\.yml$/", "/^\\.github/skills/security-scan-docker-image-scripts/run\\.sh$/" ], "matchStrings": [ "SYFT_VERSION=\\\"v(?[^\\\"\\s]+)\\\"", - "set_default_env \\\"SYFT_VERSION\\\" \\\"v(?[^\\\"]+)\\\"" + "set_default_env \\\"SYFT_VERSION\\\" \\\"v(?[^\\\"]+)\\\"", + "syft-version:\\s*v(?[^\\s]+)" ], "depNameTemplate": "anchore/syft", "datasourceTemplate": "github-releases", @@ -396,6 +413,19 @@ "datasourceTemplate": "go", "versioningTemplate": "semver" }, + { + "customType": "regex", + "description": "Track github.com/klauspost/compress version ARG in Dockerfile", + "managerFilePatterns": [ + "/^Dockerfile$/" + ], + "matchStrings": [ + "#\\s*renovate:\\s*datasource=go\\s+depName=github\\.com/klauspost/compress\\s*\\nARG KLAUSPOST_COMPRESS_VERSION=(?[^\\s]+)" + ], + "depNameTemplate": "github.com/klauspost/compress", + "datasourceTemplate": "go", + "versioningTemplate": "semver" + }, { "customType": "regex", "description": "Track golang-jwt/jwt v5 in go.mod via github-tags (workaround: Renovate go datasource no-result for /vN module paths)", diff --git a/.github/skills/README.md b/.github/skills/README.md index c6a6ce846..96a02e42e 100644 --- a/.github/skills/README.md +++ b/.github/skills/README.md @@ -69,7 +69,6 @@ Agent Skills are self-documenting, AI-discoverable task definitions that combine | Skill Name | Category | Description | Status | |------------|----------|-------------|--------| -| [utility-version-check](./utility-version-check.SKILL.md) | utility | Validate version matches git tag | โœ… Active | | [utility-clear-go-cache](./utility-clear-go-cache.SKILL.md) | utility | Clear Go build and module caches | โœ… Active | | [utility-bump-beta](./utility-bump-beta.SKILL.md) | utility | Increment beta version number | โœ… Active | | [utility-db-recovery](./utility-db-recovery.SKILL.md) | utility | Database integrity check and recovery | โœ… Active | @@ -264,7 +263,6 @@ python3 .github/skills/scripts/validate-skills.py --single .github/skills/{skill - `test-backend-coverage` - `integration-test-crowdsec` - `security-scan-trivy` - - `utility-version-check` ## Best Practices diff --git a/.github/skills/examples/gorm-scanner-ci-workflow.yml b/.github/skills/examples/gorm-scanner-ci-workflow.yml index 0666e5b74..fb5383608 100644 --- a/.github/skills/examples/gorm-scanner-ci-workflow.yml +++ b/.github/skills/examples/gorm-scanner-ci-workflow.yml @@ -25,7 +25,7 @@ jobs: - name: Setup Go uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7 with: - go-version: "1.26.5" + go-version: "1.26.6" - name: Run GORM Security Scanner id: gorm-scan diff --git a/.github/skills/security-scan-docker-image-scripts/run.sh b/.github/skills/security-scan-docker-image-scripts/run.sh index 73f6042e8..f5e614524 100755 --- a/.github/skills/security-scan-docker-image-scripts/run.sh +++ b/.github/skills/security-scan-docker-image-scripts/run.sh @@ -35,7 +35,7 @@ fi # Check Grype if ! command -v grype >/dev/null 2>&1; then log_error "Grype not found - install from: https://github.com/anchore/grype" - log_error "Installation: curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin v0.116.1" + log_error "Installation: curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin v0.117.0" error_exit "Grype is required for vulnerability scanning" 2 fi @@ -50,8 +50,8 @@ SYFT_INSTALLED_VERSION=$(syft version | grep -oP 'Version:\s*\Kv?[0-9]+\.[0-9]+\ GRYPE_INSTALLED_VERSION=$(grype version | grep -oP 'Version:\s*\Kv?[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "unknown") # Set defaults matching CI workflow -set_default_env "SYFT_VERSION" "v1.50.0" -set_default_env "GRYPE_VERSION" "v0.116.1" +set_default_env "SYFT_VERSION" "v1.51.0" +set_default_env "GRYPE_VERSION" "v0.117.0" set_default_env "IMAGE_TAG" "charon:local" set_default_env "FAIL_ON_SEVERITY" "Critical,High" diff --git a/.github/skills/utility-bump-beta.SKILL.md b/.github/skills/utility-bump-beta.SKILL.md index 34e552cdc..365e155ed 100644 --- a/.github/skills/utility-bump-beta.SKILL.md +++ b/.github/skills/utility-bump-beta.SKILL.md @@ -183,7 +183,6 @@ After running this skill: ## Related Skills -- [utility-version-check](./utility-version-check.SKILL.md) - Validate version matches tags - [build-check-go](../build-check-go.SKILL.md) - Verify build after version bump ## Notes diff --git a/.github/skills/utility-version-check-scripts/run.sh b/.github/skills/utility-version-check-scripts/run.sh deleted file mode 100755 index 3bf38c71d..000000000 --- a/.github/skills/utility-version-check-scripts/run.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# ============================================================================== -# Utility: Version Check - Execution Script -# ============================================================================== -# This script validates that the .version file matches the latest git tag. -# It wraps the original check-version-match-tag.sh script. -# -# Usage: ./run.sh -# Exit codes: 0 = success, 1 = version mismatch -# ============================================================================== - -# Determine the repository root directory -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" - -# Change to repository root -cd "$REPO_ROOT" - -# Execute the version check script -exec scripts/check-version-match-tag.sh "$@" diff --git a/.github/skills/utility-version-check.SKILL.md b/.github/skills/utility-version-check.SKILL.md deleted file mode 100644 index 9f31665da..000000000 --- a/.github/skills/utility-version-check.SKILL.md +++ /dev/null @@ -1,142 +0,0 @@ ---- -name: "utility-version-check" -version: "1.0.0" -description: "Validates that VERSION.md/version file matches the latest git tag for release consistency" -author: "Charon Project" -license: "MIT" -tags: - - "utility" - - "versioning" - - "validation" - - "git" -compatibility: - os: - - "linux" - - "darwin" - shells: - - "bash" -requirements: - - name: "git" - version: ">=2.0" - optional: false -environment_variables: [] -parameters: [] -outputs: - - name: "exit_code" - type: "integer" - description: "0 if version matches, 1 if mismatch or error" -metadata: - category: "utility" - subcategory: "versioning" - execution_time: "short" - risk_level: "low" - ci_cd_safe: true - requires_network: false - idempotent: true ---- - -# Utility: Version Check - -## Overview - -Validates that the version specified in `.version` file matches the latest git tag. This ensures version consistency across the codebase and prevents version drift during releases. The check is used in CI/CD to enforce version tagging discipline. - -## Prerequisites - -- Git repository with tags -- `.version` file in repository root (optional) - -## Usage - -### Basic Usage - -```bash -.github/skills/utility-version-check-scripts/run.sh -``` - -### Via Skill Runner - -```bash -.github/skills/scripts/skill-runner.sh utility-version-check -``` - -### Via VS Code Task - -Use the task: **Utility: Check Version Match Tag** - -## Parameters - -This skill accepts no parameters. - -## Environment Variables - -This skill requires no environment variables. - -## Outputs - -- **Success Exit Code**: 0 - Version matches latest tag or no tags exist -- **Error Exit Codes**: 1 - Version mismatch detected -- **Console Output**: Validation result message - -### Success Output Example - -``` -OK: .version matches latest Git tag v0.3.0-beta.2 -``` - -### Error Output Example - -``` -ERROR: .version (0.3.0-beta.3) does not match latest Git tag (v0.3.0-beta.2) -To sync, either update .version or tag with 'v0.3.0-beta.3' -``` - -## Examples - -### Example 1: Check Version During Release - -```bash -# Before tagging a new release -.github/skills/utility-version-check-scripts/run.sh -``` - -### Example 2: CI/CD Integration - -```yaml -- name: Validate Version - run: .github/skills/scripts/skill-runner.sh utility-version-check -``` - -## Version Normalization - -The skill normalizes both the `.version` file content and git tag by: -- Stripping leading `v` prefix (e.g., `v1.0.0` โ†’ `1.0.0`) -- Removing newline and carriage return characters -- Comparing normalized versions - -This allows flexibility in tagging conventions while ensuring consistency. - -## Error Handling - -- **No .version file**: Exits with 0 (skip check) -- **No git tags**: Exits with 0 (skip check, allows commits before first tag) -- **Version mismatch**: Exits with 1 and provides guidance -- **Git errors**: Script fails with appropriate error message - -## Related Skills - -- [utility-bump-beta](./utility-bump-beta.SKILL.md) - Increment beta version -- [build-check-go](../build-check-go.SKILL.md) - Verify Go build integrity - -## Notes - -- This check is **non-blocking** when no tags exist (allows initial development) -- Version format is flexible (supports semver, beta, alpha suffixes) -- Used in CI/CD to prevent merging PRs with version mismatches -- Part of the release automation workflow - ---- - -**Last Updated**: 2025-12-20 -**Maintained by**: Charon Project -**Source**: `scripts/check-version-match-tag.sh` diff --git a/.github/workflows/auto-changelog.yml b/.github/workflows/auto-changelog.yml deleted file mode 100644 index c57776d0a..000000000 --- a/.github/workflows/auto-changelog.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: Auto Changelog (Release Drafter) - -on: - workflow_run: - workflows: ["Docker Build, Publish & Test"] - types: [completed] - branches: [ main ] - release: - types: [published] - -concurrency: - group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.workflow_run.head_branch || github.head_ref || github.ref_name }} - cancel-in-progress: true - -permissions: - contents: write - -jobs: - update-draft: - runs-on: ubuntu-latest - if: ${{ github.event_name != 'workflow_run' || (github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_branch == 'main') }} - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - with: - ref: ${{ github.event.workflow_run.head_sha || github.sha }} - - name: Draft Release - uses: release-drafter/release-drafter@34d80673e067bdc0c24568d3af899c216adcfaa9 # v7 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/auto-versioning.yml b/.github/workflows/auto-versioning.yml deleted file mode 100644 index 5dc57e2e3..000000000 --- a/.github/workflows/auto-versioning.yml +++ /dev/null @@ -1,163 +0,0 @@ -name: Auto Versioning and Release - -# SEMANTIC VERSIONING RULES: -# - PATCH (0.14.1 โ†’ 0.14.2): fix:, perf:, refactor:, docs:, style:, test:, build:, ci: -# - MINOR (0.14.1 โ†’ 0.15.0): feat:, feat(...): -# - MAJOR (0.14.1 โ†’ 1.0.0): MANUAL ONLY - Create git tag manually when ready for 1.0.0 -# -# โš ๏ธ Major version bumps are intentionally disabled in automation to prevent accidents. - -on: - workflow_run: - workflows: ["Docker Build, Publish & Test"] - types: [completed] - branches: [ main ] - -concurrency: - group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch || github.ref }} - cancel-in-progress: false # Don't cancel in-progress releases - -permissions: - contents: write # Required for creating releases via API (removed unused pull-requests: write) - -jobs: - version: - runs-on: ubuntu-latest - if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_branch == 'main' }} - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - with: - fetch-depth: 0 - ref: ${{ github.event.workflow_run.head_sha || github.sha }} - - - name: Calculate Semantic Version - id: semver - uses: paulhatch/semantic-version@502226b17e3e242e4befd0a45690fcbb27dd1373 # v6.0.3 - with: - # The prefix to use to create tags - tag_prefix: "v" - # Regex pattern for major version bump - DISABLED (manual only) - # Use a pattern that will never match to prevent automated major bumps - major_pattern: "/__MANUAL_MAJOR_BUMP_ONLY__/" - # Regex pattern for minor version bump (new features) - # Matches "feat:" at line start, including "* feat:" bullet lines in squash-merge commit bodies - minor_pattern: "/^(\\* )?feat(\\(.+\\))?:/" - # Patch bumps: All other commits (fix:, chore:, etc.) are treated as patches by default - # Pattern to determine formatting - version_format: "${major}.${minor}.${patch}" - # If no tags are found, this version is used - version_from_branch: "0.0.0" - # This helps it search through history to find the last tag - search_commit_body: true - # Important: This enables the output 'changed' which your other steps rely on - enable_prerelease_mode: false - - - name: Show version - run: | - echo "Next version: ${{ steps.semver.outputs.version }}" - echo "Version changed: ${{ steps.semver.outputs.changed }}" - - - name: Determine tag name - id: determine_tag - run: | - # Normalize the version: remove any leading 'v' so we don't end up with 'vvX.Y.Z' - RAW="${{ steps.semver.outputs.version }}" - VERSION_NO_V="${RAW#v}" - TAG="v${VERSION_NO_V}" - echo "Determined tag: $TAG" - echo "tag=$TAG" >> "$GITHUB_OUTPUT" - - - name: Check for existing GitHub Release - id: check_release - run: | - TAG="${{ steps.determine_tag.outputs.tag }}" - echo "Checking for release for tag: ${TAG}" - STATUS=$(curl -s -o /dev/null -w "%{http_code}" \ - -H "Authorization: token ${GITHUB_TOKEN}" \ - -H "Accept: application/vnd.github+json" \ - "https://api.github.com/repos/${GITHUB_REPOSITORY}/releases/tags/${TAG}") || true - if [ "${STATUS}" = "200" ]; then - echo "exists=true" >> "$GITHUB_OUTPUT" - echo "โ„น๏ธ Release already exists for tag: ${TAG}" - else - echo "exists=false" >> "$GITHUB_OUTPUT" - echo "โœ… No existing release found for tag: ${TAG}" - fi - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Generate Release Changelog - id: changelog - if: ${{ steps.semver.outputs.changed == 'true' && steps.check_release.outputs.exists == 'false' }} - run: | - PREV_TAG=$(git describe --tags --abbrev=0 HEAD~1 2>/dev/null || echo "") - NEW_TAG="${{ steps.determine_tag.outputs.tag }}" - REPO="${{ github.repository }}" - - rm -f /tmp/feat.txt /tmp/fix.txt - touch /tmp/feat.txt /tmp/fix.txt - - # Extract feat/fix lines from commit messages, including squash-merge bullet bodies ("* feat: ...") - process_lines() { - while IFS= read -r line; do - clean="${line#\* }" - if echo "$clean" | grep -qE '^feat(\([^)]+\))?!?:'; then - echo "$clean" | sed -E 's/^feat(\([^)]+\))?!?: //' >> /tmp/feat.txt - elif echo "$clean" | grep -qE '^(fix|perf)(\([^)]+\))?!?:'; then - echo "$clean" | sed -E 's/^(fix|perf)(\([^)]+\))?!?: //' >> /tmp/fix.txt - fi - done - } - - if [ -n "$PREV_TAG" ]; then - process_lines < <(git log --format="%B" "${PREV_TAG}..HEAD") - else - process_lines < <(git log --format="%B" -50 HEAD) - fi - - { - echo "## What's Changed" - echo "" - if [ -s /tmp/feat.txt ]; then - echo "### ๐Ÿš€ Features" - sed 's/^/- /' /tmp/feat.txt - echo "" - fi - if [ -s /tmp/fix.txt ]; then - echo "### ๐Ÿ› Bug Fixes & Improvements" - sed 's/^/- /' /tmp/fix.txt - echo "" - fi - if [ ! -s /tmp/feat.txt ] && [ ! -s /tmp/fix.txt ]; then - echo "_Dependency updates and maintenance._" - echo "" - fi - echo "---" - echo "" - if [ -n "$PREV_TAG" ]; then - echo "**Full Changelog**: https://github.com/${REPO}/compare/${PREV_TAG}...${NEW_TAG}" - fi - } > /tmp/release_body.md - - echo "Generated changelog:" - cat /tmp/release_body.md - - - name: Create GitHub Release (creates tag via API) - if: ${{ steps.semver.outputs.changed == 'true' && steps.check_release.outputs.exists == 'false' }} - uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3 - with: - tag_name: ${{ steps.determine_tag.outputs.tag }} - name: Release ${{ steps.determine_tag.outputs.tag }} - body_path: /tmp/release_body.md - make_latest: true - draft: false - prerelease: false - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Output release information - if: ${{ steps.semver.outputs.changed == 'true' && steps.check_release.outputs.exists == 'false' }} - run: | - echo "โœ… Successfully created release: ${{ steps.determine_tag.outputs.tag }}" - echo "๐Ÿ“ฆ Release URL: https://github.com/${{ github.repository }}/releases/tag/${{ steps.determine_tag.outputs.tag }}" diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 5f605a4c1..631d34d1d 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -12,7 +12,7 @@ concurrency: cancel-in-progress: true env: - GO_VERSION: '1.26.5' + GO_VERSION: '1.26.6' GOTOOLCHAIN: local # Minimal permissions at workflow level; write permissions granted at job level for push only diff --git a/.github/workflows/codecov-upload.yml b/.github/workflows/codecov-upload.yml index 5e596211a..a868ee9a7 100644 --- a/.github/workflows/codecov-upload.yml +++ b/.github/workflows/codecov-upload.yml @@ -28,7 +28,7 @@ concurrency: cancel-in-progress: true env: - GO_VERSION: '1.26.5' + GO_VERSION: '1.26.6' NODE_VERSION: '24.19.0' GOTOOLCHAIN: local diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index a28524369..39132accf 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -15,7 +15,7 @@ concurrency: env: GOTOOLCHAIN: local - GO_VERSION: '1.26.5' + GO_VERSION: '1.26.6' permissions: contents: read @@ -52,7 +52,7 @@ jobs: run: bash scripts/ci/check-codeql-parity.sh - name: Initialize CodeQL - uses: github/codeql-action/init@5595ccaf912efad79be6eef63a5619ff05969be3 # v4 + uses: github/codeql-action/init@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4 with: languages: ${{ matrix.language }} queries: security-and-quality @@ -92,11 +92,11 @@ jobs: run: mkdir -p sarif-results - name: Autobuild - uses: github/codeql-action/autobuild@5595ccaf912efad79be6eef63a5619ff05969be3 # v4 + uses: github/codeql-action/autobuild@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4 - name: Perform CodeQL Analysis id: codeql_analyze - uses: github/codeql-action/analyze@5595ccaf912efad79be6eef63a5619ff05969be3 # v4 + uses: github/codeql-action/analyze@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4 with: category: "/language:${{ matrix.language }}" output: sarif-results/${{ matrix.language }} diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 76d111d5d..269447c99 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -35,6 +35,16 @@ on: # See GH #1235 / docs/plans/current_spec.md for the full rationale. branches: [main, development] workflow_dispatch: + # Reviewed as part of a default-branch/workflow_run reliability pass. This + # link is dormant in normal operation: docker-lint.yml's only trigger is + # workflow_dispatch, so nothing in the repo's day-to-day flow ever causes + # Docker Lint to run and chain into this workflow. Left unchanged + # deliberately -- this workflow's real, CI-critical triggers are the + # direct push/pull_request above, which never route through workflow_run + # and are therefore categorically unaffected by that bug class. Making + # Docker Lint auto-trigger would be a functional scope change (hadolint + # running on every push/PR), not a trigger-reliability fix, so it's out + # of scope here. workflow_run: workflows: ["Docker Lint"] types: [completed] @@ -912,7 +922,7 @@ jobs: format: 'table' severity: 'CRITICAL,HIGH' exit-code: '0' - version: 'v0.73.0' + version: 'v0.74.0' trivyignores: '.trivyignore' continue-on-error: true @@ -925,7 +935,7 @@ jobs: format: 'sarif' output: 'trivy-results.sarif' severity: 'CRITICAL,HIGH' - version: 'v0.73.0' + version: 'v0.74.0' trivyignores: '.trivyignore' continue-on-error: true @@ -941,7 +951,7 @@ jobs: - name: Upload Trivy results if: env.TRIGGER_EVENT != 'pull_request' && steps.trivy-check.outputs.exists == 'true' - uses: github/codeql-action/upload-sarif@f205ea1c3313d32999d8d6a48b4f6530d4437b38 # v4.37.4 + uses: github/codeql-action/upload-sarif@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7 with: sarif_file: 'trivy-results.sarif' category: ${{ env.TRIVY_SARIF_CATEGORY }} @@ -964,14 +974,60 @@ jobs: # Generate SBOM (Software Bill of Materials) for supply chain security # Only for production builds (main/development) - feature branches use downstream supply-chain-pr.yml + # anchore/sbom-action resolves the pinned syft release tag against the GitHub + # API at job runtime; transient upstream 5xx/rate-limit errors there fail the + # step even though the pin itself is valid, so continue-on-error + a + # deterministic pinned-Syft fallback (below) makes this resilient. - name: Generate SBOM + id: sbom_primary + continue-on-error: true uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0 if: env.TRIGGER_EVENT != 'pull_request' && needs.setup.outputs.is_feature_push != 'true' with: image: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.merge.outputs.digest }} format: cyclonedx-json output-file: sbom.cyclonedx.json - syft-version: v1.45.1 + syft-version: v1.51.0 + + - name: Generate SBOM fallback with pinned Syft + if: env.TRIGGER_EVENT != 'pull_request' && needs.setup.outputs.is_feature_push != 'true' && always() + run: | + set -euo pipefail + + if [[ "${{ steps.sbom_primary.outcome }}" == "success" ]] && [[ -s sbom.cyclonedx.json ]] && jq -e . sbom.cyclonedx.json >/dev/null 2>&1; then + echo "Primary SBOM generation succeeded with valid JSON; skipping fallback" + exit 0 + fi + + echo "Primary SBOM generation failed or produced missing/invalid output; using deterministic Syft fallback" + + SYFT_VERSION="v1.51.0" + OS="$(uname -s | tr '[:upper:]' '[:lower:]')" + ARCH="$(uname -m)" + case "$ARCH" in + x86_64) ARCH="amd64" ;; + aarch64|arm64) ARCH="arm64" ;; + *) echo "Unsupported architecture: $ARCH"; exit 1 ;; + esac + + TARBALL="syft_${SYFT_VERSION#v}_${OS}_${ARCH}.tar.gz" + BASE_URL="https://github.com/anchore/syft/releases/download/${SYFT_VERSION}" + + curl -fsSLo "$TARBALL" "${BASE_URL}/${TARBALL}" + curl -fsSLo checksums.txt "${BASE_URL}/syft_${SYFT_VERSION#v}_checksums.txt" + + grep " ${TARBALL}$" checksums.txt > checksum_line.txt + sha256sum -c checksum_line.txt + + tar -xzf "$TARBALL" syft + chmod +x syft + + DIGEST="${{ steps.merge.outputs.digest }}" + if [[ -z "$DIGEST" ]]; then + echo "::error::Digest from merge step is empty; the merge step did not complete successfully" + exit 1 + fi + ./syft "${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@${DIGEST}" -o cyclonedx-json=sbom.cyclonedx.json # Create verifiable attestation for the SBOM # (actions/attest-sbom is deprecated; actions/attest supports sbom-path natively) @@ -979,7 +1035,7 @@ jobs: # ("CI: no tokens available"), so retry once before failing the build - name: Attest SBOM id: attest-sbom - uses: actions/attest@508db95dd578ae2727ebd6217d5ba78e4fbda05d # v4.2.1 + uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 if: env.TRIGGER_EVENT != 'pull_request' && needs.setup.outputs.is_feature_push != 'true' continue-on-error: true with: @@ -1152,7 +1208,7 @@ jobs: trivyignores: '.trivyignore' severity: 'CRITICAL,HIGH' exit-code: '0' - version: 'v0.73.0' + version: 'v0.74.0' - name: Run Trivy scan on PR image (SARIF - blocking) id: trivy-scan @@ -1167,7 +1223,7 @@ jobs: # Keep scanning strict for CRITICAL/HIGH; fail is enforced explicitly # at the end so SARIF upload and summaries still run. exit-code: '1' - version: 'v0.73.0' + version: 'v0.74.0' continue-on-error: true - name: Check Trivy PR SARIF exists @@ -1182,7 +1238,7 @@ jobs: - name: Upload Trivy scan results if: always() && steps.trivy-pr-check.outputs.exists == 'true' - uses: github/codeql-action/upload-sarif@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6 + uses: github/codeql-action/upload-sarif@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7 with: sarif_file: 'trivy-pr-results.sarif' category: ${{ env.TRIVY_SARIF_CATEGORY }} @@ -1285,15 +1341,15 @@ jobs: $result.ruleId // ($result.rule // {} | .id) // ( - if ($result.ruleIndex != null and (($run.tool.driver.rules? // null) | type) == \"array\") then - ($run.tool.driver.rules[$result.ruleIndex].id // \"unknown\") + if ($result.ruleIndex != null and (($run.tool.driver.rules? // null) | type) == "array") then + ($run.tool.driver.rules[$result.ruleIndex].id // "unknown") else - \"unknown\" + "unknown" end ) )) | package: \(( - ($result.message.text // \"\") - | (try capture(\"(?i)(?:Package|PkgName|Pkg|Library)\\\\s*[:=]\\\\s*`?(?[A-Za-z0-9._+:+-]+)`?\").pkg catch \"n/a\") + ($result.message.text // "") + | (try capture("(?i)(?:Package|PkgName|Pkg|Library)\\s*[:=]\\s*`?(?[A-Za-z0-9._+:/-]+)`?").pkg catch "n/a") ))" ' "${SARIF_PATH}"; then echo "- unable to render parsed findings" diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index f4110bc37..295c8f233 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -4,6 +4,7 @@ on: workflow_run: workflows: ["Docker Build, Publish & Test"] types: [completed] + branches: [main] workflow_dispatch: # Allow manual trigger # Sets permissions to allow deployment to GitHub Pages diff --git a/.github/workflows/e2e-tests-split.yml b/.github/workflows/e2e-tests-split.yml index 7e7acf6dc..5f42fe116 100644 --- a/.github/workflows/e2e-tests-split.yml +++ b/.github/workflows/e2e-tests-split.yml @@ -83,7 +83,7 @@ on: env: NODE_VERSION: '24.19.0' - GO_VERSION: '1.26.5' + GO_VERSION: '1.26.6' GOTOOLCHAIN: local DOCKERHUB_REGISTRY: docker.io IMAGE_NAME: ${{ github.repository_owner }}/charon @@ -325,11 +325,18 @@ jobs: - name: Install Playwright Chromium run: | + set -uo pipefail echo "๐Ÿ“ฆ Installing Chromium..." - npx playwright install --with-deps chromium - EXIT_CODE=$? - echo "โœ… Install command completed (exit code: $EXIT_CODE)" - exit "$EXIT_CODE" + for attempt in 1 2 3; do + timeout 10m npx playwright install --with-deps chromium && break + if [ "$attempt" -lt 3 ]; then + echo "Attempt ${attempt}/3 failed; retrying in 15s..." >&2 + sleep 15 + else + echo "ERROR: Playwright chromium install failed after 3 attempts" >&2 + exit 1 + fi + done - name: Run Chromium Security Enforcement Tests run: | @@ -560,19 +567,33 @@ jobs: - name: Install Playwright Chromium (required by security-tests dependency) run: | + set -uo pipefail echo "๐Ÿ“ฆ Installing Chromium (required by security-tests dependency)..." - npx playwright install --with-deps chromium - EXIT_CODE=$? - echo "โœ… Install command completed (exit code: $EXIT_CODE)" - exit "$EXIT_CODE" + for attempt in 1 2 3; do + timeout 10m npx playwright install --with-deps chromium && break + if [ "$attempt" -lt 3 ]; then + echo "Attempt ${attempt}/3 failed; retrying in 15s..." >&2 + sleep 15 + else + echo "ERROR: Playwright chromium install failed after 3 attempts" >&2 + exit 1 + fi + done - name: Install Playwright Firefox run: | + set -uo pipefail echo "๐Ÿ“ฆ Installing Firefox..." - npx playwright install --with-deps firefox - EXIT_CODE=$? - echo "โœ… Install command completed (exit code: $EXIT_CODE)" - exit "$EXIT_CODE" + for attempt in 1 2 3; do + timeout 10m npx playwright install --with-deps firefox && break + if [ "$attempt" -lt 3 ]; then + echo "Attempt ${attempt}/3 failed; retrying in 15s..." >&2 + sleep 15 + else + echo "ERROR: Playwright firefox install failed after 3 attempts" >&2 + exit 1 + fi + done - name: Run Firefox Security Enforcement Tests run: | @@ -806,19 +827,33 @@ jobs: - name: Install Playwright Chromium (required by security-tests dependency) run: | + set -uo pipefail echo "๐Ÿ“ฆ Installing Chromium (required by security-tests dependency)..." - npx playwright install --with-deps chromium - EXIT_CODE=$? - echo "โœ… Install command completed (exit code: $EXIT_CODE)" - exit "$EXIT_CODE" + for attempt in 1 2 3; do + timeout 10m npx playwright install --with-deps chromium && break + if [ "$attempt" -lt 3 ]; then + echo "Attempt ${attempt}/3 failed; retrying in 15s..." >&2 + sleep 15 + else + echo "ERROR: Playwright chromium install failed after 3 attempts" >&2 + exit 1 + fi + done - name: Install Playwright WebKit run: | + set -uo pipefail echo "๐Ÿ“ฆ Installing WebKit..." - npx playwright install --with-deps webkit - EXIT_CODE=$? - echo "โœ… Install command completed (exit code: $EXIT_CODE)" - exit "$EXIT_CODE" + for attempt in 1 2 3; do + timeout 10m npx playwright install --with-deps webkit && break + if [ "$attempt" -lt 3 ]; then + echo "Attempt ${attempt}/3 failed; retrying in 15s..." >&2 + sleep 15 + else + echo "ERROR: Playwright webkit install failed after 3 attempts" >&2 + exit 1 + fi + done - name: Run WebKit Security Enforcement Tests run: | @@ -1079,11 +1114,18 @@ jobs: - name: Install Playwright Chromium run: | + set -uo pipefail echo "๐Ÿ“ฆ Installing Chromium..." - npx playwright install --with-deps chromium - EXIT_CODE=$? - echo "โœ… Install command completed (exit code: $EXIT_CODE)" - exit "$EXIT_CODE" + for attempt in 1 2 3; do + timeout 10m npx playwright install --with-deps chromium && break + if [ "$attempt" -lt 3 ]; then + echo "Attempt ${attempt}/3 failed; retrying in 15s..." >&2 + sleep 15 + else + echo "ERROR: Playwright chromium install failed after 3 attempts" >&2 + exit 1 + fi + done - name: Run Chromium Non-Security Tests (Shard ${{ matrix.shard }}/${{ matrix.total-shards }}) run: | @@ -1317,19 +1359,33 @@ jobs: - name: Install Playwright Chromium (required by security-tests dependency) run: | + set -uo pipefail echo "๐Ÿ“ฆ Installing Chromium (required by security-tests dependency)..." - npx playwright install --with-deps chromium - EXIT_CODE=$? - echo "โœ… Install command completed (exit code: $EXIT_CODE)" - exit "$EXIT_CODE" + for attempt in 1 2 3; do + timeout 10m npx playwright install --with-deps chromium && break + if [ "$attempt" -lt 3 ]; then + echo "Attempt ${attempt}/3 failed; retrying in 15s..." >&2 + sleep 15 + else + echo "ERROR: Playwright chromium install failed after 3 attempts" >&2 + exit 1 + fi + done - name: Install Playwright Firefox run: | + set -uo pipefail echo "๐Ÿ“ฆ Installing Firefox..." - npx playwright install --with-deps firefox - EXIT_CODE=$? - echo "โœ… Install command completed (exit code: $EXIT_CODE)" - exit "$EXIT_CODE" + for attempt in 1 2 3; do + timeout 10m npx playwright install --with-deps firefox && break + if [ "$attempt" -lt 3 ]; then + echo "Attempt ${attempt}/3 failed; retrying in 15s..." >&2 + sleep 15 + else + echo "ERROR: Playwright firefox install failed after 3 attempts" >&2 + exit 1 + fi + done - name: Run Firefox Non-Security Tests (Shard ${{ matrix.shard }}/${{ matrix.total-shards }}) run: | @@ -1563,19 +1619,33 @@ jobs: - name: Install Playwright Chromium (required by security-tests dependency) run: | + set -uo pipefail echo "๐Ÿ“ฆ Installing Chromium (required by security-tests dependency)..." - npx playwright install --with-deps chromium - EXIT_CODE=$? - echo "โœ… Install command completed (exit code: $EXIT_CODE)" - exit "$EXIT_CODE" + for attempt in 1 2 3; do + timeout 10m npx playwright install --with-deps chromium && break + if [ "$attempt" -lt 3 ]; then + echo "Attempt ${attempt}/3 failed; retrying in 15s..." >&2 + sleep 15 + else + echo "ERROR: Playwright chromium install failed after 3 attempts" >&2 + exit 1 + fi + done - name: Install Playwright WebKit run: | + set -uo pipefail echo "๐Ÿ“ฆ Installing WebKit..." - npx playwright install --with-deps webkit - EXIT_CODE=$? - echo "โœ… Install command completed (exit code: $EXIT_CODE)" - exit "$EXIT_CODE" + for attempt in 1 2 3; do + timeout 10m npx playwright install --with-deps webkit && break + if [ "$attempt" -lt 3 ]; then + echo "Attempt ${attempt}/3 failed; retrying in 15s..." >&2 + sleep 15 + else + echo "ERROR: Playwright webkit install failed after 3 attempts" >&2 + exit 1 + fi + done - name: Run WebKit Non-Security Tests (Shard ${{ matrix.shard }}/${{ matrix.total-shards }}) run: | diff --git a/.github/workflows/history-rewrite-tests.yml b/.github/workflows/history-rewrite-tests.yml index 0d7e5ca84..7be5dbde3 100644 --- a/.github/workflows/history-rewrite-tests.yml +++ b/.github/workflows/history-rewrite-tests.yml @@ -4,6 +4,16 @@ on: workflow_run: workflows: ["Docker Build, Publish & Test"] types: [completed] + pull_request: + paths: + - 'scripts/history-rewrite/**' + - '.github/workflows/history-rewrite-tests.yml' + push: + branches: [main, development] + paths: + - 'scripts/history-rewrite/**' + - '.github/workflows/history-rewrite-tests.yml' + workflow_dispatch: {} concurrency: group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.workflow_run.head_branch || github.head_ref || github.ref_name }} @@ -15,7 +25,7 @@ permissions: jobs: test: runs-on: ubuntu-latest - if: ${{ github.event.workflow_run.conclusion == 'success' }} + if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }} steps: - name: Checkout with full history uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 diff --git a/.github/workflows/nightly-build.yml b/.github/workflows/nightly-build.yml index 72fd05254..790917ff4 100644 --- a/.github/workflows/nightly-build.yml +++ b/.github/workflows/nightly-build.yml @@ -15,7 +15,7 @@ on: default: "false" env: - GO_VERSION: '1.26.5' + GO_VERSION: '1.26.6' NODE_VERSION: '24.19.0' GOTOOLCHAIN: local GHCR_REGISTRY: ghcr.io @@ -285,9 +285,9 @@ jobs: uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0 with: image: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.resolve_digest.outputs.digest }} - format: cyclonedx-json + format: spdx-json output-file: sbom-nightly.json - syft-version: v1.45.1 + syft-version: v1.51.0 - name: Generate SBOM fallback with pinned Syft if: always() @@ -301,7 +301,7 @@ jobs: echo "Primary SBOM generation failed or produced missing/invalid output; using deterministic Syft fallback" - SYFT_VERSION="v1.50.0" + SYFT_VERSION="v1.51.0" OS="$(uname -s | tr '[:upper:]' '[:lower:]')" ARCH="$(uname -m)" case "$ARCH" in @@ -327,7 +327,7 @@ jobs: echo "::error::Digest from resolve_digest step is empty; the digest-resolution step did not complete successfully" exit 1 fi - ./syft "${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@${DIGEST}" -o cyclonedx-json=sbom-nightly.json + ./syft "${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@${DIGEST}" -o spdx-json=sbom-nightly.json - name: Verify SBOM artifact if: always() @@ -336,11 +336,9 @@ jobs: test -s sbom-nightly.json jq -e . sbom-nightly.json >/dev/null jq -e ' - .bomFormat == "CycloneDX" - and (.specVersion | type == "string" and length > 0) - and has("version") - and has("metadata") - and (.components | type == "array") + (.spdxVersion | type == "string" and length > 0) + and has("SPDXID") + and (.packages | type == "array") ' sbom-nightly.json >/dev/null - name: Upload SBOM artifact @@ -614,11 +612,11 @@ jobs: image-ref: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:nightly@${{ needs.build-and-push-nightly.outputs.digest }} format: 'sarif' output: 'trivy-nightly.sarif' - version: 'v0.73.0' + version: 'v0.74.0' trivyignores: '.trivyignore' - name: Upload Trivy results - uses: github/codeql-action/upload-sarif@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6 + uses: github/codeql-action/upload-sarif@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7 with: sarif_file: 'trivy-nightly.sarif' category: 'trivy-nightly' diff --git a/.github/workflows/orthrus-build.yml b/.github/workflows/orthrus-build.yml index 48aa2bcb7..23f0ada33 100644 --- a/.github/workflows/orthrus-build.yml +++ b/.github/workflows/orthrus-build.yml @@ -29,7 +29,7 @@ env: GHCR_REGISTRY: ghcr.io DOCKERHUB_REGISTRY: docker.io IMAGE_NAME: wikid82/orthrus - GO_VERSION: '1.26.5' + GO_VERSION: '1.26.6' permissions: contents: read diff --git a/.github/workflows/propagate-changes.yml b/.github/workflows/propagate-changes.yml index 645fd9af0..c7cd1206e 100644 --- a/.github/workflows/propagate-changes.yml +++ b/.github/workflows/propagate-changes.yml @@ -5,6 +5,20 @@ on: workflows: ["Docker Build, Publish & Test"] types: [completed] branches: [ main, development ] + # NOTE for future contributors: do not add a direct `push:` trigger here as + # a workflow_run-reliability fallback without ALSO updating the job-level + # `if:` below. The CURRENT_BRANCH/CURRENT_SHA env fallbacks (`|| github.ref_name` + # / `|| github.sha`) already resolve correctly on a `push` event, but the + # `if:` only recognizes `workflow_dispatch` or a matching `workflow_run` โ€” + # it has no `github.event_name == 'push'` clause, so a bare `push:` addition + # would silently no-op the job on every push (worse than today: it would + # show as a registered trigger in the Actions UI while never actually + # running). workflow_run is also semantically correct here regardless: this + # job should only propagate a commit Docker Build has already validated as + # successful, which a raw `push` event can't guarantee. The existing + # `workflow_dispatch: {}` below (added in 5245858a) is the intended manual + # fallback if workflow_run ever silently fails to fire again. + workflow_dispatch: {} concurrency: group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch || github.ref }} @@ -24,8 +38,9 @@ jobs: runs-on: ubuntu-latest if: >- github.actor != 'github-actions[bot]' && - github.event.workflow_run.conclusion == 'success' && - (github.event.workflow_run.head_branch == 'main' || github.event.workflow_run.head_branch == 'development') + (github.event_name == 'workflow_dispatch' || + (github.event.workflow_run.conclusion == 'success' && + (github.event.workflow_run.head_branch == 'main' || github.event.workflow_run.head_branch == 'development'))) steps: - name: Set up Node (for github-script) uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 diff --git a/.github/workflows/quality-checks.yml b/.github/workflows/quality-checks.yml index 6a679062a..f7948c6fb 100644 --- a/.github/workflows/quality-checks.yml +++ b/.github/workflows/quality-checks.yml @@ -16,7 +16,7 @@ permissions: checks: write env: - GO_VERSION: '1.26.5' + GO_VERSION: '1.26.6' NODE_VERSION: '24.19.0' GOTOOLCHAIN: local diff --git a/.github/workflows/release-goreleaser.yml b/.github/workflows/release-goreleaser.yml deleted file mode 100644 index 151492f9e..000000000 --- a/.github/workflows/release-goreleaser.yml +++ /dev/null @@ -1,87 +0,0 @@ -name: Release (GoReleaser) - -on: - push: - tags: - - 'v*' - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: false - -env: - GO_VERSION: '1.26.5' - NODE_VERSION: '24.19.0' - GOTOOLCHAIN: local - -permissions: - contents: write - packages: write - -jobs: - goreleaser: - if: ${{ !contains(github.ref_name, '-candidate') && !contains(github.ref_name, '-rc') }} - runs-on: ubuntu-latest - env: - # Use the built-in GITHUB_TOKEN by default for GitHub API operations. - # If you need to provide a PAT with elevated permissions, add a GITHUB_TOKEN secret - # at the repo or organization level and update the env here accordingly. - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - with: - fetch-depth: 0 - - - name: Enforce PR-2 release promotion guard - env: - REPO_VARS_JSON: ${{ toJSON(vars) }} - run: | - PR2_GATE_STATUS="$(printf '%s' "$REPO_VARS_JSON" | jq -r '.CHARON_PR2_GATES_PASSED // "false"')" - if [[ "$PR2_GATE_STATUS" != "true" ]]; then - echo "::error::Releasable tag promotion is blocked until PR-2 security/retirement gates pass." - echo "::error::Set repository variable CHARON_PR2_GATES_PASSED=true only after PR-2 approval." - exit 1 - fi - - - name: Set up Go - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7 - with: - go-version-file: backend/go.mod - - cache-dependency-path: backend/go.sum - - - name: Set up Node.js - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 - with: - node-version: ${{ env.NODE_VERSION }} - - - name: Build Frontend - working-directory: frontend - run: | - # Inject version into frontend build from tag (if present) - VERSION=${GITHUB_REF#refs/tags/} - echo "VITE_APP_VERSION=${VERSION}" >> "$GITHUB_ENV" - npm ci --ignore-scripts - npm run build - - - name: Install Cross-Compilation Tools (Zig) - # Security: Pinned to full SHA for supply chain security - uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2.2.1 - with: - version: 0.13.0 - - # GITHUB_TOKEN is set from GITHUB_TOKEN or CHARON_TOKEN (fallback), defaulting to GITHUB_TOKEN - - - name: Generate Changelog Data - run: bash scripts/generate-changelog.sh - - - name: Run GoReleaser - uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7 - with: - distribution: goreleaser - version: '~> v2.5' - args: release --clean - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # CGO settings are handled in .goreleaser.yaml via Zig diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml new file mode 100644 index 000000000..3524195a7 --- /dev/null +++ b/.github/workflows/release-please.yml @@ -0,0 +1,19 @@ +name: release-please + +on: + push: + branches: [main] + +permissions: + contents: write + pull-requests: write + +jobs: + release-please: + runs-on: ubuntu-latest + steps: + - uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5 + with: + config-file: release-please-config.json + manifest-file: .release-please-manifest.json + target-branch: main diff --git a/.github/workflows/renovate.yml b/.github/workflows/renovate.yml index bc1281087..6d63abc06 100644 --- a/.github/workflows/renovate.yml +++ b/.github/workflows/renovate.yml @@ -16,7 +16,7 @@ permissions: issues: write env: - GO_VERSION: '1.26.5' + GO_VERSION: '1.26.6' jobs: renovate: diff --git a/.github/workflows/security-pr.yml b/.github/workflows/security-pr.yml index 17b4dd2c6..6eaa6c5fc 100644 --- a/.github/workflows/security-pr.yml +++ b/.github/workflows/security-pr.yml @@ -373,7 +373,7 @@ jobs: format: 'sarif' output: 'trivy-binary-results.sarif' severity: 'CRITICAL,HIGH,MEDIUM' - version: 'v0.73.0' + version: 'v0.74.0' trivyignores: '.trivyignore' config: 'trivy.yaml' continue-on-error: true @@ -392,7 +392,7 @@ jobs: - name: Upload Trivy SARIF to GitHub Security if: always() && steps.trivy-sarif-check.outputs.exists == 'true' # github/codeql-action v4.36.2 - uses: github/codeql-action/upload-sarif@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6 + uses: github/codeql-action/upload-sarif@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7 with: sarif_file: 'trivy-binary-results.sarif' category: ${{ steps.pr-info.outputs.is_push == 'true' && format('security-scan-{0}', github.event_name == 'workflow_run' && github.event.workflow_run.head_branch || github.ref_name) || format('security-scan-pr-{0}', steps.pr-info.outputs.pr_number) }} @@ -408,7 +408,7 @@ jobs: format: 'table' severity: 'CRITICAL,HIGH' exit-code: '1' - version: 'v0.73.0' + version: 'v0.74.0' trivyignores: '.trivyignore' config: 'trivy.yaml' diff --git a/.github/workflows/security-weekly-rebuild.yml b/.github/workflows/security-weekly-rebuild.yml index e582efc48..84aa89b1a 100644 --- a/.github/workflows/security-weekly-rebuild.yml +++ b/.github/workflows/security-weekly-rebuild.yml @@ -105,7 +105,7 @@ jobs: format: 'table' severity: 'CRITICAL,HIGH' exit-code: '1' # Fail workflow if vulnerabilities found - version: 'v0.73.0' + version: 'v0.74.0' continue-on-error: true - name: Run Trivy vulnerability scanner (SARIF) @@ -116,12 +116,12 @@ jobs: format: 'sarif' output: 'trivy-weekly-results.sarif' severity: 'CRITICAL,HIGH,MEDIUM' - version: 'v0.73.0' + version: 'v0.74.0' trivyignores: '.trivyignore' - name: Upload Trivy results to GitHub Security id: upload-trivy-weekly - uses: github/codeql-action/upload-sarif@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6 + uses: github/codeql-action/upload-sarif@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7 with: sarif_file: 'trivy-weekly-results.sarif' category: ${{ env.TRIVY_SARIF_CATEGORY }} @@ -156,7 +156,7 @@ jobs: format: 'json' output: 'trivy-weekly-results.json' severity: 'CRITICAL,HIGH,MEDIUM,LOW' - version: 'v0.73.0' + version: 'v0.74.0' - name: Upload Trivy JSON results uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml new file mode 100644 index 000000000..b64da6669 --- /dev/null +++ b/.github/workflows/semgrep.yml @@ -0,0 +1,101 @@ +name: Semgrep - SAST Scan + +on: + pull_request: + branches: [main, nightly, development] + push: + branches: [main, nightly, development] + workflow_dispatch: + schedule: + - cron: '0 4 * * 1' # Mondays 04:00 UTC โ€” offset 1h after CodeQL's 03:00 to avoid runner contention + +concurrency: + group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.head_ref || github.ref_name }} + cancel-in-progress: true + +permissions: + contents: read + security-events: write + actions: read + pull-requests: read + +jobs: + semgrep-scan: + name: Semgrep SAST Scan + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read + security-events: write + actions: read + pull-requests: read + container: + # renovate: datasource=docker depName=semgrep/semgrep + image: semgrep/semgrep:1.173.0@sha256:67319956da3dcb58baf5b322899c15458e3963e7018a86aeeb5cd224e69cb77a # semgrep/semgrep 1.173.0 + steps: + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + ref: ${{ github.ref }} + + - name: Configure git safe.directory + run: git config --global --add safe.directory "$GITHUB_WORKSPACE" + + - name: Verify Semgrep parity guard + run: bash scripts/ci/check-semgrep-parity.sh + + - name: Verify Semgrep version + run: semgrep --version + + - name: Run Semgrep (SARIF output) + id: semgrep_sarif + continue-on-error: true + env: + SEMGREP_SARIF_OUTPUT: semgrep-results.sarif + run: bash scripts/pre-commit-hooks/semgrep-scan.sh + + - name: Check Semgrep SARIF output exists + id: semgrep_sarif_check + if: always() + run: | + if [ -f semgrep-results.sarif ]; then + echo "exists=true" >> "$GITHUB_OUTPUT" + else + echo "exists=false" >> "$GITHUB_OUTPUT" + echo "No Semgrep SARIF output found; skipping SARIF upload" + fi + + - name: Upload Semgrep SARIF to GitHub Security + if: always() && steps.semgrep_sarif_check.outputs.exists == 'true' + uses: github/codeql-action/upload-sarif@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7 + with: + sarif_file: semgrep-results.sarif + category: semgrep + continue-on-error: true + + - name: Run Semgrep (hard-fail gate) + run: bash scripts/pre-commit-hooks/semgrep-scan.sh + + - name: Upload SARIF artifact + if: always() && steps.semgrep_sarif_check.outputs.exists == 'true' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: semgrep-sarif-${{ github.run_id }} + path: semgrep-results.sarif + retention-days: 14 + continue-on-error: true + + - name: Create job summary + if: always() + run: | + { + echo "## Semgrep SAST Scan Results" + echo "" + echo "**Rulesets**: p/golang, p/javascript, p/typescript, p/react, p/secrets, p/dockerfile" + echo "**Severity Gate**: ERROR, WARNING (--error)" + if [ "${{ job.status }}" == "success" ]; then + echo "PASSED: no blocking Semgrep findings" + else + echo "FAILED: Semgrep reported blocking findings โ€” see step logs and the Security tab" + fi + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/supply-chain-pr.yml b/.github/workflows/supply-chain-pr.yml index 8e0359b7f..c06d195e9 100644 --- a/.github/workflows/supply-chain-pr.yml +++ b/.github/workflows/supply-chain-pr.yml @@ -266,15 +266,60 @@ jobs: fi # Generate SBOM using official Anchore action (auto-updated by Renovate) + # anchore/sbom-action resolves the pinned syft release tag against the GitHub + # API at job runtime; transient upstream 5xx/rate-limit errors there fail the + # step even though the pin itself is valid, so continue-on-error + a + # deterministic pinned-Syft fallback (below) makes this resilient. - name: Generate SBOM if: steps.set-target.outputs.image_name != '' + id: sbom_primary + continue-on-error: true uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0 - id: sbom with: image: ${{ steps.set-target.outputs.image_name }} format: cyclonedx-json output-file: sbom.cyclonedx.json - syft-version: v1.45.1 + syft-version: v1.51.0 + + - name: Generate SBOM fallback with pinned Syft + if: steps.set-target.outputs.image_name != '' && always() + run: | + set -euo pipefail + + if [[ "${{ steps.sbom_primary.outcome }}" == "success" ]] && [[ -s sbom.cyclonedx.json ]] && jq -e . sbom.cyclonedx.json >/dev/null 2>&1; then + echo "Primary SBOM generation succeeded with valid JSON; skipping fallback" + exit 0 + fi + + echo "Primary SBOM generation failed or produced missing/invalid output; using deterministic Syft fallback" + + SYFT_VERSION="v1.51.0" + OS="$(uname -s | tr '[:upper:]' '[:lower:]')" + ARCH="$(uname -m)" + case "$ARCH" in + x86_64) ARCH="amd64" ;; + aarch64|arm64) ARCH="arm64" ;; + *) echo "Unsupported architecture: $ARCH"; exit 1 ;; + esac + + TARBALL="syft_${SYFT_VERSION#v}_${OS}_${ARCH}.tar.gz" + BASE_URL="https://github.com/anchore/syft/releases/download/${SYFT_VERSION}" + + curl -fsSLo "$TARBALL" "${BASE_URL}/${TARBALL}" + curl -fsSLo checksums.txt "${BASE_URL}/syft_${SYFT_VERSION#v}_checksums.txt" + + grep " ${TARBALL}$" checksums.txt > checksum_line.txt + sha256sum -c checksum_line.txt + + tar -xzf "$TARBALL" syft + chmod +x syft + + IMAGE_NAME="${{ steps.set-target.outputs.image_name }}" + if [[ -z "$IMAGE_NAME" ]]; then + echo "::error::image_name from set-target step is empty; the target-selection step did not complete successfully" + exit 1 + fi + ./syft "${IMAGE_NAME}" -o cyclonedx-json=sbom.cyclonedx.json - name: Count SBOM components if: steps.set-target.outputs.image_name != '' @@ -288,7 +333,18 @@ jobs: - name: Install Grype if: steps.set-target.outputs.image_name != '' run: | - curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin v0.116.1 + MAX_ATTEMPTS=3 + for attempt in $(seq 1 "$MAX_ATTEMPTS"); do + if curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin v0.117.0; then + break + fi + if [[ "$attempt" -eq "$MAX_ATTEMPTS" ]]; then + echo "::error::Failed to install Grype after ${MAX_ATTEMPTS} attempts" + exit 1 + fi + echo "Grype install attempt ${attempt} failed; retrying after backoff..." + sleep $((attempt * 5)) + done - name: Scan for vulnerabilities if: steps.set-target.outputs.image_name != '' @@ -365,7 +421,7 @@ jobs: - name: Upload SARIF to GitHub Security if: steps.set-target.outputs.image_name != '' - uses: github/codeql-action/upload-sarif@5595ccaf912efad79be6eef63a5619ff05969be3 # v4 + uses: github/codeql-action/upload-sarif@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4 continue-on-error: true with: sarif_file: grype-results.sarif diff --git a/.github/workflows/supply-chain-verify.yml b/.github/workflows/supply-chain-verify.yml index a5bb1eee3..8fb1e6978 100644 --- a/.github/workflows/supply-chain-verify.yml +++ b/.github/workflows/supply-chain-verify.yml @@ -118,14 +118,55 @@ jobs: fi # Generate SBOM using official Anchore action (auto-updated by Renovate) + # anchore/sbom-action resolves the pinned syft release tag against the GitHub + # API at job runtime; transient upstream 5xx/rate-limit errors there fail the + # step even though the pin itself is valid, so continue-on-error + a + # deterministic pinned-Syft fallback (below) makes this resilient. - name: Generate and Verify SBOM if: steps.image-check.outputs.exists == 'true' + id: sbom_primary + continue-on-error: true uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0 with: image: ghcr.io/${{ github.repository_owner }}/charon:${{ steps.tag.outputs.tag }} format: cyclonedx-json output-file: sbom-verify.cyclonedx.json - syft-version: v1.45.1 + syft-version: v1.51.0 + + - name: Generate SBOM fallback with pinned Syft + if: steps.image-check.outputs.exists == 'true' && always() + run: | + set -euo pipefail + + if [[ "${{ steps.sbom_primary.outcome }}" == "success" ]] && [[ -s sbom-verify.cyclonedx.json ]] && jq -e . sbom-verify.cyclonedx.json >/dev/null 2>&1; then + echo "Primary SBOM generation succeeded with valid JSON; skipping fallback" + exit 0 + fi + + echo "Primary SBOM generation failed or produced missing/invalid output; using deterministic Syft fallback" + + SYFT_VERSION="v1.51.0" + OS="$(uname -s | tr '[:upper:]' '[:lower:]')" + ARCH="$(uname -m)" + case "$ARCH" in + x86_64) ARCH="amd64" ;; + aarch64|arm64) ARCH="arm64" ;; + *) echo "Unsupported architecture: $ARCH"; exit 1 ;; + esac + + TARBALL="syft_${SYFT_VERSION#v}_${OS}_${ARCH}.tar.gz" + BASE_URL="https://github.com/anchore/syft/releases/download/${SYFT_VERSION}" + + curl -fsSLo "$TARBALL" "${BASE_URL}/${TARBALL}" + curl -fsSLo checksums.txt "${BASE_URL}/syft_${SYFT_VERSION#v}_checksums.txt" + + grep " ${TARBALL}$" checksums.txt > checksum_line.txt + sha256sum -c checksum_line.txt + + tar -xzf "$TARBALL" syft + chmod +x syft + + ./syft "ghcr.io/${{ github.repository_owner }}/charon:${{ steps.tag.outputs.tag }}" -o cyclonedx-json=sbom-verify.cyclonedx.json - name: Verify SBOM Completeness if: steps.image-check.outputs.exists == 'true' diff --git a/.gitignore b/.gitignore index 77fe2c3a4..58ec00b71 100644 --- a/.gitignore +++ b/.gitignore @@ -161,11 +161,6 @@ backend/data/caddy/ # ----------------------------------------------------------------------------- docker-compose.override.yml -# ----------------------------------------------------------------------------- -# GoReleaser -# ----------------------------------------------------------------------------- -dist/ - # ----------------------------------------------------------------------------- # Testing & Coverage # ----------------------------------------------------------------------------- diff --git a/.goreleaser.yaml b/.goreleaser.yaml deleted file mode 100644 index 44a0cea34..000000000 --- a/.goreleaser.yaml +++ /dev/null @@ -1,81 +0,0 @@ -version: 2 - -# NOTE: Charon uses a Docker-only deployment model. -# This GoReleaser configuration is used exclusively for changelog generation. -# The builds, archives, and nfpms sections below are kept for potential -# future use but are not currently utilized in the release workflow. -# All distribution happens via Docker images: -# - Docker Hub: docker pull wikid82/charon:latest -# - GHCR: docker pull ghcr.io/wikid82/charon:latest - -project_name: charon - -builds: - - id: linux - dir: backend - main: ./cmd/api - binary: charon - env: - - CGO_ENABLED=0 - goos: - - linux - goarch: - - amd64 - - arm64 - ldflags: - - -s -w - - -X github.com/Wikid82/charon/backend/internal/version.Version={{.Version}} - - -X github.com/Wikid82/charon/backend/internal/version.GitCommit={{.Commit}} - - -X github.com/Wikid82/charon/backend/internal/version.BuildTime={{.Date}} - -archives: - - formats: - - tar.gz - id: linux - ids: - - linux - name_template: >- - {{ .ProjectName }}_ - {{- .Version }}_ - {{- .Os }}_ - {{- .Arch }} - files: - - LICENSE - - README.md - -nfpms: - - id: packages - ids: - - linux - package_name: charon - vendor: Charon - homepage: https://github.com/Wikid82/charon - maintainer: Wikid82 - description: "Charon - A powerful reverse proxy manager" - license: MIT - formats: - - deb - - rpm - contents: - - src: ./backend/data/ - dst: /var/lib/charon/data/ - type: dir - - src: ./frontend/dist/ - dst: /usr/share/charon/frontend/ - type: dir - dependencies: - - libc6 - - ca-certificates - -checksum: - name_template: 'checksums.txt' - -snapshot: - version_template: "{{ .Tag }}-next" - -changelog: - sort: asc - filters: - exclude: - - '^docs:' - - '^test:' diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 000000000..05b60243f --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "0.37.0" +} diff --git a/.version b/.version deleted file mode 100644 index 0a8bf80d6..000000000 --- a/.version +++ /dev/null @@ -1 +0,0 @@ -v0.27.0 diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 890fd804a..06f3f5553 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -690,13 +690,6 @@ "group": "test", "problemMatcher": [] }, - { - "label": "Utility: Check Version Match Tag", - "type": "shell", - "command": ".github/skills/scripts/skill-runner.sh utility-version-check", - "group": "none", - "problemMatcher": [] - }, { "label": "Utility: Clear Go Cache", "type": "shell", diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index e11d9b66b..e9b8bd408 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -163,7 +163,7 @@ graph TB | **Base Image** | Debian Trixie Slim | Latest | Security-hardened base | | **CI/CD** | GitHub Actions | N/A | Automated testing and deployment | | **Registry** | Docker Hub + GHCR | N/A | Image distribution | -| **Security Scanning** | Trivy + Grype | Latest | Vulnerability detection | +| **Security Scanning** | Trivy + Grype + Semgrep | Latest | Vulnerability detection | | **SBOM Generation** | Syft | Latest | Software Bill of Materials | | **Signature Verification** | Cosign | Latest | Supply chain integrity | @@ -1373,7 +1373,7 @@ go test ./integration/... 1. **Lint:** golangci-lint, ESLint, markdownlint, hadolint 2. **Test:** Go tests, Vitest, Playwright -3. **Security:** Trivy, CodeQL, Grype, Govulncheck +3. **Security:** Trivy, CodeQL, Grype, Govulncheck, Semgrep 4. **Build:** Docker image build 5. **Coverage:** Upload to Codecov (85% gate) โ€” `backend`, `frontend`, and `agent` each upload under a distinct Codecov flag @@ -1463,20 +1463,45 @@ go test ./integration/... ### Release Workflow -**Automated Release (GitHub Actions):** - -1. **Trigger:** Push tag `v1.2.0` -2. **Build:** Multi-platform Docker images -3. **Test:** Run E2E tests against built image -4. **Security:** Scan for vulnerabilities (block if Critical/High) -5. **SBOM:** Generate Software Bill of Materials (Syft) -6. **Sign:** Cryptographic signature with Cosign -7. **Provenance:** Generate SLSA provenance attestation -8. **Publish:** Push to Docker Hub and GHCR -9. **Release Notes:** Generate changelog from commits -10. **Notify:** Send release notification (Discord, email) - -**In-app changelog data:** Separately from step 9's GitHub release notes, `scripts/generate-changelog.sh` runs during the same `release-goreleaser.yml` workflow to parse conventional-commit history into `backend/internal/changelog/data/changelog.json`, which is `//go:embed`-ed into the binary and powers the in-app "What's New" modal (see "Changelog Subsystem" above). It writes a different file than step 9 and does not affect the GitHub release notes. +Versioning and release publication are handled by +[`googleapis/release-please-action`](https://github.com/googleapis/release-please-action) +(`.github/workflows/release-please.yml`), independently of the Docker +image build pipeline described below. See `VERSION.md` for the full +user-facing walkthrough; summarized here: + +1. **Trigger:** Push to `main` (any commit) +2. **`release-please.yml` runs** (independently of the Docker build): + computes releasable versions from Conventional Commit history and + opens/updates a standing `chore(main): release X.Y.Z` pull request. + No release ships yet at this point. +3. **A human merges that release PR** โ€” this is the only step that + actually cuts a release. release-please then tags the merge commit + `vX.Y.Z` (bare, no component prefix) and creates the GitHub Release. +4. **`orthrus-build.yml` fires on the new `v*` tag** and publishes + semver-tagged Orthrus agent images โ€” the one workflow with a real, + live dependency on the tag release-please creates. + +**Automated Docker Image Build (GitHub Actions, `docker-build.yml`):** + +Triggered independently by every push to `main`/`development` (branch +push, not the release tag): + +1. **Build:** Multi-platform Docker images +2. **Test:** Run E2E tests against built image +3. **Security:** Scan for vulnerabilities (block if Critical/High) +4. **SBOM:** Generate Software Bill of Materials (Syft) +5. **Sign:** Cryptographic signature with Cosign +6. **Provenance:** Generate SLSA provenance attestation +7. **Publish:** Push to Docker Hub and GHCR + +**In-app changelog data:** `scripts/generate-changelog.sh` runs during +`nightly-build.yml` (its one remaining real caller) to parse +conventional-commit history into `backend/internal/changelog/data/changelog.json`, +which is `//go:embed`-ed into the binary and powers the in-app "What's +New" modal (see "Changelog Subsystem" above). It depends only on real +`v*` tags existing in git history โ€” not on release-please's PR/Release +mechanism directly โ€” so it keeps working unchanged by this migration +as long as release-please continues creating bare `v*` tags. **Mandatory rollout gates (sign-off block):** @@ -1499,6 +1524,7 @@ go test ./integration/... - Trivy: Fast vulnerability scanning (filesystem) - Grype: Deep image scanning (layers, dependencies) - CodeQL: Static analysis (Go, JavaScript) + - Semgrep: Static analysis for security anti-patterns (Go, JS/TS, React, secrets, Dockerfile) 3. **Cryptographic Signing:** - Cosign signs Docker images with keyless signing (OIDC) diff --git a/CLAUDE.md b/CLAUDE.md index 151a71ab4..f4e4d84f5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -258,4 +258,3 @@ Available skills (see `.github/skills/*.SKILL.md` for full docs): | `security-scan-gorm` | Run GORM security scan | | `security-scan-go-vuln` | Run Go vulnerability check | | `integration-test-all` | Run all integration tests | -| `utility-version-check` | Check tool versions | diff --git a/Dockerfile b/Dockerfile index 14f618f9d..6fa1c87f7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,7 +10,7 @@ ARG BUILD_DEBUG=0 # ---- Pinned Toolchain Versions ---- # renovate: datasource=docker depName=golang versioning=docker -ARG GO_VERSION=1.26.5 +ARG GO_VERSION=1.26.6 # renovate: datasource=docker depName=alpine versioning=docker ARG ALPINE_IMAGE=alpine:3.24.1@sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b @@ -25,9 +25,14 @@ ARG CROWDSEC_RELEASE_SHA256=704e37121e7ac215991441cef0d8732e33fa3b1a2b2b88b53a0b # renovate: datasource=github-tags depName=expr-lang/expr extractVersion=^v(?.+)$ ARG EXPR_LANG_VERSION=1.17.8 # renovate: datasource=go depName=golang.org/x/net -ARG XNET_VERSION=0.57.0 +ARG XNET_VERSION=0.58.0 # renovate: datasource=go depName=golang.org/x/crypto -ARG XCRYPTO_VERSION=0.54.0 +ARG XCRYPTO_VERSION=0.55.0 +# klauspost/compress DoS/resource-exhaustion fix, matching how golang.org/x/crypto +# is patched above: pinned here so the CrowdSec/cscli and Caddy binaries (which +# pull it in transitively) are patched immediately, ahead of upstream releases. +# renovate: datasource=go depName=github.com/klauspost/compress +ARG KLAUSPOST_COMPRESS_VERSION=1.19.2 # renovate: datasource=npm depName=npm ARG NPM_VERSION=12.0.2 @@ -299,6 +304,7 @@ ARG XCADDY_VERSION=0.4.6 ARG EXPR_LANG_VERSION ARG XNET_VERSION ARG XCRYPTO_VERSION +ARG KLAUSPOST_COMPRESS_VERSION ARG CROWDSEC_VERSION # hadolint ignore=DL3018 @@ -382,6 +388,9 @@ RUN --mount=type=cache,target=/root/.cache/go-build \ _retry go get github.com/hslatman/ipstore@v0.4.0; \ _retry go get golang.org/x/crypto@v${XCRYPTO_VERSION}; \ _retry go get golang.org/x/net@v${XNET_VERSION}; \ + # klauspost/compress DoS/resource-exhaustion fix. Affects /usr/bin/caddy + # (transitive dependency). Fix available at v1.18.7. + _retry go get github.com/klauspost/compress@v${KLAUSPOST_COMPRESS_VERSION}; \ # GHSA-hrxh-6v49-42gf: grpc-go xDS RBAC and HTTP/2 vulnerabilities # Patched in grpc-go v1.82.1. Pin here so the Caddy binary is patched immediately. # renovate: datasource=go depName=google.golang.org/grpc @@ -516,6 +525,7 @@ ARG CROWDSEC_VERSION ARG CROWDSEC_RELEASE_SHA256 ARG EXPR_LANG_VERSION ARG XNET_VERSION +ARG KLAUSPOST_COMPRESS_VERSION # hadolint ignore=DL3018 RUN apk add --no-cache git clang lld @@ -554,6 +564,9 @@ RUN set -e; \ # renovate: datasource=go depName=golang.org/x/crypto _retry go get golang.org/x/crypto@v0.52.0; \ _retry go get golang.org/x/net@v${XNET_VERSION}; \ + # klauspost/compress DoS/resource-exhaustion fix. Affects /usr/local/bin/crowdsec + # and /usr/local/bin/cscli (transitive dependency). Fix available at v1.18.7. + _retry go get github.com/klauspost/compress@v${KLAUSPOST_COMPRESS_VERSION}; \ # GHSA-hrxh-6v49-42gf: grpc-go xDS RBAC and HTTP/2 vulnerabilities # Patched in grpc-go v1.82.1. Pin here so the CrowdSec binary is patched immediately. # renovate: datasource=go depName=google.golang.org/grpc @@ -590,6 +603,12 @@ RUN set -e; \ # GHSA-r277-6w6q-xmqw: kin-openapi ValidationHandler.Load() Fail-Open Authentication Bypass via NoopAuthenticationFunc Default # renovate: datasource=go depName=github.com/getkin/kin-openapi _retry go get github.com/getkin/kin-openapi@v0.144.0; \ + # CVE-2026-56864 / CVE-2026-56865: golang.org/x/mod/sumdb GOSUMDB tile-verification bypass + # (a colluding GOPROXY+GOSUMDB pair could forge sumdb tiles / serve module content outside + # the transparency log). Affects /usr/local/bin/crowdsec and /usr/local/bin/cscli โ€” go mod + # tidy's MVS resolution otherwise lands on v0.38.0. Fix available at v0.40.0. + # renovate: datasource=go depName=golang.org/x/mod + _retry go get golang.org/x/mod@v0.40.0; \ _retry go mod tidy # Fix compatibility issues with expr-lang v1.17.7 @@ -688,7 +707,7 @@ SHELL ["/bin/ash", "-o", "pipefail", "-c"] # Note: In production, users should provide their own MaxMind license key # This uses the publicly available GeoLite2 database # In CI, timeout quickly rather than retrying to save build time -ARG GEOLITE2_COUNTRY_SHA256=b4f624e1411c28701d724503b8d15ed4997de70cb6ea05d6f11bf572ea552240 +ARG GEOLITE2_COUNTRY_SHA256=8cc00bbcd9734df804acc36196c84abe65c2ef4beb4294c2bf4d25ac356db933 RUN mkdir -p /app/data/geoip && \ if [ "$CI" = "true" ] || [ "$CI" = "1" ]; then \ echo "โฑ๏ธ CI detected - quick download (10s timeout, no retries)"; \ diff --git a/SECURITY.md b/SECURITY.md index deae334b2..bf21754fd 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -997,6 +997,7 @@ for developer iteration. Tag-only overrides MUST NOT be used in CI contexts. |------|---------| | Trivy | Container image vulnerability scanning | | CodeQL | Static analysis for Go and JavaScript | +| Semgrep | Static analysis for security anti-patterns (Go, JS/TS, React, secrets, Dockerfile) | | govulncheck | Go module vulnerability scanning | | golangci-lint (gosec) | Go code linting | | npm audit | Frontend dependency scanning | @@ -1019,6 +1020,14 @@ artifacts for 90 days. **PR-Specific Scanning** โ€” extracts and scans only the Charon application binary on each pull request. Fails the PR if CRITICAL or HIGH vulnerabilities are found in application code. +**Semgrep SAST Scan** (`.github/workflows/semgrep.yml`) โ€” runs on every push and pull request to +`main`, `nightly`, and `development`, on manual dispatch, and weekly on Mondays at 04:00 UTC. Scans +the full repository inside a pinned `semgrep/semgrep` container using the `p/golang`, +`p/javascript`, `p/typescript`, `p/react`, `p/secrets`, and `p/dockerfile` rulesets โ€” the same +rule configs, exclusions, and ERROR/WARNING severity gate developers already run locally via +`scripts/pre-commit-hooks/semgrep-scan.sh`. Uploads SARIF results to the GitHub Security tab and +fails the build on any blocking finding. + ### Manual Reviews - Security code reviews for all major features diff --git a/VERSION.md b/VERSION.md index 311c0601c..56dfbc966 100644 --- a/VERSION.md +++ b/VERSION.md @@ -19,47 +19,49 @@ Example: `0.1.0-alpha`, `1.0.0-beta.1`, `2.0.0-rc.2` ## Creating a Release -### Canonical Release Process (Tag-Derived CI) - -1. **Create and push a release tag**: - - ```bash - - git tag -a v1.0.0 -m "Release v1.0.0" - git push origin v1.0.0 - - ``` - -2. **GitHub Actions automatically**: - - Runs release workflow from the pushed tag (`.github/workflows/release-goreleaser.yml`) - - Builds and publishes release artifacts/images through CI (`.github/workflows/docker-build.yml`) - - Creates/updates GitHub Release metadata - -3. **Container tags are published**: +### Canonical Release Process (release-please) + +Charon uses [`googleapis/release-please-action`](https://github.com/googleapis/release-please-action) +to compute versions, maintain a standing release PR, and cut tags + +GitHub Releases from Conventional Commit history. There is no manual +tagging step and no in-repo version manifest to hand-edit โ€” `.release-please-manifest.json` +is the single source of truth for "what version are we at," kept in +sync by release-please itself. + +1. **Commits land on `main`**: every push to `main` runs `.github/workflows/release-please.yml`, + which walks Conventional Commits since the last release and opens + or updates a standing `chore(main): release X.Y.Z` pull request. + If nothing releasable (`feat:`/`fix:`) has landed since the last + release, no PR is opened or updated. +2. **A human merges the release PR**: merging it is what actually + ships a release โ€” nothing goes out automatically before that. +3. **On merge, release-please**: + - Tags the merge commit `vX.Y.Z` (bare, no `charon-` prefix โ€” + pinned via `include-component-in-tag: false` in + `release-please-config.json`) + - Creates the GitHub Release for that tag +4. **Downstream, independently of release-please**: + - `.github/workflows/orthrus-build.yml` triggers on the new `v*` + tag and publishes semver-tagged Orthrus agent images + - The next `nightly-build.yml` run picks up the new tag via + `scripts/generate-changelog.sh`'s `git tag -l 'v*'` scan and + regenerates the in-app "What's New" changelog data + +5. **Container tags are published** (via `.github/workflows/docker-build.yml`, + triggered on the branch push, not the tag): - `v1.0.0` (exact version) - `1.0` (minor version) - `1` (major version) - `latest` (for non-prerelease on main branch) -### Legacy/Optional `.version` Path - -The `.version` file is optional and not the canonical release trigger. - -Use it only when you need local/version-file parity checks: - -1. **Set `.version` locally (optional)**: - - ```bash - echo "1.0.0" > .version - ``` - -1. **Validate `.version` matches the latest tag**: - - ```bash - - bash scripts/check-version-match-tag.sh +**Do not manually push `v*` tags.** A manually-created tag desyncs +`.release-please-manifest.json` (which release-please treats as its +source of truth, not live tag state) from the repo's real tag +history โ€” let release-please create every release tag going forward. - ``` +`release-please-config.json` sets `skip-changelog: true`, so this +hand-curated `CHANGELOG.md` is never touched by release-please's PRs +or Release generation. ### Deterministic Rollout Verification Gates (Mandatory) @@ -71,7 +73,6 @@ Enforcement points: - Release sign-off checklist/process (mandatory): All gates below remain required for release sign-off. - CI-supported checks (current): `.github/workflows/docker-build.yml` and `.github/workflows/supply-chain-verify.yml` enforce the subset currently implemented in workflows. - Manual validation required until CI parity: Validate any not-yet-implemented workflow gates via VS Code tasks `Security: Full Supply Chain Audit`, `Security: Verify SBOM`, `Security: Generate SLSA Provenance`, and `Security: Sign with Cosign`. -- Optional version-file parity check: `Utility: Check Version Match Tag` (script: `scripts/check-version-match-tag.sh`). - [ ] **Digest freshness/parity:** Capture pre-push and post-push index digests for the target tag in GHCR and Docker Hub, confirm expected freshness, @@ -251,7 +252,7 @@ docker build \ ## Changelog Generation -The release workflow automatically generates changelogs from commit messages. Use conventional commit format: +release-please's standing release PR body is generated from commit messages. Use conventional commit format: - `feat:` New features - `fix:` Bug fixes @@ -273,7 +274,5 @@ git commit -m "fix: correct proxy timeout handling" - CI derives the release `Version` from the Git tag (e.g., `v1.2.3`) and embeds this value into the backend binary via Go ldflags; frontend reads the version from the backend's API. This avoids automatic commits to `main`. -- The `.version` file is optional. If present, use the `scripts/check-version-match-tag.sh` script - or the included pre-commit hook to validate that `.version` matches the latest Git tag. -- CI will still generate changelogs automatically using the release-drafter workflow and create - GitHub Releases when tags are pushed. +- release-please creates the tag and the GitHub Release together when its standing release PR is + merged โ€” see "Canonical Release Process (release-please)" above. diff --git a/agent/go.mod b/agent/go.mod index 2235fadf1..9f3174863 100644 --- a/agent/go.mod +++ b/agent/go.mod @@ -1,11 +1,11 @@ module github.com/Wikid82/charon/agent -go 1.26.5 +go 1.26.6 require ( github.com/gorilla/websocket v1.5.3 github.com/hashicorp/yamux v0.1.2 - github.com/sirupsen/logrus v1.9.4 + github.com/sirupsen/logrus v1.10.0 github.com/stretchr/testify v1.11.1 ) diff --git a/agent/go.sum b/agent/go.sum index 0efd37fcc..00d20580c 100644 --- a/agent/go.sum +++ b/agent/go.sum @@ -18,8 +18,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= -github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= +github.com/sirupsen/logrus v1.10.0 h1:T8MxJJXVZkfcC5zSRMRAg2F8+lxjmUCGGWPzFxO+Msc= +github.com/sirupsen/logrus v1.10.0/go.mod h1:FXZFonkDAnFozmO+5hGAFvB0Yg9/j2SIhA/QuIkP180= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= diff --git a/backend/go.mod b/backend/go.mod index 84adc4cf6..19a9d28be 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -1,6 +1,6 @@ module github.com/Wikid82/charon/backend -go 1.26.5 +go 1.26.6 require ( filippo.io/age v1.3.1 @@ -17,14 +17,14 @@ require ( github.com/pkg/sftp v1.13.11 github.com/prometheus/client_golang v1.24.1 github.com/robfig/cron/v3 v3.0.1 - github.com/sirupsen/logrus v1.9.4 + github.com/sirupsen/logrus v1.10.0 github.com/stretchr/testify v1.11.1 github.com/studio-b12/gowebdav v0.13.0 - golang.org/x/crypto v0.54.0 - golang.org/x/mod v0.39.0 - golang.org/x/net v0.57.0 + golang.org/x/crypto v0.55.0 + golang.org/x/mod v0.40.0 + golang.org/x/net v0.58.0 golang.org/x/oauth2 v0.36.0 - golang.org/x/text v0.40.0 + golang.org/x/text v0.41.0 golang.org/x/time v0.15.0 gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/yaml.v3 v3.0.1 diff --git a/backend/go.sum b/backend/go.sum index 06a0f1cf9..7cd563f9a 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -165,8 +165,8 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= -github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= -github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= +github.com/sirupsen/logrus v1.10.0 h1:T8MxJJXVZkfcC5zSRMRAg2F8+lxjmUCGGWPzFxO+Msc= +github.com/sirupsen/logrus v1.10.0/go.mod h1:FXZFonkDAnFozmO+5hGAFvB0Yg9/j2SIhA/QuIkP180= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -218,12 +218,12 @@ go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= golang.org/x/arch v0.30.0 h1:sB9h+1gRGa2+LauFSV0tm8bK1J2yo1bx6/Uyi/P6DTU= golang.org/x/arch v0.30.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8= -golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= -golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= -golang.org/x/mod v0.39.0 h1:UF5zwQdCRRUpHfyPwr7d4UrGiVeldIsogtzWVnczL74= -golang.org/x/mod v0.39.0/go.mod h1:bvIbwjQ0HUFFf5AKukeeYQG4ZBUG9yxQbR9aEweIwYY= -golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= -golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= +golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M= +golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis= +golang.org/x/mod v0.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs= +golang.org/x/mod v0.40.0/go.mod h1:0/weTWkPWGBikyTWAX3dkjVztMmBA5hM0DH6BElSupE= +golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= +golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= @@ -232,12 +232,12 @@ golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= -golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= -golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= +golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= +golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= -golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE= -golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk= +golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI= +golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo= google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/backend/internal/api/handlers/cerberus_logs_ws.go b/backend/internal/api/handlers/cerberus_logs_ws.go index 222fa78a1..c4aad94c7 100644 --- a/backend/internal/api/handlers/cerberus_logs_ws.go +++ b/backend/internal/api/handlers/cerberus_logs_ws.go @@ -41,8 +41,7 @@ func (h *CerberusLogsHandler) LiveLogs(c *gin.Context) { logger.Log().Info("Cerberus logs WebSocket connection attempt") // Upgrade HTTP connection to WebSocket - // CheckOrigin is enforced on the shared upgrader in logs_ws.go (same package). - conn, err := upgrader.Upgrade(c.Writer, c.Request, nil) // nosemgrep: go.gorilla.security.audit.websocket-missing-origin-check.websocket-missing-origin-check + conn, err := upgradeWebSocket(c) if err != nil { logger.Log().WithError(err).Error("Failed to upgrade Cerberus logs WebSocket") return diff --git a/backend/internal/api/handlers/hecate_ws_handler.go b/backend/internal/api/handlers/hecate_ws_handler.go index b392f7940..541b5b0fe 100644 --- a/backend/internal/api/handlers/hecate_ws_handler.go +++ b/backend/internal/api/handlers/hecate_ws_handler.go @@ -37,7 +37,7 @@ func (h *HecateWSHandler) StreamLogs(c *gin.Context) { return } - conn, upgradeErr := upgrader.Upgrade(c.Writer, c.Request, nil) // nosemgrep: go.gorilla.security.audit.websocket-missing-origin-check.websocket-missing-origin-check + conn, upgradeErr := upgradeWebSocket(c) if upgradeErr != nil { logger.Log().WithError(upgradeErr).Error("hecate ws: upgrade failed") return diff --git a/backend/internal/api/handlers/logs_ws.go b/backend/internal/api/handlers/logs_ws.go index 2b846e7c5..cad751c4e 100644 --- a/backend/internal/api/handlers/logs_ws.go +++ b/backend/internal/api/handlers/logs_ws.go @@ -35,6 +35,15 @@ var upgrader = websocket.Upgrader{ }, } +// upgradeWebSocket upgrades an HTTP connection to WebSocket using the +// shared, origin-checked upgrader above. All WS handlers in this package +// MUST go through this helper rather than calling upgrader.Upgrade +// directly, so the origin check stays visible to single-file static +// analysis at every call site. +func upgradeWebSocket(c *gin.Context) (*websocket.Conn, error) { + return upgrader.Upgrade(c.Writer, c.Request, nil) +} + // LogEntry represents a structured log entry sent over WebSocket. type LogEntry struct { Level string `json:"level"` @@ -68,7 +77,7 @@ func (h *LogsWSHandler) HandleWebSocket(c *gin.Context) { logger.Log().Info("WebSocket connection attempt received") // Upgrade HTTP connection to WebSocket - conn, err := upgrader.Upgrade(c.Writer, c.Request, nil) + conn, err := upgradeWebSocket(c) if err != nil { logger.Log().WithError(err).Error("Failed to upgrade WebSocket connection") return diff --git a/backend/internal/api/handlers/stats_handler.go b/backend/internal/api/handlers/stats_handler.go index 00276ff82..23c52c6f0 100644 --- a/backend/internal/api/handlers/stats_handler.go +++ b/backend/internal/api/handlers/stats_handler.go @@ -172,7 +172,7 @@ func (h *StatsHandler) GetStatsHealth(c *gin.Context) { // StatsWS upgrades the connection to WebSocket and streams stats push messages. // GET /api/stats/ws func (h *StatsHandler) StatsWS(c *gin.Context) { - conn, err := upgrader.Upgrade(c.Writer, c.Request, nil) // nosemgrep: go.gorilla.security.audit.websocket-missing-origin-check.websocket-missing-origin-check + conn, err := upgradeWebSocket(c) if err != nil { return } diff --git a/docs/issues/semgrep-ci-manual-test-plan.md b/docs/issues/semgrep-ci-manual-test-plan.md new file mode 100644 index 000000000..bda5edb5a --- /dev/null +++ b/docs/issues/semgrep-ci-manual-test-plan.md @@ -0,0 +1,128 @@ +--- +title: "Manual Test Plan - Semgrep CI Security Scan" +status: Open +priority: Medium +labels: testing, ci, security +--- + +# Test Objective + +Confirm that the new `.github/workflows/semgrep.yml` CI workflow behaves correctly once it +actually runs against a live GitHub Actions PR โ€” image pull, scan execution, SARIF upload, and +the hard-fail gate. This is the one part of the Semgrep CI Security Scan feature that could not +be verified locally: Supervisor code review and the qa-security audit both passed (see +`docs/plans/current_spec.md` and `docs/reports/qa_report.md`), but neither can observe a real +GitHub Actions runner pulling the pinned container image or timing a full-repo scan under actual +CI conditions. + +# What Was Built + +- `scripts/pre-commit-hooks/semgrep-scan.sh` gained an additive, backward-compatible + `SEMGREP_SARIF_OUTPUT` env var so CI can reuse the exact same scan invocation developers already + run locally, for both a SARIF-producing pass and a hard-fail gate pass. +- `.github/workflows/semgrep.yml` runs that script inside a pinned + `semgrep/semgrep:1.173.0@sha256:...` container on every push/PR to `main`, `nightly`, and + `development`, on manual dispatch, and weekly (Mondays 04:00 UTC). It uploads SARIF results to + the GitHub Security tab and hard-fails the job on any ERROR/WARNING-severity finding. +- `scripts/ci/check-semgrep-parity.sh` guards against the workflow and the local script silently + drifting apart in the future. +- `SECURITY.md` and `ARCHITECTURE.md` were updated to document the new coverage. + +Commits: `6bf066f8` (script hook + parity guard), `2fbecf07` (workflow), `7c6fb04f` (docs). + +# Prerequisites + +- A pull request open against `development` (or `main`/`nightly`) that includes these three + commits, so `semgrep.yml`'s `pull_request` trigger fires. +- Repo admin/write access to view the Actions run and the Security โ†’ Code scanning alerts tab. + +# Manual Scenarios + +## 1) Workflow triggers and appears as a PR check + +- [ ] Open the PR containing commits `6bf066f8`, `2fbecf07`, `7c6fb04f`. +- [ ] **Expected**: A check named **Semgrep SAST Scan** (job `semgrep-scan` in workflow + `Semgrep - SAST Scan`) appears in the PR's checks list shortly after the PR is opened or + updated. + +## 2) Pinned container image pulls successfully + +- [ ] Open the Actions run for the Semgrep workflow, expand the earliest steps. +- [ ] **Expected**: No container-pull error (e.g. `manifest unknown`, rate-limit, or timeout + pulling `semgrep/semgrep:1.173.0@sha256:...`). The job proceeds past the container-setup + phase into "Checkout repository." + +## 3) Job completes within the timeout; check actual timing + +- [ ] Note the total run duration for the `semgrep-scan` job once it finishes. +- [ ] **Expected**: Job completes well within the current `timeout-minutes: 15` cap. +- [ ] **If the run takes noticeably close to 15 minutes** (cold image pull + rule-registry fetch + was never observed live before this PR โ€” flagged as an open risk by both Supervisor and + DevOps): file a follow-up to bump `timeout-minutes` to ~20-25 in `semgrep.yml`. This is not + a blocker for merging this PR, but should not be left unaddressed if observed. + +## 4) SARIF results appear in the Security tab + +- [ ] Navigate to the repo's **Security โ†’ Code scanning alerts** tab. +- [ ] Filter by tool **Semgrep**, category **semgrep**. +- [ ] **Expected**: A scan result is listed for the commit/PR, even if it shows 0 findings (a + SARIF upload with an empty `results` array is still a valid, visible scan entry โ€” this + confirms the upload step itself worked, not just that the repo is clean). + +## 5) Hard-fail gate passes on a clean repo + +- [ ] Check the **Run Semgrep (hard-fail gate)** step's log output. +- [ ] **Expected**: Step exits 0. The repo is expected to be clean โ€” 0 findings was reproduced + locally multiple times (both in DevOps validation and independently in QA's audit) โ€” so + this step should pass without needing any fix commits. + +## 6) Job summary renders correctly + +- [ ] Open the Actions run's **Summary** tab (not the individual job log). +- [ ] **Expected**: A "Semgrep SAST Scan Results" section is present, listing the rulesets + scanned (`p/golang, p/javascript, p/typescript, p/react, p/secrets, p/dockerfile`), the + severity gate (`ERROR, WARNING`), and a clear PASSED/FAILED line matching the job's actual + outcome. + +# Expected Results + +| Scenario | Expected outcome | +|---|---| +| PR trigger | "Semgrep SAST Scan" check appears on the PR | +| Image pull | Pinned `semgrep/semgrep` image pulls with no error | +| Timing | Job finishes comfortably under 15 minutes | +| SARIF upload | Result visible under Security โ†’ Code scanning alerts, tool "Semgrep", category `semgrep` | +| Hard-fail gate | Passes (0 findings expected) | +| Job summary | Renders ruleset, severity gate, and pass/fail line in the run summary tab | + +# Pass / Fail Criteria + +**PASS** โ€” All six scenarios behave as expected: the check appears, the image pulls, the job +finishes well under the timeout, SARIF results are visible in the Security tab under the correct +category, the gate step passes, and the job summary renders correctly. + +**FAIL** โ€” Any of: the check never appears on the PR, the image fails to pull, the job times out +or runs suspiciously close to the 15-minute cap, no SARIF entry appears in the Security tab, the +gate step fails unexpectedly on a repo believed to be clean, or the job summary is missing/blank. + +A FAIL on the gate step specifically should be triaged on its merits (a real finding vs. a CI +environment issue) before assuming the feature itself is broken โ€” see +`docs/plans/current_spec.md` ยง3.7 for documented edge cases. + +# Known Follow-Ups (not blockers) + +1. ~~**Renovate coverage for the pinned image is not yet configured.**~~ **Resolved** (commit + `9dc2be4e`). Added an explicit custom regex manager in `.github/renovate.json`, anchored on a + `# renovate: datasource=docker depName=semgrep/semgrep` comment above the `image:` line in + `semgrep.yml`, mirroring the existing Alpine-image digest tracker pattern used elsewhere in this + repo. Confirm on the next Semgrep image bump that Renovate actually opens a PR as expected. +2. **`timeout-minutes` may need adjustment after observing real timing.** Set to 15 based on local + estimates (~45-48s per full-repo scan pass locally); this was never observed against a cold + image pull + rule-registry fetch on an actual GitHub Actions runner. See Scenario 3 above โ€” + bump to ~20-25 if the real run comes in close to the cap. + +# Related + +- `docs/plans/current_spec.md` โ€” full implementation plan for the Semgrep CI Security Scan feature. +- `docs/reports/qa_report.md` โ€” QA/security audit (PASS, no blocking issues). +- Commits `6bf066f8`, `2fbecf07`, `7c6fb04f` on `development`. diff --git a/docs/plans/archive/2026-08-14_semgrep-ci-security-scan-plan.md b/docs/plans/archive/2026-08-14_semgrep-ci-security-scan-plan.md new file mode 100644 index 000000000..7aa3ef37f --- /dev/null +++ b/docs/plans/archive/2026-08-14_semgrep-ci-security-scan-plan.md @@ -0,0 +1,505 @@ +# Semgrep CI Security Scan โ€” Implementation Plan + +Status: Planning complete, revised per Supervisor review (round 2). +Owner for implementation: **devops** agent (CI/CD-only change; no backend-dev or frontend-dev involvement โ€” no application code, no models, no UI). +Branch: current working branch (`development`) per `CLAUDE.md` โ€” no worktree. +PR base branch: `development` (standard feature PR convention observed in this repo; `main` only receives weekly `nightly` promotion merges). + +--- + +## 1. Introduction + +### 1.1 Objective + +Add an independent Semgrep SAST scan to GitHub Actions CI that reproduces, byte-for-byte, the same scan behavior developers already run locally via `scripts/pre-commit-hooks/semgrep-scan.sh` (wired through `lefthook.yml`'s `pre-commit`/`pre-push`/`security-full` targets and `make security-local`). Today, Semgrep coverage exists **only** on the developer's machine โ€” CI has zero Semgrep footprint (confirmed: no match in `.github/workflows/`, `.github/renovate.json`, or any Dockerfile/compose file). This means: + +- A developer who bypasses lefthook (`--no-verify`, an emergency hotfix, a machine without semgrep installed) ships code with no Semgrep signal at all. +- Nobody re-verifies the "clean" local Semgrep run against a controlled, versioned environment โ€” the local binary's version, ruleset revision, and installed registry rules can silently drift per-developer with no CI backstop. + +This plan adds CI-side Semgrep coverage that is authoritative (independent of the developer's local environment) while staying faithful to the existing local invocation. + +### 1.2 Goals + +1. A new CI job runs the **exact same** rule configs, exclusions, and severity/error-gating behavior as `scripts/pre-commit-hooks/semgrep-scan.sh`'s default (no-override) path, scanning the full repo. +2. Semgrep's version is pinned in CI (image tag + digest) โ€” today there is no version pin anywhere in the repo for Semgrep, local or CI. +3. Findings are visible in the GitHub Security tab (SARIF upload), consistent with how CodeQL and Trivy results are surfaced today. +4. A hard-fail gate blocks the PR/branch on ERROR/WARNING findings, mirroring the local script's `--error` behavior โ€” CI is a gate, not just an informational report. +5. `scripts/pre-commit-hooks/semgrep-scan.sh`'s binary/version resolution logic (the `command -v semgrep` check, ยง2.1) is **not touched** โ€” that stays developer-local tooling, per the original brief's explicit scope boundary. The script's rule-config/exclude/severity logic, by contrast, **is** extended with one small, additive, backward-compatible hook (ยง2.7/ยง3.0) so CI can reuse it directly instead of duplicating it โ€” see ยง2.7 for why this is a different constraint than "freeze the whole file," and why the narrower reading is the right one. +6. Documentation (`SECURITY.md` and `ARCHITECTURE.md`) is updated to reflect the new CI coverage. + +### 1.3 Non-goals + +- No change to how the local pre-commit/pre-push semgrep **binary** is discovered, installed, or versioned (the `command -v semgrep` / exit-127 block in `scripts/pre-commit-hooks/semgrep-scan.sh` is untouched). +- No new GitHub Action marketplace dependency requiring npm/JS runtime โ€” Semgrep ships as a self-contained CLI in an official container image, which is used directly. +- No change to `.gitignore`, `.dockerignore`, `.codecov.yml`, or any `Dockerfile` (see ยง2.9 โ€” reviewed explicitly, no changes needed). +- No attempt to unify Trivy's/CodeQL's SARIF-upload plumbing into a shared reusable workflow โ€” out of scope for this feature; each scanner's workflow remains independent, consistent with current repo structure (`codeql.yml`, `security-pr.yml`, `security-weekly-rebuild.yml` are all separate files today). + +--- + +## 2. Research Findings + +### 2.1 Local Semgrep invocation (`scripts/pre-commit-hooks/semgrep-scan.sh`) + +Full script behavior (verified by reading the file): + +- Requires `semgrep` on `PATH`; exits 127 if missing (this resolution logic is untouched by this plan โ€” see ยง1.3). +- Default rule configs (used unless `SEMGREP_CONFIG` env override is set): + ``` + --config p/golang + --config p/javascript + --config p/typescript + --config p/react + --config p/secrets + --config p/dockerfile + ``` +- Targets: staged files if passed as args (lefthook `pre-commit`), else full-repo default `Dockerfile backend frontend/src scripts .github/workflows` (lefthook `security-full` / manual run). +- Exact scan flags (current, pre-change): + ``` + semgrep scan \ + "${SEMGREP_CONFIGS[@]}" \ + --severity ERROR \ + --severity WARNING \ + --error \ + --exclude "frontend/node_modules" \ + --exclude "frontend/coverage" \ + --exclude "frontend/dist" \ + --exclude-rule "go.secrets.gorm.gorm-empty-password.gorm-empty-password" \ + "${TARGETS[@]}" + ``` +- `--error` makes semgrep exit non-zero if any ERROR/WARNING-severity finding exists โ€” this is the local "hard fail" behavior CI must reproduce. + +Wiring confirmed in `lefthook.yml`: +- `pre-commit.semgrep` (line ~113-116): glob-scoped, staged-files-only, blocking. +- `security-full.semgrep` (line ~137-140, manual stage, `lefthook run security-full`): full-repo, no args โ†’ this is the invocation CI should mirror most closely (full-repo, not staged-file-scoped). +- `Makefile:security-local` additionally runs `SEMGREP_CONFIG=p/golang` as a fast pre-push subset โ€” this is a narrower override path, not the target for CI parity (CI should mirror the **full** default ruleset, matching `security-full`). + +### 2.2 Confirmed: zero Semgrep footprint in CI today + +`grep -rn "semgrep" .github/workflows/ .github/renovate.json` (and Dockerfiles/compose) returns no matches. Semgrep is 100% local-only today. (Note: the repo's Renovate config lives at `.github/renovate.json`, not a root-level `renovate.json` โ€” corrected throughout this plan.) + +### 2.3 Existing CI patterns to mirror + +**`.github/workflows/codeql.yml`** (closest pattern for a source-level SAST tool): +- Triggers: `pull_request`/`push` on `[main, nightly, development]`, `workflow_dispatch`, weekly `schedule` cron (`0 3 * * 1`, Mondays 03:00 UTC). +- `concurrency` group keyed on workflow/event/ref, `cancel-in-progress: true`. +- `permissions:` declared at **both** the workflow (top) level and again, identically, at job level (`contents: read`, `security-events: write`, `actions: read`, `pull-requests: read`). +- All third-party actions pinned by commit SHA with a `# vX.Y.Z` trailing comment, e.g. `github/codeql-action/init@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4`. +- Has a **parity guard** step ("Verify CodeQL parity guard" โ†’ `scripts/ci/check-codeql-parity.sh`) that runs *before* the scan, structurally checking that local pre-commit scripts, `.vscode/tasks.json`, and the CI workflow all agree on query-suite pinning and trigger branches โ€” added specifically because CodeQL's local/CI ruleset previously drifted silently (see `check-codeql-parity.sh` comment referencing a real incident: a suppressed finding rode through PR #1216 unnoticed because local and CI independently duplicated blocking logic). +- Emits results to `$GITHUB_STEP_SUMMARY`, then a **separate, later step** does the actual hard-fail (`Fail on High-Severity Findings`) โ€” reporting and gating are deliberately split into two steps so the summary always renders even on failure. + +**`.github/workflows/security-pr.yml`** (closest pattern for "pinned scanner โ†’ SARIF upload โ†’ hard-fail gate"): +- Runs Trivy via `aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25` (SHA-pinned, `# aquasecurity/trivy-action 0.36.0` comment), with an explicit `version: 'v0.73.0'` input additionally pinning the *scanner* version, not just the action wrapper. +- Runs the scan **twice**: once with `format: 'sarif'` (`continue-on-error: true`, purely for the Security tab), then again with `format: 'table'` + `exit-code: '1'` (no continue-on-error) as the actual blocking gate. It also has an explicit "Check Trivy SARIF output exists" gating step between the SARIF-producing run and the upload step. This two-pass "report, then gate" split, plus the existence check, is the direct template for Semgrep's SARIF-vs-hard-fail split (ยง3.3). +- SARIF uploaded via `github/codeql-action/upload-sarif@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7` (same SHA-pinned action already used elsewhere in this repo for SARIF ingestion โ€” no new third-party dependency needed for the upload step). +- Trigger shape is materially more complex than needed here (`workflow_run` chaining off `docker-build.yml`, PR-number resolution, artifact download) because Trivy scans a **built container image**. Semgrep scans **source**, so it needs none of that โ€” it can trigger directly on `push`/`pull_request` like CodeQL, with no dependency on a prior Docker build. + +### 2.4 Repo-wide pinning convention + +Every third-party action in this repo is pinned to an exact commit SHA with a trailing `# vX.Y.Z` comment โ€” never a floating tag, never `@latest`. This is enforced by convention/review, not currently by a lint rule for actions specifically. Any new job must follow this exactly. + +### 2.5 Semgrep version/mechanism research + +Options considered: + +| Option | Assessment | +|---|---| +| `pip install semgrep==` on `ubuntu-latest` | Works, but reintroduces a Python toolchain dependency into a Go+TS repo purely for CI plumbing (`CLAUDE.md`: "No Python โ€” do not introduce Python scripts or requirements"). While this is arguably a tooling install rather than an authored script, it still pulls in `pip`/Python resolution behavior (version solving, transitive dependency drift) that the repo's own conventions steer away from. Rejected. | +| `semgrep/semgrep-action` (formerly `returntocorp/semgrep-action`) marketplace GitHub Action | Semgrep's own current CI docs no longer lead with this as the primary GitHub Actions pattern; it's a thin wrapper around the same official Docker image. Using it would add an extra layer of indirection (an Action wrapping an image) for no behavioral benefit over using the image directly, and re-pinning *that* action's SHA doesn't pin Semgrep's own version any more precisely than pinning the image does. Rejected in favor of the image directly. | +| Official `semgrep/semgrep` Docker image, used as a job-level `container:`, pinned by exact tag **and** digest | Matches this repo's SHA-pinning strictness (a digest is the container-image equivalent of an action's commit SHA โ€” both are content-addressed, immutable references). Gives the CLI directly, with the identical `semgrep scan ...` invocation used locally โ€” maximizes behavioral parity with `semgrep-scan.sh`. **Selected.** | + +Confirmed via the Semgrep GitHub releases API (`api.github.com/repos/semgrep/semgrep/releases/latest`) and PyPI, current stable version at plan time is **`1.173.0`**. Resolved the corresponding Docker Hub manifest digest for `semgrep/semgrep:1.173.0`: + +``` +sha256:67319956da3dcb58baf5b322899c15458e3963e7018a86aeeb5cd224e69cb77a +``` + +Pinned reference to use in the workflow: + +``` +semgrep/semgrep:1.173.0@sha256:67319956da3dcb58baf5b322899c15458e3963e7018a86aeeb5cd224e69cb77a +``` + +**Note for the implementer (devops agent):** re-resolve this digest at implementation time (`docker buildx imagetools inspect semgrep/semgrep:1.173.0` or the registry API) rather than trusting the value transcribed into this plan verbatim, in case the tag's digest has moved between planning and implementation (Docker Hub does not guarantee a tag's digest is immutable the way a Git SHA is โ€” pinning to the *tag+digest pair as observed at merge time* is the achievable guarantee here, and Renovate, already active in this repo via `.github/renovate.json`, will pick up future digest/tag bumps the same way it tracks other pinned SHAs if configured to watch this image โ€” see ยง3.7 edge case). + +**Known gotcha (Semgrep's own docs, `semgrep.dev/docs/kb/semgrep-ci/using-nonroot-docker-image-with-gha`):** running `semgrep/semgrep` as a job-level `container:` against a `actions/checkout`-produced workspace can hit git's "dubious ownership" safety check because the container user doesn't match the checkout's file ownership. Mitigate with an explicit `git config --global --add safe.directory "$GITHUB_WORKSPACE"` step before invoking `semgrep-scan.sh` (ยง3.3). + +**Correction (Supervisor round 2, required change 1):** the container image reference **cannot** be centralized in a workflow-level `env:` var and referenced as `container.image: ${{ env.SEMGREP_IMAGE }}`. GitHub Actions' documented context-availability rules do not expose the `env` context to `jobs..container` โ€” this is a known, currently-true limitation (not something that needs "verification at implementation time"; treating it as an open question in the prior draft was itself the error). The plan now specifies the pinned string **inlined directly** in `container.image` as the only correct form (ยง3.2) โ€” no `env` indirection. + +### 2.6 Placement decision: new file vs. an existing workflow + +*(Per mid-task correction from Management: decide the best location and justify it, rather than defaulting to a new file. Approved as-is by Supervisor round 2 โ€” no changes in this revision.)* + +Three placements were evaluated: + +| Placement | Verdict | +|---|---| +| **New job added to `codeql.yml`** | Rejected. `codeql.yml`'s entire structure is a `strategy.matrix` over CodeQL *languages* (`go`, `javascript-typescript`), with per-language conditional steps (`if: matrix.language == 'go'`) for Go toolchain setup/build and the CodeQL parity guard. Semgrep is not a CodeQL language variant โ€” it's a different tool with a different container, different config format, and a different (single, non-matrixed) invocation. Bolting it in as a third matrix leg would force awkward `if: matrix.language == 'semgrep'` conditionals across steps that don't apply to it (Autobuild, `codeql-action/init`, Go build verification), degrading the readability of a file whose entire premise is "one job, matrixed by CodeQL language." Also couples Semgrep's schedule/trigger lifecycle to CodeQL's, when they are independent tools that should be able to fail, be disabled, or be re-scheduled independently. | +| **New job added to `security-pr.yml`** | Rejected. That workflow's trigger shape and majority of its steps exist *solely* to solve "how do I scan a Docker image that was already built by a separate upstream workflow" โ€” PR-number resolution from `workflow_run` payloads, artifact download/load fallback logic, container extraction of the `charon` binary, a trust-boundary validation step for the `workflow_run` event. None of that applies to Semgrep, which scans source text directly on `push`/`pull_request` with no dependency on `docker-build.yml` having run first. Adding a source-scanning job to an image-scanning workflow would mean either (a) it inherits triggers/conditions built for image scanning that don't fit it (e.g. `workflow_dispatch` inputs are `pr_number`-shaped, meaningless for a source scan), or (b) it needs its own parallel `if:` conditions bolted onto an already condition-heavy file, adding complexity for no shared benefit โ€” the two jobs would share a file but no actual logic. | +| **New file: `.github/workflows/semgrep.yml`** | **Selected.** Semgrep is source-level SAST, triggered directly on `push`/`pull_request`/`schedule`/`workflow_dispatch` โ€” structurally identical in trigger shape to `codeql.yml`, but a distinct tool with its own container, config, and failure/gating semantics. This also matches the repo's existing convention of **one file per scanner**: `codeql.yml` (CodeQL), `security-pr.yml` (Trivy on PR images), `security-weekly-rebuild.yml` (Trivy weekly full scan) are already separate files rather than merged into one "security" workflow, even though they're conceptually related. A dedicated `semgrep.yml` continues that pattern: each scanner is independently triggerable, independently disable-able, and independently readable, at the cost of one more file โ€” a cost the repo has already accepted three times over for its other scanners. | + +### 2.7 "Freeze the whole script" reconsidered โ€” design revision (Supervisor round 2, required change 3) + +**The original brief's non-goal, re-read precisely:** *"Do NOT touch `scripts/pre-commit-hooks/semgrep-scan.sh`'s binary/version resolution logic itself โ€” that's explicitly out of scope, reserved for the user's own local tooling."* This is a constraint about **binary/version discovery** (the `command -v semgrep` / exit-127 block, ยง2.1) โ€” not a blanket freeze on every line of the file. The first draft of this plan over-read it into "never touch this file at all," which forced: + +- A second, hand-written `semgrep scan ...` invocation inline in the workflow YAML, duplicating all six `--config` flags, all three `--exclude` flags, and the `--exclude-rule` value. +- A `check-semgrep-parity.sh` script whose primary job was detecting drift between that duplicated invocation and the real script. +- Pressure to extract shared assertion helpers out of `check-codeql-parity.sh` mainly to support that parity script's config-matching checks. + +That is real, avoidable complexity, not an inherent requirement. **Revised design (adopted โ€” option (a) from Supervisor's feedback):** add one small, additive, backward-compatible hook to `semgrep-scan.sh` itself, leaving the binary/version-resolution logic (the actual thing the non-goal protects) completely untouched: + +```bash +# Existing lines (SEMGREP_CONFIGS / TARGETS construction) unchanged above this point. + +if [ -n "${SEMGREP_SARIF_OUTPUT:-}" ]; then + OUTPUT_FLAGS=(--sarif --output "${SEMGREP_SARIF_OUTPUT}") +else + OUTPUT_FLAGS=(--error) +fi + +semgrep scan \ + "${SEMGREP_CONFIGS[@]}" \ + --severity ERROR \ + --severity WARNING \ + "${OUTPUT_FLAGS[@]}" \ + --exclude "frontend/node_modules" \ + --exclude "frontend/coverage" \ + --exclude "frontend/dist" \ + --exclude-rule "go.secrets.gorm.gorm-empty-password.gorm-empty-password" \ + "${TARGETS[@]}" +``` + +Behavior: +- **`SEMGREP_SARIF_OUTPUT` unset (every existing call site โ€” `pre-commit`, `pre-push`/`security-full`, `make security-local`):** `OUTPUT_FLAGS=(--error)` โ€” byte-identical to today's behavior. Zero change for any existing developer workflow. +- **`SEMGREP_SARIF_OUTPUT=` set (new โ€” CI only):** swaps `--error` for `--sarif --output `, while every `--config`, `--exclude`, and `--exclude-rule` argument stays exactly as-is, sourced from exactly one place. + +This lets CI invoke the **same script** for both the SARIF-producing pass and the hard-fail gate pass (ยง3.3 steps 5 and 8), varying only an env var. Consequences: + +- The duplicated `--config`/`--exclude` list in the workflow YAML is **eliminated entirely** โ€” there is now exactly one place (`semgrep-scan.sh`) that defines what gets scanned, for both local and CI, for both the reporting pass and the gating pass. +- `check-semgrep-parity.sh` shrinks correspondingly (ยง3.4) โ€” it no longer needs to compare two independent config lists (nothing to compare; there's only one). It still has a real, narrower job: confirming the additive hook isn't silently removed, confirming the workflow actually delegates to the script for both passes (rather than a future edit reintroducing an inline duplicate), and confirming the image pin and trigger branches stay correct. This is a smaller, more clearly justified guard than the original draft's. +- The pressure to extract `scripts/ci/lib/workflow-yaml-asserts.sh` out of `check-codeql-parity.sh` is now a plain, optional DRY nicety (the branch-check helper is still needed by both scripts) rather than something load-bearing for the config-parity story โ€” see ยง3.5. + +**Why not stop here and also drop the parity guard entirely?** Because two failure-independent invariants remain worth checking even with zero config duplication: (1) that the additive `SEMGREP_SARIF_OUTPUT` hook stays present in the script (a future refactor of `semgrep-scan.sh` could drop it without realizing CI depends on it), and (2) that the workflow keeps *delegating* to the script for both passes rather than a future edit reintroducing an inline `semgrep scan` call (e.g. someone "simplifying" the SARIF step by hand and accidentally dropping an `--exclude`). Both are cheap, structural, grep-level checks โ€” proportionate, not over-engineering, and much smaller than the original draft's guard (ยง3.4). + +This section supersedes the original ยง2.7 ("Parity guard: warranted, and why") from the first draft. + +### 2.8 Documentation review + +- **`SECURITY.md`** (`## Security Audits & Scanning` โ†’ `### Automated Scanning` table, lines 992-1020): lists Trivy, CodeQL, govulncheck, golangci-lint (gosec), npm audit, and a `### Scanning Workflows` subsection describing each workflow file's purpose (`docker-build.yml`, `supply-chain-verify.yml`, `security-weekly-rebuild.yml`, PR-specific scanning). **This is the primary file to update** โ€” add a `Semgrep` row to the table and a new `**Semgrep SAST Scan**` paragraph under `### Scanning Workflows` describing `.github/workflows/semgrep.yml`. +- **`docs/security.md`**: verified by full-text search (`codeql|trivy|scan|pipeline`, no matches) โ€” this file is entirely about the Cerberus runtime security feature (CrowdSec/WAF/access lists), unrelated to the CI/SAST scanning pipeline. **No change needed here.** +- **`ARCHITECTURE.md` (Supervisor round 2, required change 2 โ€” added to scope):** `CLAUDE.md` requires `ARCHITECTURE.md` updates for changes touching security architecture, and this file already documents the CI security-scanning stack in three places that must be kept current: + - Line 166, tech-stack table: `| **Security Scanning** | Trivy + Grype | Latest | Vulnerability detection |` โ€” append Semgrep, e.g. `Trivy + Grype + Semgrep`. + - Line 1376, CI Jobs list: `3. **Security:** Trivy, CodeQL, Grype, Govulncheck` โ€” append `, Semgrep`. + - Lines 1498-1501, "Container Scanning" components list (`Trivy: ...`, `Grype: ...`, `CodeQL: ...`) โ€” add a fourth line, `Semgrep: Static analysis for security anti-patterns (Go, JS/TS, React, secrets, Dockerfile)`, consistent with the existing one-line-per-tool style. + - This is now part of Commit 3's scope (ยง6) and Acceptance Criteria (ยง5), alongside `SECURITY.md`. + +### 2.9 Ignore-file / build-file review (explicit confirmation per `CLAUDE.md`) + +- **`.gitignore`**: already contains a blanket `*.sarif` ignore (line 189) with a narrow `!scripts/security/testdata/*.sarif` carve-out (line 190). A new `semgrep-results.sarif` file in the repo root matches the existing wildcard โ€” **no change needed**. +- **`.dockerignore`**: already excludes `*.sarif` (line 179) โ€” irrelevant anyway, since this is a CI-only workflow change with no Docker image content change โ€” **no change needed**. +- **`.codecov.yml`**: workflow-only YAML change, produces no coverage-relevant files โ€” **no change needed**. +- **Any `Dockerfile`**: not touched; Semgrep runs in its own CI container, never inside the Charon application image โ€” **no change needed**. + +(Approved as-is by Supervisor round 2 โ€” no changes in this revision.) + +### 2.10 Commit scope: `feat:` vs `feat(security):` + +Per `CLAUDE.md`, `feat:`/`fix:`/`perf:` trigger Docker builds; `chore:` skips them, and `feat(security):`/`fix(security):` is reserved for "genuinely security-relevant... real vulnerability fixes, new protective mechanisms." **Decision:** the workflow-adding commit (Commit 2, ยง6) qualifies as a **new protective mechanism** โ€” it is, definitionally, new automated vulnerability/anti-pattern detection gating merges โ€” so it uses `feat(security):`, not plain `feat:`. Commit 1 (the additive `semgrep-scan.sh` hook + parity guard) also touches genuine security tooling directly and is scoped `feat(security):` for the same reason. Commit 3 (docs) stays `docs:`, matching repo convention for documentation-only changes regardless of what they document. Per `CLAUDE.md`'s vagueness requirement for `(security)` subjects, none of these commit subjects name a vulnerability class or attack vector โ€” they describe the category ("add CI security scanning coverage") only, which is appropriate here since this isn't a vulnerability fix in the first place, just extra coverage. + +**Nuance retained from the original draft:** this change touches zero Docker-build-relevant paths (no `Dockerfile`, no backend/frontend source), so the triggered Docker build (a side effect of `feat`/`feat(security)` prefixes repo-wide) is harmless but expected โ€” not a sign something is wrong with a "just workflow files + one shell script" PR. + +--- + +## 3. Technical Specifications + +### 3.0 Change to `scripts/pre-commit-hooks/semgrep-scan.sh` (additive, in scope per ยง2.7) + +**File:** `scripts/pre-commit-hooks/semgrep-scan.sh` +**Change:** insert the `OUTPUT_FLAGS` branch (ยง2.7) immediately before the existing `semgrep scan \` invocation, and replace the invocation's `--error` line with `"${OUTPUT_FLAGS[@]}"`. No other line in the file changes โ€” the `command -v semgrep` check, the `SEMGREP_CONFIG` override branch, and the `TARGETS` construction are byte-identical to today. +**Backward compatibility:** every existing call site (`lefthook.yml`'s `pre-commit.semgrep`, `security-full.semgrep`, `Makefile`'s `security-local`) never sets `SEMGREP_SARIF_OUTPUT`, so `OUTPUT_FLAGS=(--error)` unconditionally for all of them โ€” identical exit-code and output behavior to the pre-change script. +**New behavior (CI-only):** `SEMGREP_SARIF_OUTPUT= bash scripts/pre-commit-hooks/semgrep-scan.sh [targets...]` scans with the same configs/exclusions but emits SARIF to `` instead of hard-failing on findings. + +### 3.1 New file: `.github/workflows/semgrep.yml` + +No API/DB/frontend surface โ€” this is CI/YAML only. Full structural spec below (devops agent should treat this as the authoritative shape; exact YAML syntax is implementer's to finalize, but every element listed must be present). + +**Workflow name:** `Semgrep - SAST Scan` + +**Triggers:** +```yaml +on: + pull_request: + branches: [main, nightly, development] + push: + branches: [main, nightly, development] + workflow_dispatch: + schedule: + - cron: '0 4 * * 1' # Mondays 04:00 UTC โ€” offset 1h after CodeQL's 03:00 to avoid runner contention +``` + +**Concurrency:** +```yaml +concurrency: + group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.head_ref || github.ref_name }} + cancel-in-progress: true +``` +(Identical pattern to `codeql.yml`.) + +**Permissions โ€” declared at both workflow (top) level and job level, identically, matching `codeql.yml`'s style (Supervisor round 2, non-blocking fix):** +```yaml +permissions: + contents: read + security-events: write + actions: read + pull-requests: read +``` +This exact block appears twice: once at the workflow top level (sibling of `on:`/`concurrency:`), and again inside `jobs.semgrep-scan.permissions` (ยง3.2). + +### 3.2 Job: `semgrep-scan` + +**Correction (Supervisor round 2, required change 1):** the pinned image is inlined directly as a literal string in `container.image` โ€” `jobs..container` does not have access to the `env` context per GitHub Actions' documented context-availability rules, so a workflow-level `env:` indirection (as drafted originally) would not resolve at all. Inlining is the only correct form, not a fallback. + +```yaml +jobs: + semgrep-scan: + name: Semgrep SAST Scan + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read + security-events: write + actions: read + pull-requests: read + container: + image: semgrep/semgrep:1.173.0@sha256:67319956da3dcb58baf5b322899c15458e3963e7018a86aeeb5cd224e69cb77a # semgrep/semgrep 1.173.0 +``` + +### 3.3 Steps + +1. **Checkout repository** + ```yaml + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + ref: ${{ github.ref }} + ``` + (Same SHA already pinned and in active use in `codeql.yml` โ€” reuse, don't re-pin a different version.) + +2. **Fix git safe.directory for container user** (mitigates the "dubious ownership" issue documented in Semgrep's own GHA KB article, ยง2.5): + ```yaml + - name: Configure git safe.directory + run: git config --global --add safe.directory "$GITHUB_WORKSPACE" + ``` + +3. **Verify Semgrep parity guard** (ยง3.4): + ```yaml + - name: Verify Semgrep parity guard + run: bash scripts/ci/check-semgrep-parity.sh + ``` + +4. **Print Semgrep version** (cheap sanity check that the pinned image actually resolves to the expected CLI version โ€” catches a bad digest pin immediately and legibly, rather than surfacing as a confusing downstream scan failure): + ```yaml + - name: Verify Semgrep version + run: semgrep --version + ``` + +5. **Run Semgrep (SARIF output, non-blocking)** โ€” calls the real script (ยง2.7/ยง3.0) with the new opt-in var; no duplicated config list. + ```yaml + - name: Run Semgrep (SARIF output) + id: semgrep_sarif + continue-on-error: true + env: + SEMGREP_SARIF_OUTPUT: semgrep-results.sarif + run: bash scripts/pre-commit-hooks/semgrep-scan.sh + ``` + `continue-on-error: true` because this pass must not block the job even if semgrep itself errors โ€” the SARIF file's presence is checked explicitly next (step 6), and the actual gate is step 8, not this step. + +6. **Check Semgrep SARIF output exists** (mirrors `security-pr.yml`'s `Check Trivy SARIF output exists` step โ€” this was an orphaned reference in the first draft of this plan; it is now a real, numbered step): + ```yaml + - name: Check Semgrep SARIF output exists + id: semgrep_sarif_check + if: always() + run: | + if [ -f semgrep-results.sarif ]; then + echo "exists=true" >> "$GITHUB_OUTPUT" + else + echo "exists=false" >> "$GITHUB_OUTPUT" + echo "No Semgrep SARIF output found; skipping SARIF upload" + fi + ``` + +7. **Upload Semgrep SARIF to GitHub Security** (gated on step 6's output rather than blindly attempting the upload): + ```yaml + - name: Upload Semgrep SARIF to GitHub Security + if: always() && steps.semgrep_sarif_check.outputs.exists == 'true' + uses: github/codeql-action/upload-sarif@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7 + with: + sarif_file: semgrep-results.sarif + category: semgrep + continue-on-error: true + ``` + (Reuses the exact SHA already pinned for this purpose in `security-pr.yml` โ€” no new pin to introduce or maintain.) + +8. **Run Semgrep (hard-fail gate)** โ€” calls the same script, this time with the default (unset `SEMGREP_SARIF_OUTPUT`) path, i.e. its normal `--error` behavior: + ```yaml + - name: Run Semgrep (hard-fail gate) + run: bash scripts/pre-commit-hooks/semgrep-scan.sh + ``` + This is the literal `security-full` invocation (ยง2.1) โ€” same script, same default full-repo targets, same `--error` flag, run a second time (this time without the SARIF env var) so failure here genuinely gates the job. If this step fails, the job fails, blocking the PR/branch โ€” this is the CI-independent reproduction of the local "green" signal the feature exists to deliver. SARIF upload (step 7) has already completed by this point, so a gate failure here does not suppress the informational upload โ€” order matters and is intentional. + +9. **Upload SARIF artifact** (retention, matches `security-pr.yml`'s `Upload scan artifacts` step): + ```yaml + - name: Upload SARIF artifact + if: always() && steps.semgrep_sarif_check.outputs.exists == 'true' + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v4.4.3 + with: + name: semgrep-sarif-${{ github.run_id }} + path: semgrep-results.sarif + retention-days: 14 + continue-on-error: true + ``` + +10. **Job summary** + ```yaml + - name: Create job summary + if: always() + run: | + { + echo "## Semgrep SAST Scan Results" + echo "" + echo "**Rulesets**: p/golang, p/javascript, p/typescript, p/react, p/secrets, p/dockerfile" + echo "**Severity Gate**: ERROR, WARNING (--error)" + if [ "${{ job.status }}" == "success" ]; then + echo "PASSED: no blocking Semgrep findings" + else + echo "FAILED: Semgrep reported blocking findings โ€” see step logs and the Security tab" + fi + } >> "$GITHUB_STEP_SUMMARY" + ``` + +### 3.4 New file: `scripts/ci/check-semgrep-parity.sh` + +**Revised, smaller scope (per ยง2.7's design change)** โ€” modeled on `scripts/ci/check-codeql-parity.sh`'s approach (grep/structural assertions, not full YAML parsing), but no longer needs to compare two independent config lists, because ยง3.0's design means there's only one config list in the whole repo (in `semgrep-scan.sh`) and the workflow only ever delegates to it. Exits non-zero with an `::error title=Semgrep parity drift::` annotation on any mismatch. + +**Checks performed:** + +1. Required files exist: `.github/workflows/semgrep.yml`, `scripts/pre-commit-hooks/semgrep-scan.sh`. +2. Assert `scripts/pre-commit-hooks/semgrep-scan.sh` still contains the string `SEMGREP_SARIF_OUTPUT` โ€” the additive CI hook (ยง3.0) must not be silently removed by a future edit to the script that forgets CI depends on it. +3. Assert `.github/workflows/semgrep.yml` contains **two** distinct delegating calls to the real script, not a reimplemented/inlined `semgrep scan ...` invocation: + - a call with `SEMGREP_SARIF_OUTPUT` set (the reporting pass, step 5) โ€” e.g. assert both the literal strings `SEMGREP_SARIF_OUTPUT` and `scripts/pre-commit-hooks/semgrep-scan.sh` appear within the same step block; + - a bare `bash scripts/pre-commit-hooks/semgrep-scan.sh` call with no env override (the gate pass, step 8). + - This is the direct analogue of `check-codeql-parity.sh`'s "shared blocking logic must live in exactly one place" check (that script's lines enforcing `SHARED_GATE_SCRIPT` usage) โ€” applied here to prevent a future edit from "simplifying" either step by inlining `semgrep scan` directly, which would silently reintroduce the duplicated-config problem ยง2.7 eliminated. +4. Assert `.github/workflows/semgrep.yml`'s pinned image reference matches the pattern `semgrep/semgrep:[0-9]+\.[0-9]+\.[0-9]+@sha256:[0-9a-f]{64}` (tag + digest both present โ€” catches an accidental un-pin, e.g. someone changing it to `semgrep/semgrep:latest` during a quick edit). +5. Assert `pull_request`/`push` trigger branches in `semgrep.yml` are `[main, nightly, development]`, reusing `check-codeql-parity.sh`'s existing `ensure_event_branches_semantic` helper pattern (ยง3.5). + +Note what this script **no longer does**, relative to the first draft: it does not enumerate or compare `--config`/`--exclude`/`--exclude-rule` values between two files, because after ยง3.0's change there is only one file that defines them. + +**Where it's invoked:** +- `.github/workflows/semgrep.yml` step 3 (ยง3.3), analogous to `codeql.yml`'s "Verify CodeQL parity guard" step. +- Not wired into `lefthook.yml` in this PR โ€” consistent with existing precedent: `check-codeql-parity.sh` is also CI-only today, invoked directly from `codeql.yml` and not from any lefthook stage. Noted as a possible follow-up, not a gap introduced by this plan. + +### 3.5 Shared helper extraction (optional, DRY nicety โ€” from ยง3.4 item 5) + +`check-semgrep-parity.sh` is now the second script needing the branch-list assertion logic (`ensure_event_branches` / `ensure_event_branches_with_yq` / `ensure_event_branches_semantic`) that currently lives only in `check-codeql-parity.sh`. Recommend extracting it into `scripts/ci/lib/workflow-yaml-asserts.sh`, sourced by both scripts via `source "$(dirname "${BASH_SOURCE[0]}")/lib/workflow-yaml-asserts.sh"`, per `CLAUDE.md`'s "consolidate after second occurrence" DRY guideline. This is a pure refactor of existing, already-tested logic โ€” low risk. Unlike the first draft, this extraction is no longer load-bearing for anything (the config-parity story doesn't depend on it, since there's no config duplication left to compare) โ€” it is a legitimate but strictly optional cleanup. If time-boxed out of this PR, note it as a follow-up rather than skipping silently. + +### 3.6 SARIF category naming + +`category: semgrep` for the `upload-sarif` step (ยง3.3 step 7) โ€” single, flat category since (unlike CodeQL's per-language matrix) there is only one Semgrep job/run per commit, no need for a parameterized category string. + +### 3.7 Error handling / edge cases + +| Scenario | Behavior | +|---|---| +| Pinned image digest becomes invalid/removed from registry (rare, but Docker Hub retention policies exist) | Job fails at container-pull time with a clear GitHub Actions infra error, not a silent skip. Remediation: re-resolve digest, bump the pin โ€” a normal dependency-bump PR, same as any other pinned SHA bump in this repo. | +| SARIF step (step 5) itself crashes (e.g. semgrep internal error, not a rule finding) | `continue-on-error: true` on step 5 means the job continues; step 6 explicitly checks for the SARIF file's existence and sets an output consumed by steps 7 and 9, so a missing file cleanly skips upload rather than `upload-sarif` failing opaquely on a missing path. | +| Hard-fail gate step (step 8) fails legitimately (real findings) | Job fails, PR shows a red check, `$GITHUB_STEP_SUMMARY` still renders (step 10 runs on `if: always()`), SARIF is still uploaded to the Security tab (step 7 already ran before step 8 โ€” order is intentional: SARIF upload must happen *before* the blocking step so a gate failure doesn't skip the informational upload). | +| Renovate later proposes bumping the pinned `semgrep/semgrep` image tag/digest | Handled like any other Renovate-tracked pin, via `.github/renovate.json` โ€” devops agent should confirm at implementation time whether Renovate's Docker-image datasource already picks up `container: image:` refs in workflow YAML by default, or needs an explicit entry added to `.github/renovate.json`; note as a follow-up if configuration is needed, not a blocker for this PR. | +| A future edit to `semgrep-scan.sh` removes the `SEMGREP_SARIF_OUTPUT` hook, or an edit to `semgrep.yml` reintroduces an inline `semgrep scan` call instead of delegating | `check-semgrep-parity.sh` fails CI on the very next PR that makes either change, per ยง3.4 items 2-3. | + +--- + +## 4. Implementation Plan + +This is a CI/DevOps-only change plus one small, additive shell-script change. There is no Playwright/E2E surface (no user-facing behavior changes), no backend implementation, no frontend implementation. The phase structure below is adapted accordingly โ€” **the `devops` agent implements this directly; no handoff to backend-dev, frontend-dev, or playwright-dev is needed.** + +### Phase 1 โ€” Foundation (script hook + parity guard + optional shared lib) +- Apply the additive `SEMGREP_SARIF_OUTPUT` change to `scripts/pre-commit-hooks/semgrep-scan.sh` (ยง3.0). +- Write `scripts/ci/check-semgrep-parity.sh` (ยง3.4). +- Optionally extract `scripts/ci/lib/workflow-yaml-asserts.sh` from `check-codeql-parity.sh` (ยง3.5); if done, refactor `check-codeql-parity.sh` to source it and verify it still passes unchanged. +- Validation gate: run the existing `semgrep` lefthook hooks locally (`lefthook run pre-commit` touching a Go/JS file, or `lefthook run security-full`) to confirm the script's default (`SEMGREP_SARIF_OUTPUT` unset) behavior is byte-identical to pre-change โ€” this is the regression check for ยง3.0's edit. `shellcheck scripts/pre-commit-hooks/semgrep-scan.sh scripts/ci/check-semgrep-parity.sh` (+ `scripts/ci/lib/workflow-yaml-asserts.sh` if extracted). If the shared lib was extracted, `bash scripts/ci/check-codeql-parity.sh` still exits 0 (regression check on that refactor). + +### Phase 2 โ€” Workflow file +- Write `.github/workflows/semgrep.yml` per ยง3.1-ยง3.3 (10 steps, including the SARIF-existence-check as a first-class step, not an orphaned reference). +- Validation gate: `actionlint .github/workflows/semgrep.yml` (tool already required per `lefthook.yml`'s `actionlint` hook, ยง2 header comment listing required tools). `bash scripts/ci/check-semgrep-parity.sh` now passes against the real files. YAML syntax sanity via `yq eval '.' .github/workflows/semgrep.yml >/dev/null` or equivalent. +- **Live GitHub Actions execution cannot be validated locally** โ€” the actual scan run (image pull, semgrep execution against the real repo, SARIF upload, gate pass/fail) is confirmed only once the PR opens and the workflow triggers on `pull_request`. Note this explicitly in the PR description as a manual verification step, not a local DoD gate. + +### Phase 3 โ€” Documentation +- Update `SECURITY.md` per ยง2.8: add Semgrep row to the `### Automated Scanning` table, add a `**Semgrep SAST Scan**` paragraph to `### Scanning Workflows` describing `.github/workflows/semgrep.yml`'s trigger shape and what it covers. +- Update `ARCHITECTURE.md` per ยง2.8: the three call-outs at lines 166, 1376, and 1498-1501. +- Validation gate: `markdownlint SECURITY.md ARCHITECTURE.md` (tool already required per `lefthook.yml` header comment). + +### Phase 4 โ€” Integration validation +- `lefthook run pre-commit` (full local hook suite, including the existing `actionlint` and `semgrep` hooks, to confirm nothing in this change breaks existing local gates). +- `bash scripts/ci/check-codeql-parity.sh` (if refactored) and `bash scripts/ci/check-semgrep-parity.sh` both green. +- Manual review of the rendered `semgrep.yml` against `codeql.yml`/`security-pr.yml` for pinning-comment consistency (every third-party `uses:` has a SHA + version comment; the container image has tag + digest inlined, not via `env`). + +### Phase 5 โ€” PR & CI confirmation +- Open PR; confirm `semgrep.yml` actually triggers on the PR event, pulls the pinned image successfully, produces a SARIF upload visible under the repo's Security โ†’ Code scanning alerts (filtered by tool "Semgrep"), and that the hard-fail gate step correctly reflects the current repo's Semgrep cleanliness (expected: green, since this is the same ruleset the repo already passes locally today). +- If CI surfaces findings the local run didn't (e.g. a stale local semgrep binary/ruleset that had drifted below the pinned CI version), that is itself the feature working as intended โ€” resolve findings on their merits, not by weakening the pin. + +--- + +## 5. Acceptance Criteria + +1. `.github/workflows/semgrep.yml` exists, triggers on `pull_request`/`push` to `[main, nightly, development]`, `workflow_dispatch`, and a weekly `schedule`. +2. Semgrep runs inside a `container:` whose `image:` is the pinned string `semgrep/semgrep:@sha256:` inlined directly (no `env:` indirection) โ€” no floating tag, no `@latest`. +3. `permissions:` is declared identically at both the workflow (top) level and the job level. +4. `scripts/pre-commit-hooks/semgrep-scan.sh` carries exactly the additive `SEMGREP_SARIF_OUTPUT` change described in ยง3.0 โ€” its binary/version-resolution logic and its `--config`/`--exclude`/`--exclude-rule` values are otherwise unchanged, and every existing call site's behavior (`SEMGREP_SARIF_OUTPUT` unset) is byte-identical to pre-change. +5. `.github/workflows/semgrep.yml` invokes `scripts/pre-commit-hooks/semgrep-scan.sh` directly for **both** the SARIF-producing pass (with `SEMGREP_SARIF_OUTPUT` set) and the hard-fail gate pass (unset) โ€” no independent/duplicated `semgrep scan ...` invocation exists anywhere in the workflow YAML. +6. `scripts/ci/check-semgrep-parity.sh` exists, passes against the merged state, and is invoked as a CI step in `semgrep.yml` before the scan runs. +7. SARIF results upload to the GitHub Security tab under category `semgrep`, using the same `github/codeql-action/upload-sarif` SHA pin already used in `security-pr.yml`, gated on an explicit SARIF-existence check step. +8. `SECURITY.md`'s `### Automated Scanning` table and `### Scanning Workflows` section mention Semgrep and `semgrep.yml`. +9. `ARCHITECTURE.md` mentions Semgrep at all three existing security-scanning call-out locations (tech-stack table, CI Jobs list, Container Scanning components list). +10. `.gitignore`, `.dockerignore`, `.codecov.yml`, and all Dockerfiles are confirmed unchanged (per ยง2.9 โ€” no diff expected in this PR). +11. `actionlint`, `markdownlint`, `shellcheck`, and `lefthook run pre-commit` all pass locally on the final diff. +12. `docs/plans/current_spec.md` (this file) reflects the implemented state โ€” no divergence between plan and shipped workflow at PR time (devops agent should update this file if implementation deviates from any spec section above, per standard plan-fidelity practice). + +--- + +## 6. Commit Slicing Strategy + +**Decision:** single PR, one feature ("Semgrep CI coverage"), ordered logical commits. No cross-PR splitting per `CLAUDE.md`'s "One Feature = One PR" rule โ€” this is a small, cohesive, CI-only change (plus one additive shell-script hook); splitting it further would violate that rule for no benefit (there's no independently-shippable sub-feature here โ€” a workflow with no parity guard, or a parity guard with no workflow, are both incomplete on their own). Approved as-is by Supervisor round 2 โ€” shape unchanged, contents updated below for ยง2.7's design revision and the ARCHITECTURE.md addition. + +### Commit 1 โ€” Script hook + parity guard foundation +- **Scope:** Additive-only, no behavior change for any existing call site. Adds the `SEMGREP_SARIF_OUTPUT` hook to `semgrep-scan.sh`, the new (as-yet-unused-by-CI) parity script, and optionally the shared helper extraction. +- **Files:** `scripts/pre-commit-hooks/semgrep-scan.sh` (modified โ€” additive only, per ยง3.0), `scripts/ci/check-semgrep-parity.sh` (new โ€” will fail if run now, since `semgrep.yml` doesn't exist yet; not wired into any workflow in this commit), `scripts/ci/lib/workflow-yaml-asserts.sh` (new, optional) and `scripts/ci/check-codeql-parity.sh` (refactored to source it, optional, no behavioral change) if the extraction from ยง3.5 is included. +- **Dependencies:** none. +- **Validation gate:** `shellcheck` on all touched/new scripts; local `lefthook run pre-commit` / `lefthook run security-full` on a sample file confirms `semgrep-scan.sh`'s default behavior is unchanged; `bash scripts/ci/check-codeql-parity.sh` passes unchanged if the refactor is included (regression check). +- **Commit message:** `feat(security): add opt-in SARIF output mode to local Semgrep script and add CI parity guard` + +### Commit 2 โ€” Semgrep CI workflow +- **Scope:** Adds the new workflow file, delegating both its SARIF and gate passes to the script from Commit 1, and wires the parity guard from Commit 1 into it. +- **Files:** `.github/workflows/semgrep.yml` (new). +- **Dependencies:** Commit 1 (the `SEMGREP_SARIF_OUTPUT` hook and the parity guard must exist for this workflow to reference real, working behavior). +- **Validation gate:** `actionlint .github/workflows/semgrep.yml`; `bash scripts/ci/check-semgrep-parity.sh` now passes (workflow file exists, delegates correctly, image pin format valid); `lefthook run pre-commit` clean on the diff. +- **Commit message:** `feat(security): add pinned Semgrep SAST scan to CI, mirroring local pre-commit/pre-push scan` + +### Commit 3 โ€” Documentation +- **Scope:** `SECURITY.md` and `ARCHITECTURE.md` updates only, per ยง4 Phase 3. +- **Files:** `SECURITY.md`, `ARCHITECTURE.md`. +- **Dependencies:** Commit 2 (documents the workflow file that now exists). +- **Validation gate:** `markdownlint SECURITY.md ARCHITECTURE.md`. +- **Commit message:** `docs: document Semgrep CI scan in SECURITY.md and ARCHITECTURE.md` + +### Commit 4 โ€” Hardening / fixups (conditional) +- **Scope:** Only if Phase 5 (opening the PR and observing the first real workflow run) surfaces something unfixable purely by inspection โ€” e.g. the digest needs re-resolution, `actionlint`/a GitHub Actions schema quirk requires a syntax adjustment not visible from local linting alone, or the container's default shell needs an explicit `shell: bash` on a step. +- **Files:** `.github/workflows/semgrep.yml` and/or `scripts/ci/check-semgrep-parity.sh`, as needed. +- **Dependencies:** Commits 1-3, plus one observed CI run on the PR. +- **Validation gate:** the actual GitHub Actions run on the PR going green. +- **Commit message:** `fix: address Semgrep CI workflow issues found in first live run` (only created if needed โ€” do not pre-author an empty placeholder commit). + +### Rollback / contingency + +- **Rollback:** revert the PR's merge commit. The change is additive-only (new files + one additive, backward-compatible shell-script hook + documentation sections); reverting it removes Semgrep CI coverage cleanly with no residual state โ€” no DB migration, no data written, no schema changed. `git revert -m 1 ` is sufficient. +- **Contingency โ€” pinned image becomes unpullable mid-development-cycle (e.g. registry outage, Docker Hub rate limiting on `ubuntu-latest` runners):** the job fails visibly (container pull failure is unambiguous in the Actions log, distinct from a scan failure), does not block other workflows (independent job, independent file), and does not gate merges any more strictly than any other required-check outage would โ€” same failure mode and same operational response as a transient CodeQL or Trivy Action outage today. +- **Contingency โ€” CI Semgrep surfaces findings that don't reproduce locally:** expected and desired (ยง4 Phase 5) โ€” indicates local environment drift, not a CI bug. Do not suppress via `--exclude-rule` additions without documenting rationale (matching the existing precedent set by the one documented `gorm-empty-password` exclusion already in the script). +- **Contingency โ€” parity guard is judged too strict/noisy after landing** (e.g. flags legitimate divergence that's actually fine): tune the specific assertion in `check-semgrep-parity.sh`, don't delete the guard wholesale โ€” same operating principle already established for `check-codeql-parity.sh`, which has been iterated on rather than removed. diff --git a/docs/plans/current_spec.md b/docs/plans/current_spec.md index 3e74a26d9..3bea26e97 100644 --- a/docs/plans/current_spec.md +++ b/docs/plans/current_spec.md @@ -1,481 +1,446 @@ -# Issue #619 โ€” Phase 3 Technical Debt: Test Infrastructure Cleanup - -Status: Planning complete, pending supervisor review. -Branch: `test/issue-619-test-infra-debt` (tip of `development`, working tree clean at plan time). -PR base branch: **`development`** (per `gh pr list` convention โ€” `main` only receives weekly `nightly` promotion merges via merge commit; this is a normal feature PR). -Closes: `#619` ("Phase 3 Technical Debt Issues" โ€” bundles 5 sub-issues, verified below). - --- - -## 1. Introduction - -### 1.1 Objective - -Close out GitHub issue #619 with a single feature PR that: - -1. Un-skips 5 confirmed-stale Vitest suites blocked on a long-fixed `undici`/jsdom WebSocket bug (sub-issue 1), resolves the 6th related skip with a root-cause-appropriate fix (not a blind unskip), and proves no regressions via a full frontend suite run. -2. Replaces 59 tautological (`expect(x || true).toBeTruthy()`-shaped) Playwright assertions across 11 E2E spec files with real, deterministic assertions or explicit `test.skip()` calls with accurate reasons (sub-issue 2) โ€” the bulk of this PR's work. -3. Confirms backend coverage for `internal/services` and the relocated `backend/pkg/dnsprovider/builtin` package remains healthy with no code changes required (sub-issue 3). -4. Confirms the feature-flag async propagation flakiness was already resolved via `waitForFeatureFlagPropagation()` in the reorganized spec file, with no code changes required (sub-issue 4). -5. Confirms WebKit E2E test discovery/config is healthy and schedules the outstanding full WebKit run as a Definition-of-Done gate (sub-issue 5). - -### 1.2 Why one PR - -Per `CLAUDE.md` "Commit Slicing & PR Strategy" and repo memory (`feedback_one_feature_one_pr.md`): issue #619 is one feature (test-infrastructure debt), closed by one PR with ordered commits. Sub-issues 3 and 4 require **no code changes** โ€” they contribute verification evidence to the PR's DoD run and the closing PR description, not separate commits. - -### 1.3 Non-goals - -- No production code changes (backend or frontend application code). This PR touches only test files, test infrastructure, and documentation. -- No changes to `.gitignore`, `.dockerignore`, `codecov.yml`, or any `Dockerfile` โ€” reviewed explicitly in ยง3.6, all confirmed already correct for this change (see findings). -- `CrowdSecBouncerKeyDisplay.test.tsx` (4 `it.skip` at lines 205/209/213/219, unrelated clipboard-API mock issue) is explicitly **out of scope** and must not be touched. - +goal: Migrate Charon's release/versioning pipeline from custom auto-tagging (paulhatch/semantic-version + GoReleaser) to release-please +version: 1.0 +date_created: 2026-08-17 +status: 'Planned' +tags: [chore, infrastructure, migration, ci-cd] --- -## 2. Research Findings โ€” Ground Truth Verification (2026-08-07) +# Introduction -All findings below were re-verified directly against the current working tree (branch `test/issue-619-test-infra-debt`, tip of `development`) โ€” greps, file reads, and non-mutating test/coverage runs. Numbers in the original issue text and the prior same-day investigation summary are corrected where they drifted. +![Status: Planned](https://img.shields.io/badge/status-Planned-blue) -### 2.1 Sub-issue 1 โ€” undici/WebSocket jsdom blocker: CONFIRMED STALE, ACTION REQUIRED +Charon's release pipeline currently computes the next semantic version with `paulhatch/semantic-version` inside `.github/workflows/auto-versioning.yml`, then hand-rolls a changelog body with shell/grep and publishes a GitHub Release via `softprops/action-gh-release`. A second workflow, `.github/workflows/release-goreleaser.yml`, listens for the resulting `v*` tag push and runs GoReleaser โ€” but this workflow has never once succeeded (every run since `v0.3.0` fails at an "Enforce PR-2 release promotion guard" step because the gating repo variable was never set), so GoReleaser has never built or published any binary/archive/deb/rpm asset for this repo. -Dependency state confirmed via `npm ls`: -- `jsdom@30.0.1` (root + deduped under `vitest@4.1.10`) -- `undici@8.10.0` (transitive, via jsdom only) +This plan replaces both workflows with `googleapis/release-please-action`, following the pattern already validated in the sibling project `/projects/go_notify_yourself`. Release-please computes versions from Conventional Commits by walking real git history, opens/maintains a standing "release PR," and tags + publishes the GitHub Release itself when that PR is merged โ€” eliminating the hand-rolled changelog script and the custom semver-calculation action in one move. -The upstream bug this blocker cited (`nodejs/undici#1671`, WebSocket mock `InvalidArgumentError`) is long fixed at this version pair. +This is a standalone, CI/CD-configuration-only chore. It touches no Go or TypeScript application code, no database schema, no API surface, and is entirely unrelated to the in-progress `feature/notifications-engine-extraction` work. It targets a **new branch cut from `main`**, not `development` โ€” see [Branching Note](#branching-note-deviation-from-normal-development-first-flow) below for why. -**Confirmed skip inventory** (exact, re-counted against source, not the prior summary): +**Recommended branch name**: `chore/release-please-migration` (from `main`). -| File | Skip marker | Test count (verified via grep) | -|---|---|---| -| `frontend/src/pages/__tests__/Security.test.tsx:35` | `describe.skip('Security', ...)`, comment `// BLOCKER 3: Temporarily skipped due to undici InvalidArgumentError in WebSocket mocks` | 22 | -| `frontend/src/pages/__tests__/Security.audit.test.tsx:52` | `describe.skip('Security Page - QA Security Audit', ...)` | 18 | -| `frontend/src/pages/__tests__/Security.errors.test.tsx:68` | `describe.skip('Security Error Handling Tests', ...)` | 13 | -| `frontend/src/pages/__tests__/Security.loading.test.tsx:59` | `describe.skip('Security Loading Overlay Tests', ...)` | 12 | -| `frontend/src/pages/__tests__/Security.dashboard.test.tsx:67` | `describe.skip('Security Dashboard - Card Status Tests', ...)` | 18 | - -Subtotal: **83 tests** across 5 files. Sum matches exactly. - -**The 6th file โ€” `Security.functional.test.tsx:680`, `it.skip('should open notification settings modal when button is clicked', ...)`, comment `// Skip: Modal component uses WebSocket connections internally`:** +**Merge strategy for this PR itself**: `chore/release-please-migration` โ†’ `main` should be merged via **squash merge** (the repo's default for ordinary feature/chore PRs; the "merge commit only" rule in CLAUDE.md applies specifically and only to the weekly `nightly` โ†’ `main` promotion PR โ€” see Decision 3). Squashing this PR's six commits into one `chore:`-prefixed commit on `main` is fine either way for release-please's own purposes, since none of this PR's commits are themselves releasable (`feat:`/`fix:`) and release-please's manifest is being explicitly seeded rather than derived from this PR's commit history. -This comment is **inaccurate**, and unskipping as-is would produce a real (non-WebSocket) failure. Root-cause trace performed per `CLAUDE.md`'s Root Cause Analysis Protocol: +## User Decisions Required Before Implementation -- `frontend/src/pages/Security.tsx:297-303` โ€” the "Notifications" header button's `onClick` is `() => navigate('/settings/notifications')`. It is a **React Router navigation**, not a modal. There is no `role="dialog"` anywhere in `Security.tsx`. -- `Security.functional.test.tsx:20-27` mocks `useNavigate` (`mockNavigate = vi.hoisted(() => vi.fn())`) โ€” the file's own test harness already expects navigation, not a modal, elsewhere. -- **The correct test already exists in the same file**, passing, uncontested: `Security.functional.test.tsx:452-464`, `it('should navigate to notifications settings when Notifications button is clicked', ...)`, which asserts `expect(mockNavigate).toHaveBeenCalledWith('/settings/notifications')`. +Two behavior changes were load-bearing enough that they should not be discovered after the fact โ€” surfaced here for explicit sign-off before implementation starts, in addition to their mentions later in Edge Cases / Manual Post-Merge Follow-Ups. **Both have since been resolved by explicit user sign-off; recorded below for the record.** -Conclusion: the skipped test at line ~680 is **dead, stale test code** describing UI behavior (a modal) that was replaced by a navigation at some prior refactor, and the replacement behavior already has full, correct, passing coverage elsewhere in the same file. Per `CLAUDE.md` "CLEAN: Delete dead code immediately," the correct fix is **deletion of the stale `it.skip` block** (the `describe('Notification Settings Modal', ...)` wrapper at line ~677 becomes empty and should be removed with it), not an unskip and not a comment-only edit. This is a stronger, more correct resolution than either option the investigation brief offered, and it should be called out explicitly in the PR description as the resolution for this file. +1. **Release cadence changes from fully-automatic to manually-gated.** Today, any non-`chore:` push to `main` is *immediately* followed by `auto-versioning.yml` cutting a tag and publishing a GitHub Release โ€” no human action required. Under release-please, a push to `main` only updates a *standing draft PR*; nothing ships (no tag, no Release, no `orthrus-build.yml` trigger) until a human explicitly merges that PR. (Release-please does support auto-merging its own PR via a label + a second small workflow, but this plan does not implement that โ€” see Manual Post-Merge Follow-Up #7 โ€” so the default behavior after this migration is manual-gate-by-default.) **RESOLVED โ€” user sign-off: APPROVED, proceed as planned (manual-gate-by-default, no auto-merge configured in this PR).** +2. **Chore-only pushes no longer cut a release**, and **`perf:` commits stop being patch-worthy.** Today, a week where only `chore:`/`docs:`/`test:`/`ci:`/`build:`/`style:`/`refactor:` commits land on `main` still gets a patch-bumped release (since `auto-versioning.yml` treats "anything that isn't `feat:`" as patch-worthy, and its changelog-categorization step explicitly buckets `perf:` alongside `fix:`). Under release-please's default releasable-type set (`feat`, `fix`, `deps`), a chore-only period produces **no** release PR at all, and `perf:` commits โ€” not in that default set โ€” likewise stop triggering a release/changelog entry on their own. **RESOLVED โ€” user sign-off: ACCEPT release-please's defaults as-is. Do not add `perf:` (or any other type) to `changelog-sections`/releasable types as a special-case override; `perf:` commits behave like `chore:`/`docs:`/`test:` under release-please's out-of-the-box config** โ€” no config change proposed for this. This is treated as an accepted, disclosed behavior change (arguably a correctness improvement โ€” no more empty "dependency updates and maintenance" releases), not an oversight. -**Current full-suite baseline** (`npx vitest run --coverage=false`, non-mutating, run to completion โ€” 635s): - -``` -Test Files 263 passed | 5 skipped (268) - Tests 3247 passed | 88 skipped | 2 todo (3337) -``` +## Branching Note: Deviation from Normal Development-First Flow -88 skipped = 83 (sub-issue-1, in scope) + 1 (`Security.functional.test.tsx` notification-modal test, in scope, to be deleted not unskipped) + 4 (`CrowdSecBouncerKeyDisplay.test.tsx`, confirmed out of scope). Arithmetic reconciles exactly โ€” no other undici/WebSocket-flavored skips exist anywhere else in `frontend/src` (verified via repo-wide grep for `undici`, `BLOCKER 3`, `WebSocket connections internally`). +The repo's normal convention (see the Semgrep CI plan previously in this file, and the "Merge Soak Before Main" project memory) is feature branches off `development`, PR into `development`, then a weekly `nightly` โ†’ `main` promotion carries validated work to `main` roughly a week later. This plan deliberately breaks that pattern: `auto-versioning.yml`, `release-goreleaser.yml`, and the new `release-please.yml` all trigger specifically on `main` (`workflow_run` on the main-branch Docker build, or `push: branches: [main]`), and the live release state (existing `v*` tags, the `CHARON_PR2_GATES_PASSED` variable, the next real release cut) only exists on `main`. A `development`-first path would leave the repo running two parallel, half-migrated release mechanisms for up to a week, which is worse than a direct, carefully-gated `main` PR. **Recommendation for the user**: apply extra manual review scrutiny before merging this PR, since it bypasses the usual nightly soak period by design, not by oversight. -### 2.2 Sub-issue 2 โ€” Weak/tautological E2E assertions: CONFIRMED, LARGER THAN ORIGINAL ISSUE TEXT, MAJORITY REQUIRE REAL FIXES +# Research Findings -Pattern searched: literal `|| true` immediately preceding `.toBeTruthy()` in `tests/**/*.spec.ts` (the actual pattern in this repo โ€” confirmed not a generic `x` placeholder). Exact current count: **59 occurrences across 11 files**, matching the prior investigation's file list and the prior day's rough counts almost exactly (one file's estimate, `system-settings-feature-toggles.spec.ts`, is 1, not the previously-noted range โ€” reconfirmed by direct grep): +## Existing Architecture Summary -| File | Count | Lines | +| File | Role | Status found | |---|---|---| -| `tests/security-enforcement/zzz-security-ui/access-lists-crud.spec.ts` | 13 | 156, 200, 339, 465, 545, 623, 825, 845, 862, 949, 985, 1018, 1061 | -| `tests/core/certificates.spec.ts` | 9 | 204, 229, 718, 759, 1037, 1052, 1118, 1146, 1158 | -| `tests/security-enforcement/zzz-security-ui/encryption-management.spec.ts` | 8 | 188, 314, 393, 498, 596, 601, 684, 708 | -| `tests/core/proxy-hosts.spec.ts` | 8 | 209, 255, 461, 523, 547, 643, 969, 1014 | -| `tests/security-enforcement/zzz-security-ui/system-security-settings.spec.ts` | 7 | 290, 325, 352, 452, 555, 680, 736 | -| `tests/core/navigation.spec.ts` | 4 | 238, 559, 733, 758 | -| `tests/settings/smtp-settings.spec.ts` | 4 | 121, 165, 231, 906 | -| `tests/core/dashboard.spec.ts` | 3 | 232, 370, 491 | -| `tests/settings/account-settings.spec.ts` | 1 | 875 | -| `tests/security/system-settings-feature-toggles.spec.ts` | 1 | 317 | -| `tests/manual-dns-provider.spec.ts` | 1 | 311 | -| **Total** | **59** | | - -**Decision framework applied to every occurrence** (per task instructions): - -- **(a) Real conditional assertion** โ€” used when the test's own name or an adjacent comment already states a definite, deterministic expectation ("should show X", "X should appear") that the app can be made to satisfy reliably. Mechanical sub-case: when the expression already has a *real* multi-condition OR (e.g. `hasX || hasY || true`), the fix is simply dropping the trailing `|| true` โ€” the meaningful disjunction underneath is preserved. -- **(b) Explicit `test.skip()` / early return with accurate comment** โ€” used only where the underlying condition is genuinely environment- or timing-dependent (cross-browser keyboard focus order, network-dependent external reachability *content* โ€” as opposed to "some feedback appeared," which is still deterministic, race conditions in animation/skeleton timing). This repo already has an established, correct convention for this โ€” `tests/proxy-host-drag-drop.spec.ts` (19 call sites) and `tests/certificate-delete.spec.ts` / `tests/certificate-bulk-delete.spec.ts` (1 each) all use `test.skip(true, '')` mid-test when a precondition isn't met. **Reuse this exact convention** โ€” do not invent a new pattern. -- **(dead code) Delete** โ€” used when a hard `expect(...).toBeVisible()` (or equivalent) already precedes the tautological line for the *same* condition, making the soft check unreachable/redundant. - -Classification results by file (full per-line detail for implementers; "(a)", "(b)", "(dead)" tags below are the required fix per line): - -#### `tests/core/certificates.spec.ts` (9 `|| true` occurrences, plus 1 additional vacuous test with no tautology to grep for) โ€” includes the 3 originally-named tests plus 2 more sharing the same defect - -- **L204** `hasSortIcon || true` โ€” comment above states "Sort icon should appear" as a definite requirement โ†’ **(a)**: `expect(hasSortIcon).toBe(true)`. -- **L229** `hasAlert || true`, test `'should show SSL info alert'` โ†’ **(a)**: test name itself is the requirement. -- **L718** `hasDelete || true`, test `'should show delete button for staging certificates'` โ†’ **(a)**. -- **L759** `hasToast || true`, test `'should warn if certificate is in use by proxy host'` โ†’ **(a), root-cause fix required, see below.** -- **L1037** `hasSslColumn || true` โ€” comment: "SSL column *may* show certificate info" โ†’ checked against the source (`frontend/src/pages/ProxyHosts.tsx:556-557`): the SSL column (`key: 'ssl', header: t('proxyHosts.columnSSL')`) is a **static column definition**, not conditional per-row/per-feature-flag โ€” unlike proxy-hosts.spec.ts L643's `hasWs`/`hasAcl` (which genuinely vary per host's configuration and need a seeded host to be deterministic), this column header renders unconditionally whenever the table itself renders. โ†’ **(a), and simpler than L643**: no seeding needed โ€” the `hasTable` check already above this line guarantees the table is rendered, so `expect(hasSslColumn).toBe(true)` is deterministic as-is; verify at implementation time that no feature flag gates the column before finalizing. -- **L1052** `hasHeading || true` โ€” a **hard** `await expect(heading).toBeVisible({ timeout: 10000 })` already executes immediately above this line for the identical locator โ†’ **(dead)**: delete the redundant soft-check (3 lines). -- **L1118** `hasError || true`, test `'should show error message on API failure'` โ€” **root-cause issue**: the test never injects a failure (no `page.route(...)` interception forcing a 4xx/5xx). It cannot show an error message because no error is ever induced. โ†’ **(a) with expanded scope**: add a `page.route('**/api/v1/certificates', route => route.fulfill({ status: 500, ... }))` (or equivalent, matching the mocking convention used elsewhere in this same file's "Error Handling" section if one exists โ€” verify at implementation time) before navigation, then assert the error message is real and visible. This is not a one-line fix; note it in the commit as a slightly larger item. -- **L1146** `hasDescription || true`, test `'should have PageShell with title and description'` โ†’ **(a)**. -- **L1158** `hasIcon || true` โ€” comment: "Button should have Plus icon" โ†’ **(a)**. - -**The 3 named tests, plus 2 more sharing the identical defect, in detail** (backend root-cause traced via `backend/internal/api/handlers/certificate_handler.go:387-470`, `CertificateHandler.Delete`): - -Note: a 5th test in the same `describe` block, `'should show config reload overlay during deletion'` (~L806-821), was not caught by the initial `|| true` grep sweep because it doesn't end in a tautology โ€” it uses the identical broken `page.once('dialog', dialog => dialog.accept())` pattern against the same non-existent native dialog, then only does `await waitForDebounce(page)` with **no assertion at all** afterward. It is just as vacuous as the other four and requires the identical interaction-model fix, so it is grouped with them below (item 5) and included in the same commit. - -Critical finding: **the delete UI does not use a native `window.confirm()` dialog.** `frontend/src/components/dialogs/DeleteCertificateDialog.tsx` is a fully custom React modal (uses the shared `Dialog`/`DialogContent`/`DialogFooter` primitives, `Button` components with `onClick={onCancel}` / `onClick={onConfirm}`, translated via i18n keys `certificates.deleteTitle`/`deleteConfirmCustom`/`deleteButton`/`common.cancel`). It contains **no `confirm()` call and no "backup" text is guaranteed** โ€” the backup-mentioning copy (`certificates.deleteConfirmCustom`: *"This will permanently delete this certificate. A backup will be created first."*, `frontend/src/locales/en/translation.json:234`) is used **only** when `getWarningKey()` falls through to the default case (i.e. the certificate is not `expired`, not `expiring`, and not `letsencrypt-staging`); the other 3 status-specific messages (`deleteConfirmStaging`, `deleteConfirmExpired`, `deleteConfirmExpiring`) never mention backups at all. - -All five existing tests (`'should show delete confirmation dialog'` L723, `'should warn if certificate is in use by proxy host'` L741, `'should cancel delete when confirmation dismissed'` L764, `'should create backup before deletion'` L788, `'should show config reload overlay during deletion'` L806) currently drive the flow via `page.once('dialog', ...)` โ€” Playwright's **native browser dialog** handler. Since the app never opens a native dialog for this flow, **that handler callback never fires**; the tests currently click the delete button (opening the *custom* modal, which is left dangling/unclosed) and then either do nothing further or check a `hasX || true` that trivially passes. These tests currently exercise almost none of the real deletion flow. This is a larger, root-cause-level fix, not a one-line assertion swap: - -1. **`'should show delete confirmation dialog'` (L723, not currently `|| true` but must be fixed alongside the others for the block to work at all)**: replace `page.once('dialog', ...)` with locating the actual custom modal (`page.getByRole('dialog')` from the shared `Dialog` primitive โ€” verify exact role/testid in `frontend/src/components/ui/Dialog.tsx` at implementation time) and asserting its title (`t('certificates.deleteTitle')` โ†’ "Delete Certificate") and Cancel/Delete buttons are visible. -2. **`'should warn if certificate is in use by proxy host'` (L741/L759)**: backend `Delete()` returns `409 {"error": "certificate is in use by one or more proxy hosts"}` **before** any backup is attempted, when `IsCertificateInUse`/`IsCertificateInUseByUUID` is true. Real fix: select (or seed via API, matching this file's existing seeding convention) a certificate that is actually attached to a proxy host, click delete, click the custom modal's Confirm button, and assert a real error toast/message appears (matching the app's toast convention โ€” `sonner`/`[role="alert"]`, consistent with other files in this PR) rather than the current always-true check. Do not rely on "whichever cert happens to be first in the table." -3. **`'should cancel delete when confirmation dismissed'` (L764)**: replace `page.once('dialog', dialog => dialog.dismiss())` with clicking the custom modal's **Cancel** button. The existing row-count check (`rowsBefore === rowsAfter`) is real and should be **kept**, but per the task's explicit instruction, **add a backend-state assertion**: `GET /api/v1/certificates/{id}` (via `getCertificateViaAPI` from `tests/utils/api-helpers.ts`, the file's already-established API-verification helper โ€” reuse it, do not invent a new one) returns `200` and the certificate is still present, proving cancellation didn't merely hide a row client-side. -4. **`'should create backup before deletion'` (L788, currently checks `dialog.message()` contains "backup" via a handler that never fires โ€” the current implementation is not even a tautology, it is dead/vacuous)**: backend confirms `CreateBackup()` (via `BackupServiceInterface`) is called synchronously in the `Delete` handler, for a certificate that is **not** in use, before the delete completes. Correct fix: capture the backup list via `GET /api/v1/backups` (same endpoint mocked/used in `tests/tasks/backups-create.spec.ts`; no existing typed helper for it in `tests/utils/api-helpers.ts` โ€” add one, `getBackupsViaAPI`, following the exact pattern of the file's other `get*ViaAPI` functions) **before** the delete, click Confirm on the custom modal for a certificate guaranteed not in use, wait for the delete to complete, then `GET /api/v1/backups` again and assert a new backup entry exists (by count increase and/or a `created_at`/filename close to "now"). Do **not** assert on dialog text โ€” the text does not reliably mention "backup" depending on certificate status, as shown above. -5. **`'should show config reload overlay during deletion'` (~L806-821, currently `page.once('dialog', dialog => dialog.accept())` then only `await waitForDebounce(page)` โ€” no assertion at all, silently vacuous)**: replace with clicking the custom modal's **Confirm/Delete** button for a certificate guaranteed not in use, then assert the actual loading/config-reload overlay is real: locate it the same way `tests/security/system-settings-feature-toggles.spec.ts:317`'s `overlayVisible` check does (`.fixed.inset-0.z-50` / `[data-testid="config-reload-overlay"]` โ€” reuse that locator convention rather than inventing a new one) and assert it becomes visible during the delete request and then resolves/disappears once the request completes, rather than the current no-op. +| `.github/workflows/auto-versioning.yml` | Computes next semver via `paulhatch/semantic-version@v6.0.3` from `workflow_run` of "Docker Build, Publish & Test" on `main`; hand-greps `feat:`/`fix:`/`perf:` commit-body bullets into a release note; creates tag + GitHub Release via `softprops/action-gh-release@v3`. | Working, but hand-rolled and duplicative of what release-please does natively. | +| `.github/workflows/release-goreleaser.yml` | Triggers on `push: tags: ['v*']`; runs `goreleaser release --clean` (builds linux amd64/arm64 binary, tar.gz archive, deb/rpm via nfpm). | **Never succeeded.** Every run since `v0.3.0` fails at "Enforce PR-2 release promotion guard" (repo variable `CHARON_PR2_GATES_PASSED` has never been set โ€” confirmed via `gh api repos/Wikid82/charon/actions/workflows/release-goreleaser.yml/runs`, `REPO_VARS_JSON: {}` on every run). `gh release view` on `v0.36.3`โ€“`v0.36.5` shows zero attached assets. | +| `.goreleaser.yaml` | GoReleaser config: builds linux/amd64+arm64 binary, tar.gz archive, deb/rpm packages, changelog section. Its own header comment: *"used exclusively for changelog generation... builds/archives/nfpms kept for potential future use but not currently utilized."* | Dead weight โ€” see Decision 5. | +| `.github/workflows/docker-build.yml` | Builds/publishes the actual Charon Docker images. Triggers: `pull_request`, `push: branches: [main, development]`, `workflow_dispatch`, `workflow_run` (Docker Lint). **Does NOT trigger on tag push** (`on:` block has no `tags:` key โ€” confirmed by reading the full trigger block). | This is the sole real distribution channel for Charon (100% Docker). Its `docker/metadata-action` step (line ~334-345) includes `type=semver,pattern={{version}}` tag patterns, but since the workflow never runs on a tag ref, `TRIGGER_REF` is always `refs/heads/*` and those semver patterns never actually resolve to anything โ€” they are inert/vestigial in the current design, not a live tag consumer. | +| `.github/workflows/orthrus-build.yml` | Builds the separate Orthrus agent image. Triggers: `push: branches: [main, development], tags: ['v*']`. Its `docker/metadata-action` step (`type=semver,pattern={{version}}` etc.) **does** fire off tag pushes. | **This is the one real, live downstream consumer of the `v*` tag** that this migration must not break โ€” release-please's created tag must still be a bare `v` (see Decision 1 / tag-naming risk below) for this workflow's `tags: ['v*']` trigger and semver Docker-tag derivation to keep working identically. | +| `.github/workflows/auto-changelog.yml` ("Auto Changelog (Release Drafter)") | Triggers on `workflow_run` (Docker Build success on `main`) and `release: types: [published]`; runs `release-drafter/release-drafter@v7` against `.github/release-drafter.yml`. | **Not mentioned in the task brief but discovered during research โ€” see "Additional Finding" below.** Redundant with release-please's own standing-PR/changelog mechanism; its own tag-template (`v$NEXT_PATCH_VERSION`) always increments patch regardless of PR label, so it's already partially broken (a PR labeled `feature` would still only bump patch). | +| `.github/release-drafter.yml` | release-drafter config: label-based categorization (`feature`/`feat`, `bug`/`fix`, `chore`, `test`), `tag-template: 'v$NEXT_PATCH_VERSION'`. | Retire alongside `auto-changelog.yml` โ€” see Additional Finding. | +| `VERSION.md` | Documents the "canonical" release process. References `.version` as optional/non-canonical (still accurate). Also references a "release-drafter workflow" for changelog generation โ€” **this turned out to be real** (`auto-changelog.yml`), not stale as originally suspected; the doc's inaccuracy is instead that it describes `release-goreleaser.yml`/`docker-build.yml` as if they jointly "build and publish release artifacts/images through CI" from the tag, which is false per the findings above. | Needs a full rewrite (Phase 5 / Commit 6). | +| `.version` | Currently `v0.27.0` (stale โ€” real latest tag is `v0.36.5`). Already documented in `VERSION.md` as "optional... not the canonical release trigger." | Recommend removal โ€” see Decision 4. | +| `scripts/check-version-match-tag.sh` | Compares `.version` to the latest git tag; **already self-deprecating** โ€” prints a warning telling callers to use `.github/skills/scripts/skill-runner.sh utility-version-check` instead, then runs its own logic anyway. `.github/skills/utility-version-check-scripts/run.sh` just `exec`s this same script โ€” the "migration" to the skill runner is circular and never actually happened. | Recommend removal โ€” see Decision 4. | +| `lefthook.yml` (line ~99) | `check-version-match: { glob: ".version", run: "bash scripts/check-version-match-tag.sh" }` | Remove this hook entry alongside `.version` deletion (Commit 5). | +| `backend/internal/version/version.go` | `Version`, `BuildTime`, `GitCommit` vars, defaulted to `"dev"`/`"unknown"`, set via `-ldflags -X ...` at build time. No in-repo manifest file to bump โ€” confirms release-please needs a manifest-less strategy (Decision 1). | Untouched by this migration. | +| `Dockerfile` (lines ~250-286) | Injects `VERSION`/`GIT_COMMIT`/`BUILD_DATE` into the Go binary via `-ldflags -X github.com/Wikid82/charon/backend/internal/version.*=...`, **identically** to what `.goreleaser.yaml`'s `builds.ldflags` does. Also sets `VITE_APP_VERSION` for the frontend build (line ~133-134). | Confirms version injection already happens independently of GoReleaser in the real (Docker) build path โ€” removing GoReleaser does not touch actual version injection at all. | +| `scripts/generate-changelog.sh` | Regenerates `backend/internal/changelog/data/changelog.json` (the in-app "What's New" popup's data source, `go:embed`-ed at build time) by walking `git tag -l 'v*' --sort=v:refname` and categorizing commit subjects between each pair of tags via conventional-commit regex (feat/fix/security-scoped/other). **Depends only on real `v*` tags existing in git history** โ€” it does not read `auto-versioning.yml`'s or `release-goreleaser.yml`'s output, the GitHub Releases API, or any state those workflows produce. | **Fully decoupled from this migration** as long as release-please still creates real, plain-`v*`-prefixed git tags (Decision 1's tag-naming requirement is what makes this true). Called from `nightly-build.yml` (line 225) and the dead `release-goreleaser.yml` (line 77) โ€” **not** from `docker-build.yml`, meaning stable/`latest`-tagged production images currently always ship the placeholder `[]` changelog while nightly images get real data. This is a **pre-existing gap in `docker-build.yml`, out of scope for this CI/CD-versioning-only migration**, flagged here only so it isn't mistaken for something this migration caused or should fix. | +| `backend/internal/services/update_service.go` (line 40) | Self-update checker hits `https://api.github.com/repos/Wikid82/charon/releases/latest` for `tag_name`/`html_url` only โ€” does not need release assets. | Unaffected: release-please still publishes a `releases/latest`-eligible GitHub Release with those fields. | +| `backend/internal/services/orthrus_service.go` (line ~222) | Orthrus agent "Tarball" install method's `curl` command points at `.../releases/latest/download/charon-agent-linux-amd64.tar.gz`. | **This asset has never existed** (GoReleaser never ran successfully, and even if it had, `.goreleaser.yaml` builds `charon`, not `charon-agent`). This is a pre-existing, already-broken feature, unrelated to and unmade-worse by this migration โ€” flagged for the user's awareness only; fixing it is out of scope (would require actually building/publishing an agent binary, a feature-level change). | +| `CHANGELOG.md` (repo root, 600 lines) | Hand-curated, Keep-a-Changelog-format, rich multi-line entries with issue references. Not written by any current CI workflow. | **Conflicts with release-please's default "go" strategy**, which manages `CHANGELOG.md` by prepending auto-generated entries. See Additional Finding / Decision below โ€” resolved via `skip-changelog: true`. | +| `renovate.json` | No references to `goreleaser`/`release-goreleaser`/`release-drafter` by name โ€” Renovate discovers pinned actions generically by scanning workflow YAML. | No Renovate config changes needed; removing `release-goreleaser.yml` and `auto-changelog.yml` simply removes those pinned actions from Renovate's future PRs. | +| `.gitignore` (line ~163-167) | Dedicated `# GoReleaser` section ignoring `dist/`. | Remove alongside `.goreleaser.yaml` (Commit 5). | +| `.dockerignore` (line 13) | Lists `.goreleaser.yaml`. | Remove alongside `.goreleaser.yaml` (Commit 5). | +| `codecov.yml` | No workflow-file or release-artifact references in `ignore:`. | No changes needed. | -#### `tests/core/proxy-hosts.spec.ts` (8 occurrences) +## Reference Implementation (`/projects/go_notify_yourself`) -- **L209** `hasBulkBar || true` โ€” comment: "Should show bulk action bar" โ†’ **(a)**. -- **L255** `isInvalid || true` โ€” comment: "Browser validation or custom validation should prevent submission" โ†’ **(a)**; consider also asserting no `POST /api/v1/proxy-hosts` was sent (stronger, matches sub-issue 2's "verify backend state" spirit) if feasible without large rework. -- **L461** `hostCreated || true` โ€” this is the **creation-verification step of a core CRUD test**. โ†’ **(a), strengthen**: assert UI text visible **and** verify via `getProxyHostsViaAPI`/`getProxyHostViaAPI` (already in `tests/utils/api-helpers.ts`) that the host exists server-side with the expected `domain`/`forward_host`/`forward_port` โ€” mirrors the existing convention in this same helper file. -- **L523** `exists || true` (loop over expected security-option checkboxes: force SSL, HTTP/2, HSTS, block exploits, websocket) โ†’ **(a)**: these are static, always-rendered form fields; assert each `expect(exists).toBe(true)`. -- **L547** `exists || true` (loop over preset dropdown options: plex, jellyfin, homeassistant, nextcloud) โ†’ **(a)**, pending a quick implementation-time check that these presets are indeed static/guaranteed (grep the preset source, e.g. `frontend/src/**/presets*`) rather than feature-flagged. -- **L643** `hasWs || hasAcl || true`, test `'should show feature badges (WebSocket, ACL)'` โ€” comment: "May or may not exist depending on host configuration" โ†’ **(a) via test-setup fix**: rather than leaving this permanently unverifiable, seed/select a host in the test's own setup with `websocket_support: true` (via `createProxyHostViaAPI`) so `hasWs` is deterministic; drop `|| true`. -- **L969** `hasApply || hasRemove || true` โ€” comment: "Should have apply/remove tabs or buttons" (definite) โ†’ **(a)**: drop `|| true`. -- **L1014** `hasFocus || true` (keyboard nav: 3 Tabs inside an open modal, expect something focused) โ†’ **(a)** preferred (modals should trap/receive focus deterministically); fall back to **(b)** only if empirically flaky per-browser during implementation (cross-reference with sub-issue 5's WebKit focus-order risk). +- `.release-please-manifest.json`: `{".": "0.2.0"}`. +- `.github/workflows/release-please.yml`: `googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5`, `on: push: branches: [main]`, `permissions: contents: write, pull-requests: write`, `config-file`/`manifest-file` inputs. +- `release-please-config.json`: **as of this session, corrected to `"release-type": "go"`** (it was `"node"` earlier in the session, before the sibling repo's own config was independently fixed โ€” this happened to land on the exact same conclusion this plan's research below reaches independently via release-please's own docs, which is reassuring cross-validation, not something to blindly trust as "already proven correct"). The repo has no `package.json`, so `"node"` would have been wrong (release-please would look for a manifest file that doesn't exist). -#### `tests/security-enforcement/zzz-security-ui/access-lists-crud.spec.ts` (13 occurrences) +# Six Required Decisions -- **L156** `hasBadge || true` (allow/deny type badge in first seeded row) โ†’ **(a)**, contingent on the `beforeEach` seed fixture guaranteeing a row with a known type โ€” verify at implementation time. -- **L200** `isInvalid || true` โ€” comment: "HTML5 validation should prevent submission" (definite) โ†’ **(a)**. -- **L339** `hasInfo || true` โ€” "Blacklist should show 'Recommended' info box" (definite, immediately after `selectOption('blacklist')`) โ†’ **(a)**. -- **L465** `hasPresets || true` (preset options after clicking "Show presets") โ†’ **(a)**. -- **L545** `hasUpdated || true` (rename ACL, verify) โ†’ **(a)**, strengthen with `getAccessListViaAPI` name check (same convention as certificates ยง2.2 backup verification). -- **L623** `hasSuccess || true` (save success toast) โ†’ **(a)**. -- **L825** `hasBulkBar || true` โ†’ **(a)** (same pattern as proxy-hosts L209). -- **L845** `hasBulkDelete || true`, test `'should show bulk delete button when items selected'` โ†’ **(a)**. -- **L862** `hasHeading || true`, test `'should navigate between Access Lists and Proxy Hosts'` โ€” **no** preceding hard assert here (unlike the near-identical certificates.spec.ts:1052 case, which is dead code) โ†’ **(a)**: promote to a hard `await expect(heading).toBeVisible({ timeout: 5000 })`. -- **L949** `hasWarning || true`, test `'should show CGNAT warning when ACLs exist'` โ†’ **(a)**, contingent on seeded ACL data guaranteeing the CGNAT condition โ€” verify seed fixture at implementation time. -- **L985** `hasExternalIcon || true` (external-link icon on "best practices" link) โ†’ **(a)**. -- **L1018** `hasFocus || true` (same keyboard-tab pattern as proxy-hosts L1014) โ†’ **(a)** preferred, **(b)** fallback if flaky. -- **L1061** `isHidden || true` (IP input hidden when "local network only" toggle enabled) โ†’ **(a)**: deterministic conditional-field-visibility behavior. +## Decision 1 โ€” `release-type` for `release-please-config.json` -#### `tests/security-enforcement/zzz-security-ui/encryption-management.spec.ts` (8 occurrences) +**Decision: `"release-type": "go"`.** -- **L188** `hasWarning || true` (rotate-key confirm dialog warning content) โ†’ **(a)**: dialog title/confirm/cancel are already hard-asserted immediately above; the warning text check should be promoted to match. -- **L314** `hasProgress || true` โ€” comment: "Progress may appear briefly - capture if visible" โ†’ **(b)**: genuinely a timing race (progress indicator can legitimately complete before the 5s poll ever samples it); use `test.skip()`-with-reason or remove if no stronger signal (e.g., a `waitForResponse` on the rotation request) can be substituted. -- **L393** `hasWarning || true` โ€” inside `if (isDisabled)` guard, checking rotation-disabled warning text โ†’ **(a)**: once inside the guard the condition is deterministic (button is confirmed disabled). -- **L498** `hasWarning || true` โ€” comment: "Warnings may or may not be present - just verify we can detect them" โ†’ **(b)**: explicitly optional per comment; convert to non-blocking annotation or remove โ€” it currently asserts nothing meaningful either way. -- **L596** `hasBadge || true` (action-type badge in first audit-log row) โ†’ **(a)**, contingent on seeded rotation-history data. -- **L601** `hasVersionInfo || true` (version/duration info in same row) โ†’ **(a)**, same seeding caveat as L596. -- **L684** `hasToast || true` (keyboard-activated validate button should trigger a result toast) โ†’ **(a)**: deterministic feedback requirement. -- **L708** `accessibleName || true` (every visible button should have an accessible name) โ†’ **(a)**: real, valuable a11y assertion; drop `|| true`. +Verified directly against release-please's documented strategy table (`docs/customizing.md` release-type list, fetched this session): -#### `tests/security-enforcement/zzz-security-ui/system-security-settings.spec.ts` (7 occurrences) +| release-type | Description (verbatim from docs) | +|---|---| +| `go` | "A repository with a CHANGELOG.md" | +| `simple` | "A repository with a version.txt and a CHANGELOG.md" | +| `node` | "A Node.js repository, with a package.json and CHANGELOG.md" | -- **L290** `hasValidation || true` โ€” comment: "May not have inline validation" (explicit hedge) โ†’ **(b)**: convert to `test.skip()`/annotation. -- **L325** `toastVisible || true` โ€” the immediately preceding step already hard-asserts `expect(saveResponse.ok()).toBeTruthy()` (a real successful save) โ†’ **(a)**: success feedback must follow a confirmed-successful save; drop `|| true`. -- **L352** `hasSuccess || true` (green-checkmark validation indicator for a valid URL) โ†’ **(a)**, pending a quick check that this indicator is unconditionally rendered for the field (verify at implementation time). -- **L452** `toastVisible || true` โ€” comment: "URL reachability depends on network - just verify test button works" โ†’ **(a) for the meta-assertion**: regardless of network outcome (reachable/unreachable), *some* toast must always appear after clicking Test โ€” that part is deterministic. Drop `|| true`; do not assert on toast *content*. -- **L555** `hasVersion || true` (version string format) โ†’ **(a)**: app always renders a build version (semver or `dev`). -- **L680** `newState !== initialState || true` โ€” this directly overlaps sub-issue 4's feature-flag propagation fix; the test already awaits both the `PUT` and `GET` feature-flags responses via `Promise.all` before reading `newState` โ†’ **(a)**: the toggle state change is deterministic once both responses have resolved; drop `|| true`. -- **L736** `accessibleName || true` โ€” same pattern as encryption-management L708 โ†’ **(a)**. +`simple` was the task brief's suggested candidate to verify โ€” it is **not** correct: it manages a `version.txt` manifest file, which Charon does not have and does not want (per the pre-verified finding: no in-repo version manifest exists, version comes from git tag โ†’ Docker build-arg โ†’ Go ldflags only). `go` is the only listed strategy requiring **zero** manifest file โ€” exactly Charon's situation, and it still manages `CHANGELOG.md` (mitigated via `skip-changelog: true`, see Additional Finding below) and still creates git tags + GitHub Releases from Conventional Commits. -#### `tests/core/navigation.spec.ts` (4 occurrences) +`node` (the sibling repo's original, now-corrected choice) would have required release-please to manage a `package.json` that doesn't exist in either repo โ€” confirmed by direct filesystem check of `/projects/go_notify_yourself` (no `package.json` present). Not applicable to Charon either. -- **L238** `hasActiveCurrent || hasActiveClass || true` โ†’ **(a)**, mechanical: drop `|| true`, keep the two-way OR (an active nav item must signal state via `aria-current` or an active class โ€” at least one is a real requirement). -- **L559** `foundNavLink || true` โ€” comment: "May not find nav link depending on focus order - this is acceptable" โ†’ **(b)**: convert to `test.skip(true, 'no focusable nav link found via keyboard tab order in this run')` per the established `test.skip(true, reason)` convention, rather than a fake pass. -- **L733** `hasAriaCurrent || true` โ€” comment: "aria-current is recommended but not always implemented" โ†’ **(a), converge with L238's convention**: check `aria-current` **or** active class (matching the pattern already used at L238 in the same file) instead of `aria-current` alone with a fake fallback โ€” this makes it a real, DRY assertion instead of leaving it permanently soft. -- **L758** `outline || true` (focus-visible indicator style) โ†’ **(a)** preferred: assert `outline` is non-empty/not `'none'`; fall back to **(b)** only if empirically flaky across Chromium/Firefox/WebKit during implementation. +## Decision 2 โ€” Downstream consumers of the tag/Release that `auto-versioning.yml` currently produces -#### `tests/core/dashboard.spec.ts` (3 occurrences) +Read in full: `docker-build.yml`, `orthrus-build.yml`, `auto-changelog.yml`, `nightly-build.yml`, `update_service.go`, `orthrus_service.go`, `generate-changelog.sh`. -- **L232** `foundButton || true` (quick-action button reachable via keyboard tab loop) โ†’ **(b)**: tab-order flakiness, same pattern as navigation.spec.ts:559; convert to `test.skip()`-with-reason when not found within the loop bound, rather than fake pass. The real assertion (`expect(focused).toBeFocused()`) already fires correctly when found. -- **L370** `hasEmptyState || hasActualContent || true` โ€” comment: "Dashboard should show either empty state or content, not crash" (a genuine, environment-independent invariant) โ†’ **(a)**, mechanical: drop `|| true`. -- **L491** `reachedCard || focusableElementsFound > 0 || true` โ€” comment: "verify we at least found some focusable elements" โ†’ **(a)**, mechanical: drop `|| true`, keep the two-way OR. - -#### `tests/settings/smtp-settings.spec.ts` (4 occurrences) +| Consumer | Trigger | Depends on | +|---|---|---| +| `orthrus-build.yml` | `push: tags: ['v*']` | **Real dependency.** Must keep firing on the same bare `v` tag pattern; its `docker/metadata-action` step derives `type=semver` Docker tags directly from the pushed tag ref. | +| `docker-build.yml` | Branch push only, never tags | **No dependency** โ€” confirmed dead/inert `type=semver` patterns in its own `docker/metadata-action` config (never actually reached because `TRIGGER_REF` is always a branch ref). Nothing to preserve here beyond not breaking branch-push behavior, which this migration doesn't touch. | +| `release-goreleaser.yml` | `push: tags: ['v*']` | **No real dependency** โ€” never succeeded, produces nothing (Decision 5). | +| `auto-changelog.yml` | `workflow_run` + `release: types: [published]` | Redundant, not load-bearing for anything else โ€” see Additional Finding. | +| `update_service.go` | Polls `releases/latest` REST endpoint | Needs *a* GitHub Release to exist with `tag_name`/`html_url` โ€” release-please provides this natively. | +| `generate-changelog.sh` (via `nightly-build.yml`) | Reads `git tag -l 'v*'` directly | Needs real `v*` tags in git history โ€” release-please provides this as long as tag-naming is kept bare (Decision 1's `include-component-in-tag: false`, see risk below). | -- **L121** `skeletonVisible || true` โ€” comment: "Either skeleton is shown or page loads very fast" โ†’ **(b)**: genuine loading-timing race; convert to `test.skip()`-with-reason or remove โ€” a 500ms artificial delay plus a 1000ms visibility timeout should make the skeleton reliably visible, so first try tightening the mock/timeout to make this **(a)** before falling back to **(b)**. -- **L165** `hasValidation || true` โ€” comment: "Either inline validation or form submission is blocked" (definite requirement, required-field case) โ†’ **(a)**. -- **L231** `hasValidation || true` โ€” comment: "Validation should occur (inline or via toast)" (definite, invalid-email-format case) โ†’ **(a)**. -- **L906** `hasAccessibleError || true` โ€” comment: "Some form of accessible error feedback should exist" (definite a11y requirement) โ†’ **(a)**. +**Conclusion**: the only workflow with a genuine, live functional dependency on the tag is `orthrus-build.yml`, and it only needs the tag to (a) exist, (b) match glob `v*`, (c) parse as semver for the `docker/metadata-action` `type=semver` patterns. Release-please satisfies all three by default, provided tag-naming is pinned explicitly (see risk callout in Technical Specifications). -#### `tests/settings/account-settings.spec.ts` (1 occurrence) +## Decision 3 โ€” Interaction with the nightlyโ†’main "merge commit only" rule -- **L875** `foundApiButton || true` โ€” comment: "Non-blocking assertion" (explicit hedge, keyboard tab-order search for API key buttons) โ†’ **(b)**: convert to `test.skip()`-with-reason, consistent with the tab-order-flakiness cases above. +**Decision: keep the CLAUDE.md rule unchanged. Do not relax it, and do not propose editing CLAUDE.md.** -#### `tests/security/system-settings-feature-toggles.spec.ts` (1 occurrence) +Investigated directly against release-please's docs (`docs/design.md`, GitHub README commit-conventions section, fetched this session): -- **L317** `overlayVisible || true` โ€” comment: "Overlay may appear briefly - either is acceptable" โ†’ **(b)**: genuine timing race (config-reload overlay can complete before the 1s poll samples it); the `responsePromise` for the `PUT /feature-flags` call is already captured above but never awaited/used to gate this check โ€” first try awaiting that promise before sampling the overlay (would make this **(a)**); fall back to **(b)** if still flaky. +- Release-please's own docs state it **"highly recommends"** squash-merging *feature* PRs into a linear history, and that it discovers releasable commits by **iterating backwards through actual git commits** (not by re-parsing PR bodies or bullet-ized squash-commit text) until it hits a known prior release SHA. +- This is superficially the opposite of Charon's current constraint (which exists specifically to keep bullet-per-commit squash bodies parseable by `paulhatch/semantic-version`'s regex). But the underlying *mechanism* that makes the constraint necessary is unchanged: the weekly `nightly` โ†’ `main` promotion PR itself accumulates a full week of already-individually-squashed feature commits. If that promotion PR were **squash-merged** into `main`, all of that week's discrete `feat:`/`fix:` commits would collapse into a single commit on `main` whose own subject line is whatever GitHub picks for the squash (typically the PR title, not necessarily a clean Conventional Commit type) โ€” release-please's per-commit git-log walk would then see **one** commit for the entire week, not one-per-change, and lose the same granularity that currently breaks `paulhatch`. Using **"Create a merge commit"** for the promotion PR preserves each week's individual squashed-per-feature commits as distinct commits in `main`'s history, which is exactly what release-please's commit-by-commit walk needs to correctly attribute each `feat:`/`fix:` to the right release. +- **Conclusion**: the rule's *justification* text in CLAUDE.md ("squash merging collapses all commits into bullet lines that the auto-versioning workflow cannot parse") becomes slightly inaccurate wording once `paulhatch` is gone, but the *rule itself* remains equally necessary under release-please, for an adjacent reason. This plan does **not** propose editing CLAUDE.md's rule or its wording โ€” flagging the wording-vs-mechanism nuance here is for the user's own future reference only, per the task's instruction not to propose CLAUDE.md edits. -#### `tests/manual-dns-provider.spec.ts` (1 occurrence) +## Decision 4 โ€” Fate of `.version` and `scripts/check-version-match-tag.sh` -- **L311** `hasVisibleIcon || true` (status icon inside an already-hard-asserted status indicator) โ†’ **(a)**: the indicator itself is already hard-asserted visible immediately above; the icon inside it should be deterministic too. +**Decision: remove both, plus the `check-version-match` lefthook hook entry (`lefthook.yml` line ~99) and the now-pointless `.github/skills/utility-version-check*` skill wrapper.** -**Summary**: of 59 `|| true` occurrences, **~47 become real assertions (a)**, **~4 are dead code to delete**, and **~8 are genuinely environment/timing-dependent and become explicit `test.skip()` calls (b)** using the repo's existing convention โ€” plus 1 additional vacuous test (certificates.spec.ts's `'should show config reload overlay during deletion'`) that has no `|| true` to count here but requires the identical interaction-model fix (see the certificates.spec.ts breakdown above). Exact per-line final disposition is confirmed during implementation per the guidance above; the DoD gate in ยง5 enforces that zero bare `|| true`-before-`toBeTruthy()` patterns remain regardless of which bucket each line lands in. +Reasoning: +- `.version` is already stale (`v0.27.0` vs. real latest tag `v0.36.5`) and already documented in `VERSION.md` as "optional... not the canonical release trigger" โ€” it carries no functional weight today. +- `scripts/check-version-match-tag.sh` is **already self-deprecated in its own source** (prints a warning telling callers to use the skill-runner instead) โ€” but `.github/skills/utility-version-check-scripts/run.sh` just `exec`s this same script, so the "recommended" migration path is circular dead code, not an actual alternative implementation. +- Release-please replaces the entire concept this check exists for: `.release-please-manifest.json` becomes the single source of truth for "what version are we at," continuously kept in sync with tags by release-please itself. A hand-run parity check between a stale flat-text file and `git tag` adds no safety release-please doesn't already provide, and having *two* "canonical" version records (`.release-please-manifest.json` and `.version`) invites exactly the kind of drift the check script exists to catch. +- The check is non-blocking today (`exit 0` when `.version` is absent, per the script's own logic), so removing the file cannot regress any currently-enforced gate. -### 2.3 Sub-issue 3 โ€” Backend coverage gaps: CONFIRMED STALE / ALREADY RESOLVED, NO CODE CHANGES +**Full blast radius** (repo-wide `grep -rln "utility-version-check\|check-version-match-tag"`, re-verified against Supervisor's independent review): -Re-ran directly (non-mutating `go test -cover`): +| Reference | Action | +|---|---| +| `.github/skills/utility-version-check-scripts/run.sh`, `.github/skills/utility-version-check.SKILL.md`, `scripts/check-version-match-tag.sh`, `.version`, `lefthook.yml`'s `check-version-match` hook | Delete/remove โ€” already in original Deleted/Modified Files scope. | +| `.github/skills/README.md:72` (Utility Skills table row) and `:267` (kebab-case naming example, `utility-version-check` bullet) | **Now added to this plan's scope**: remove both โ€” assigned to Commit 5. | +| `.vscode/tasks.json:694-698` ("Utility: Check Version Match Tag" task, shells out to `skill-runner.sh utility-version-check`) | **Now added to this plan's scope**: remove this task block โ€” assigned to Commit 5. Left in place, it would error every time it's run post-deletion. | +| `.github/skills/utility-bump-beta.SKILL.md:186` ("Related Skills" cross-link to `utility-version-check.SKILL.md`) | **Now added to this plan's scope**: remove the dead link (keep the rest of that skill's "Related Skills" list intact) โ€” assigned to Commit 5. | +| `CLAUDE.md:261` (Skills table row: `utility-version-check \| Check tool versions`) | **Amended after explicit user sign-off**: the user has explicitly authorized editing this governance row as part of this PR ("include the edit in claude.md. no need to make it a follow-up when it can be done now"), satisfying this plan's own constraint against silently touching CLAUDE.md. **Decision: remove the `utility-version-check` row from CLAUDE.md's Skills table (line 261) in this PR**, folded into Commit 5 alongside the rest of this skill's deletion (contingent on that deletion actually happening in this same commit, which it does โ€” see Commit 5 scope). No longer deferred as a Manual Post-Merge Follow-Up. | -``` -ok internal/services coverage: 88.4% of statements (target 85%) -ok internal/services/remotestorage coverage: 90.3% of statements -ok backend/pkg/dnsprovider/builtin coverage: 91.8% of statements (target 50% incremental) -``` +## Decision 5 โ€” Fate of `.goreleaser.yaml` and `release-goreleaser.yml` -Confirms the prior investigation exactly. `backend/pkg/dnsprovider/builtin` is the correct current location (relocated from `internal/dnsprovider/builtin` as the original issue text said) and is excluded from `codecov.yml` reporting (`ignore:` list, line 136 โ€” "tested via integration tests, not unit tests") but not from `go-test-coverage.sh`'s enforcement; either way, actual coverage is far above both the codecov project target (87%) and the issue's original incremental target (50%). **No regression, no code changes required.** This PR's only obligation here is to capture a coverage run as DoD evidence (ยง5) and state this explicitly in the PR description (ยง6). +**Decision: remove both entirely.** -### 2.4 Sub-issue 4 โ€” Feature flag async propagation tests: CONFIRMED STALE / ALREADY RESOLVED, NO CODE CHANGES +Confirmed via full reads of `docker-build.yml` and `orthrus-build.yml` (Decision 2) that neither depends on GoReleaser's build/archive/nfpm output โ€” Charon's Docker images are built directly by `docker-build.yml`'s own multi-stage `Dockerfile`, with version injected via ldflags independently and identically to what `.goreleaser.yaml`'s `builds.ldflags` section does (Dockerfile lines ~250-286 vs. `.goreleaser.yaml`'s `builds[0].ldflags`). `.goreleaser.yaml`'s own header comment already self-documents as unused: *"builds, archives, and nfpms... kept for potential future use but are not currently utilized."* -`tests/settings/system-settings.spec.ts` no longer exists (confirmed via `find`); the feature-flag tests were reorganized into `tests/security/system-settings-feature-toggles.spec.ts`, which: -- Imports and calls `waitForFeatureFlagPropagation` **9 times** (exact count via `grep -c`, correcting the prior investigation's "11" estimate) across all 9 tests in the file. -- Has **zero** `.skip`/`.fixme` markers (aside from the one tautological assertion at L317, covered under sub-issue 2 above โ€” a different problem, not the async-propagation flakiness this sub-issue was about). +The only *other* thing `release-goreleaser.yml` does is call `scripts/generate-changelog.sh` (line 77) โ€” but that script is independently invoked by `nightly-build.yml` too, and depends only on git tags, not on GoReleaser itself, so removing the workflow does not remove changelog-generation capability from anywhere it currently actually runs. -**No regression, no code changes required.** This file is already fully in-scope for the mandatory full E2E run in ยง5 (it was already going to run; no special inclusion action needed). +**No nfpm/package-distribution consumer exists** โ€” grep across `docs/`, `.github/workflows/`, and `Dockerfile` for `nfpm`/`.deb`/`.rpm` distribution steps outside `.goreleaser.yaml` itself returns nothing; Charon ships exclusively as Docker images per `ARCHITECTURE.md`'s stated deployment model. -### 2.5 Sub-issue 5 โ€” WebKit E2E tests not executing: CONFIG CONFIRMED HEALTHY, ONE REAL RUN STILL OUTSTANDING +## Decision 6 โ€” Fate of the "PR-2 release promotion guard" -- WebKit `26.5` installed; `npx playwright test --list --project=webkit` discovers **963 tests across 86 files** (re-verified, matches prior investigation exactly). -- `playwright.config.js` (repo root โ€” the config actually governing `tests/`, distinct from the unrelated minimal `frontend/e2e/playwright.config.ts`) reviewed line-by-line: the `webkit` project (L299-314) has **identical** `dependencies`, `testMatch`, and `testIgnore` patterns to `chromium`/`firefox` โ€” no webkit-specific exclusion, no `browserName`-conditioned `test.skip()` anywhere in `tests/**` (repo-wide grep confirmed zero matches). -- Note (informational, not a defect): the `webkit` project's `testIgnore` excludes `**/security-enforcement/**` and `**/tests/security/**`, same as chromium/firefox โ€” those specs only run under the dedicated `security-tests` project, which is **Chromium-only by design** (L237-254, "SEQUENTIAL, Chromium only"). This means 29 of this PR's 59 sub-issue-2 fixes (all of `access-lists-crud.spec.ts`, `encryption-management.spec.ts`, `system-security-settings.spec.ts`, `system-settings-feature-toggles.spec.ts`) are **out of WebKit's run scope entirely, by existing design** โ€” not something this PR changes or needs to change. -- A dedicated `tests/core/caddy-import/caddy-import-webkit.spec.ts` (`@webkit-only` tag) already exists for known WebKit-specific quirks in the Caddyfile-import flow, and `caddy-import-cross-browser.spec.ts` already parameterizes assertions per `browserName` โ€” evidence the team has previously handled real WebKit differences correctly elsewhere; no similar per-browser branching is missing here. -- **Risk flagged**: none identified in config. The keyboard-focus-order tautologies converted to real assertions in ยง2.2 (proxy-hosts.spec.ts:1014, access-lists-crud.spec.ts:1018, navigation.spec.ts:758) are the most plausible source of **new** WebKit-specific flakiness once they stop being unconditionally true โ€” this is exactly why ยง2.2 marks them "(a) preferred, (b) fallback if empirically flaky" rather than a hard mandate, and why the full WebKit run (ยง5) must happen **after** the sub-issue-2 commits land, not before. -- **Not run in this planning pass** (explicitly deferred to execution/QA phase per task instructions): the actual full `npx playwright test --project=webkit` execution. This is a mandatory, explicit Definition-of-Done gate (ยง5) for this PR. +**Decision: the gate is a deliberate, purpose-built temporary safety mechanism (not a misconfigured accident), whose guarded purpose has since been satisfied and whose host workflow is being retired anyway โ€” so it is removed as a natural consequence of Decision 5, not silently dropped on its own merits.** -### 2.6 `.gitignore` / `.dockerignore` / `codecov.yml` / `Dockerfile` review +Evidence trail: +- `git log -S"Enforce PR-2 release promotion guard"` traces the gate's introduction to commit `834b27f2` / `45458df1`, `"chore: Add Caddy compatibility gate workflow and related scripts; enhance SMTP settings tests"`, dated 2026-02-23. That same commit also adds `.github/workflows/caddy-pr1-compat.yml` and `docs/reports/caddy-pr1-compatibility-matrix.md`. +- `docs/reports/caddy-security-posture.md`, also dated 2026-02-23, is explicitly titled **"PR-2 Security Patch Posture and Advisory Disposition"** โ€” a Caddy 2.11.x upgrade security review (patch retention/retirement decisions for `expr`, `ipstore`, `nebula`; CVE/GHSA disposition table) that is **unrelated to any numbered pull request in this repo's PR history** โ€” "PR-2" here names a phase of a specific historical security workstream (Caddy version-bump security review), not a generic or accidental label. +- That doc's own closure statement: *"PR-2 posture decisions are review-ready: patch disposition is explicit, admin API assumptions are enforced, and rollback remains deterministic."* โ€” i.e., the gate's guarded condition (finish the Caddy PR-2 security review before letting GoReleaser cut a publishable release) **was satisfied the same day the gate was added.** +- Nobody ever flipped `CHARON_PR2_GATES_PASSED=true` afterward โ€” confirmed via `REPO_VARS_JSON: {}` on every subsequent `release-goreleaser.yml` run. This is most plausibly an oversight (the review closed, but the repo-variable flip was a separate manual step nobody circled back to) rather than a deliberate ongoing hold, since the closure doc gives no indication the gate was meant to stay engaged indefinitely. -All reviewed; **no changes required** for this PR: +**Recommendation for the user**: this is not evidence of a security requirement that needs to be re-implemented elsewhere โ€” it was scoped to one specific, already-closed security review. Removing it alongside the rest of `release-goreleaser.yml` is safe. If the user wants a similar "hold releases pending a security sign-off" mechanism for *future* security reviews, that would be a new, forward-looking control to design separately โ€” explicitly flagged here as a possible follow-up, not something this plan implements. -- `.gitignore`: `frontend/coverage/`, `frontend/test-results/`, `/test-results/`, `/playwright-report/` already cover all artifacts this PR's test runs will produce. -- `.dockerignore`: `tests/`, `test-results/`, `test-data/` already excluded from the Docker build context; no new test directories are being introduced by this PR (only edits to existing spec/test files). -- `codecov.yml`: `**/e2e/**`, `**/*.spec.ts`, `**/__tests__/**` already excluded from coverage accounting; `backend/pkg/dnsprovider/builtin/**` already excluded (consistent with ยง2.3's finding that this package is verified via integration tests). No new source paths are introduced. -- No `Dockerfile` changes โ€” this PR ships no runtime code. +**A second, independent argument for removal, stronger than the intent-inference above**: even setting aside whether the Caddy-review closure was "meant" to release the gate, Decision 5 establishes that GoReleaser will **never again attempt to publish anything** โ€” the workflow it lives in is being deleted outright, not merely disabled. A gate that guards an action which no longer exists has nothing left to guard, independent of any judgment call about the gate's original intent or whether it was ever properly released. This makes the removal safe on structural grounds alone, not just on the historical-intent grounds argued above. ---- +# Additional Findings Beyond the Six Required Points -## 3. Technical Specifications +These surfaced during the mandated research and materially affect the design, so they're resolved here rather than left implicit. -This PR is test-infrastructure-only. There is no new API surface, no database schema change, and no new component. The "component design" for this PR is the test-file structure itself. +## A. `auto-changelog.yml` / `.github/release-drafter.yml` are redundant with release-please -### 3.1 Affected files (exhaustive) +Not named in the original task brief, but directly in-scope: it's a `.github/workflows/*` file in the exact pipeline being migrated, and it will actively conflict with release-please if left running (both listen on `release: types: [published]`-adjacent events and both try to own "the changelog for this release"). `release-drafter`'s own tag-template (`v$NEXT_PATCH_VERSION`) already never bumps minor/major regardless of label โ€” a pre-existing bug, further weakening the case for keeping it. -**Frontend unit tests (sub-issue 1):** -- `frontend/src/pages/__tests__/Security.test.tsx` -- `frontend/src/pages/__tests__/Security.audit.test.tsx` -- `frontend/src/pages/__tests__/Security.errors.test.tsx` -- `frontend/src/pages/__tests__/Security.loading.test.tsx` -- `frontend/src/pages/__tests__/Security.dashboard.test.tsx` -- `frontend/src/pages/__tests__/Security.functional.test.tsx` +**Decision: remove `.github/workflows/auto-changelog.yml` and `.github/release-drafter.yml` in this PR.** Release-please's standing release PR (with its auto-updated body) replaces the "always-fresh draft changelog" function these two files provide. -**E2E specs (sub-issue 2):** -- `tests/core/certificates.spec.ts` (includes the 5-test "Certificate Deletion" block rewrite โ€” see ยง2.2) -- `tests/core/proxy-hosts.spec.ts` -- `tests/core/navigation.spec.ts` -- `tests/core/dashboard.spec.ts` -- `tests/settings/smtp-settings.spec.ts` -- `tests/settings/account-settings.spec.ts` -- `tests/security-enforcement/zzz-security-ui/access-lists-crud.spec.ts` -- `tests/security-enforcement/zzz-security-ui/encryption-management.spec.ts` -- `tests/security-enforcement/zzz-security-ui/system-security-settings.spec.ts` -- `tests/security/system-settings-feature-toggles.spec.ts` -- `tests/manual-dns-provider.spec.ts` +## B. `CHANGELOG.md` conflict -**Test utility additions (sub-issue 2):** -- `tests/utils/api-helpers.ts` โ€” add `getBackupsViaAPI(request, token?)`, following the exact signature/error-handling pattern of the file's existing `get*ViaAPI` functions (e.g. `getCertificatesViaAPI`), targeting `GET /api/v1/backups`. +Charon's root `CHANGELOG.md` is hand-curated (Keep a Changelog format, multi-line rich entries, issue cross-references, 600 lines of history). Release-please's `go` release-type, by default, prepends its own auto-generated entries to whatever file `changelog-path` points at (default `CHANGELOG.md`). -**No changes**: any `backend/**` file, any `frontend/src` file outside `__tests__/`, `.gitignore`, `.dockerignore`, `codecov.yml`, any `Dockerfile`. +**Decision**: set `"skip-changelog": true` on the `"."` package in `release-please-config.json`. Per the release-please JSON schema (`schemas/config.json`, fetched this session): *"Skip generating a changelog for this package. Defaults to `false`."* This stops release-please from touching `CHANGELOG.md` at all, preserving the existing hand-curated file untouched. GitHub Release notes generation is understood to be independent of the changelog-file-write path (the Release body is built from the same underlying commit grouping, separately from whether it's also written to a file) โ€” **this exact interaction is not explicitly documented** in the pages fetched this session, so it is flagged in Manual Post-Merge Follow-Ups as something to positively confirm on the first real release-please run (does the created GitHub Release still get a populated body with `skip-changelog: true`?). -### 3.2 API contracts referenced (read-only, no changes) +## C. Tag-naming default is a real breakage risk โ€” must be pinned explicitly -These existing endpoints are what the strengthened assertions in ยง2.2 verify against โ€” documented here for implementer reference, not as new contracts: +Per the release-please JSON schema (fetched this session): `include-component-in-tag` **defaults to `true`** ("When tagging a release, include the component name as part of the tag"). For a single non-monorepo package at `"."`, this risks producing a tag like `charon-v0.37.0` instead of the bare `v0.37.0` every existing consumer expects (`orthrus-build.yml`'s `tags: ['v*']` trigger, `generate-changelog.sh`'s `git tag -l 'v*'` scan, `update_service.go`'s expectations, and every pre-existing tag in the repo's own history back to `v0.1.0`-style tags). -| Endpoint | Method | Used by (test) | Purpose in this PR | -|---|---|---|---| -| `/api/v1/certificates/:uuid` | `GET` | `certificates.spec.ts` cancel-delete test | Verify certificate still exists after a dismissed delete | -| `/api/v1/certificates/:uuid` | `DELETE` | `certificates.spec.ts` in-use/backup tests | Existing delete flow (`certificate_handler.go:387-470`) โ€” unchanged | -| `/api/v1/backups` | `GET` | `certificates.spec.ts` backup-creation test | Verify a new backup entry appears after a successful cert delete | -| `/api/v1/access-lists/:id` | `GET` | `access-lists-crud.spec.ts` rename test | Verify renamed ACL persisted server-side | -| `/api/v1/proxy-hosts` / `/api/v1/proxy-hosts/:id` | `GET` | `proxy-hosts.spec.ts` creation test | Verify created host persisted server-side | +**Decision**: explicitly set `"include-component-in-tag": false` in `release-please-config.json`. Do not rely on whatever component-name-derivation-for-an-unnamed-root-package default behavior release-please falls back to โ€” pin it. Flagged as a **must-verify-on-first-live-run** item (see Manual Post-Merge Follow-Ups): confirm the first release-please-created tag is exactly `v`, no prefix/suffix. -### 3.3 Error handling / edge cases to cover in the new assertions +## D. Pre-1.0 major-version-bump behavior must be pinned explicitly -- Certificate delete "in use" path: assert the **specific** 409 error surface (toast/message), not merely "a toast of some kind." -- Certificate delete "backup" path: must select/seed a certificate guaranteed **not** in use (backend returns 409 before attempting backup if in use โ€” asserting backup creation against an in-use cert would be a false test). -- ACL/proxy-host rename/creation: API-level verification must tolerate eventual consistency the same way existing passing tests in these files already do (reuse existing `waitFor`/polling helpers, do not add new ad hoc `setTimeout`s). -- WebKit-sensitive keyboard-focus assertions (ยง2.2, "(a) preferred, (b) fallback"): implementers must actually run the affected spec under `--project=webkit` (not just chromium/firefox) before finalizing as (a); if flaky, fall back to (b) with an accurate WebKit-specific skip reason, not a silent revert to `|| true`. +`auto-versioning.yml`'s current design deliberately disables automatic major-version bumps ("Major version bumps are intentionally disabled in automation to prevent accidents" โ€” its own header comment; `major_pattern` is set to a regex that can never match). Release-please instead supports major bumps automatically via `!` suffix or `BREAKING CHANGE:` footer conventions, gated pre-1.0 by two schema options (`bump-minor-pre-major`, `bump-patch-for-minor-pre-major`) whose **schema definitions carry no explicit documented default** (confirmed by direct inspection of `schemas/config.json` this session โ€” both properties have a `description` but no `default` key shown). -### 3.4 Data flow notes +**Decision**: set both `"bump-minor-pre-major": true` and `"bump-patch-for-minor-pre-major": true` explicitly in `release-please-config.json`, regardless of what the undocumented actual default turns out to be. This guarantees a `feat!:`/`BREAKING CHANGE:` commit at the current `v0.36.5` bumps to `v0.37.0` (matching the existing "major bumps require a deliberate manual tag, never automatic" philosophy) rather than silently jumping to `v1.0.0`. This is materially safer than trusting an unconfirmed default and is worth the two explicit lines. -No data flow changes. The certificate-deletion backup verification (ยง2.2) exercises an **existing** synchronous flow: `DELETE /api/v1/certificates/:uuid` โ†’ `IsCertificateInUse` check โ†’ (if not in use) `backupService.GetAvailableSpace()` โ†’ `backupService.CreateBackup()` โ†’ `service.DeleteCertificateByID()` โ†’ response. All calls are synchronous within the single request; no polling/async job is involved for this specific path (unlike the general `POST /api/v1/backups` flow used elsewhere, which does return `202` + a job id โ€” do not conflate the two; the cert-delete backup call is a direct, blocking `CreateBackup()`). +## E. `chore:`-scoped commits will not trigger a Docker build on `main` ---- +`docker-build.yml`'s `setup` job (lines ~170-173) already skips the real build when the head commit or PR title matches `^chore:` or `^chore\(deps`. Since every commit in this PR is `chore:`-scoped (pure CI/CD config), merging it to `main` will not trigger a Docker build โ€” expected and desired, not a gap to fix. -## 4. Implementation Plan +# Technical Specifications -### Phase 1 โ€” E2E specs for new/changed behavior +## New Files -No net-new user-facing behavior is being introduced (this is a test-quality fix, not a feature), so there is no `test.fixme()` scaffolding phase in the usual sense. Instead, Phase 1 is: write the `getBackupsViaAPI` helper addition to `tests/utils/api-helpers.ts` (foundation for Commit 2's certificate tests) and confirm it compiles/type-checks against the existing `parseResponse`/`getAuthHeaders` pattern. +### `release-please-config.json` (repo root) -### Phase 2 โ€” Foundation (no behavior change) - -- Add `getBackupsViaAPI` to `tests/utils/api-helpers.ts`. -- No other foundation work required โ€” this PR doesn't touch shared fixtures, `global-setup.ts`, or `playwright.config.js`. - -### Phase 3 โ€” Backend - -N/A โ€” confirmed no backend code changes required (ยง2.3). - -### Phase 4 โ€” Frontend / test changes (the bulk of the work) +```json +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "release-type": "go", + "include-component-in-tag": false, + "bump-minor-pre-major": true, + "bump-patch-for-minor-pre-major": true, + "pull-request-header": "Here's what's new in Charon", + "pull-request-footer": "Merge this PR to cut the release.", + "packages": { + ".": { + "skip-changelog": true + } + } +} +``` -- Sub-issue 1: unskip 5 files, delete the stale test in the 6th (ยง2.1). -- Sub-issue 2: fix all 59 tautologies per the file-by-file disposition in ยง2.2, including the certificate-deletion flow's larger rewrite (native-dialog โ†’ custom-modal interaction). +### `.release-please-manifest.json` (repo root) -### Phase 5 โ€” Hardening, full-suite validation, docs +```json +{ + ".": "0.36.5" +} +``` -- Full Vitest suite run (not just the touched files) to catch regressions from unskipping. -- Full Playwright run across chromium/firefox/webkit, including the security-tests shard. -- Coverage checks (frontend + backend) at/above enforced thresholds. -- PR description scaffolding (ยง6). +Seeded to the real latest tag at plan-authoring time (`git tag --sort=-v:refname | head -1` โ†’ `v0.36.5`). **Implementer note**: re-run that command immediately before implementation and use whatever the actual latest tag is at that time โ€” do not blindly copy `0.36.5` if additional tags have landed on `main` since this plan was written. ---- +### `.github/workflows/release-please.yml` -## 5. Commit Slicing Strategy +```yaml +name: release-please -Single PR, `test/issue-619-test-infra-debt` โ†’ `development`, ordered commits. Each commit is independently buildable/testable; later commits depend on earlier ones as noted. +on: + push: + branches: [main] -### Commit 1 โ€” `test: add getBackupsViaAPI helper for E2E backup verification` -- **Scope**: Foundation. Add `getBackupsViaAPI(request, token?)` to `tests/utils/api-helpers.ts`, matching the existing `get*ViaAPI` pattern exactly (JSDoc block, `parseResponse`, `getAuthHeaders`). -- **Files**: `tests/utils/api-helpers.ts`. -- **Dependencies**: none. -- **Validation gate**: `cd frontend && npm run type-check` passes (the helper file is TS, checked as part of the frontend project); no test run needed yet (unused until Commit 3). +permissions: + contents: write + pull-requests: write -### Commit 2 โ€” `fix: unskip Security.* Vitest suites now that undici/jsdom WebSocket bug is fixed` -- **Scope**: Sub-issue 1. Remove `describe.skip` โ†’ `describe` in the 5 files; delete the stale `it.skip('should open notification settings modal...')` block (and its now-empty `describe('Notification Settings Modal', ...)` wrapper) from `Security.functional.test.tsx`. -- **Files**: the 6 files listed in ยง2.1. -- **Dependencies**: none (independent of Commits 1/3+). -- **Validation gate**: `npx vitest run src/pages/__tests__/Security.test.tsx src/pages/__tests__/Security.audit.test.tsx src/pages/__tests__/Security.errors.test.tsx src/pages/__tests__/Security.loading.test.tsx src/pages/__tests__/Security.dashboard.test.tsx src/pages/__tests__/Security.functional.test.tsx` โ€” zero failures, zero unexpected skips. Then a **full** `npx vitest run` (not just these files) โ€” zero regressions vs. the ยง2.1 baseline (263โ†’268 passed test files, 3247โ†’3330 passed tests, 88โ†’4 skipped [only the out-of-scope CrowdSec ones remain]). Frontend coverage (`scripts/frontend-test-coverage.sh`) at/above 85%. +jobs: + release-please: + runs-on: ubuntu-latest + steps: + - uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5 + with: + config-file: release-please-config.json + manifest-file: .release-please-manifest.json +``` -### Commit 3 โ€” `fix: replace tautological assertions in certificates.spec.ts with real backend-verified checks` -- **Scope**: Sub-issue 2, certificates file only (highest-complexity file โ€” isolated to its own commit given the custom-modal rewrite). All 9 `|| true` lines in ยง2.2's certificates.spec.ts breakdown, plus the native-dialog โ†’ custom-modal rewrite for all 5 deletion tests in the "Certificate Deletion" block (`should show delete confirmation dialog`, `should warn if certificate is in use by proxy host`, `should cancel delete when confirmation dismissed`, `should create backup before deletion`, and `should show config reload overlay during deletion` โ€” the last of which has no `|| true` to grep for but shares the identical broken `page.once('dialog', ...)` interaction model and currently asserts nothing). -- **Files**: `tests/core/certificates.spec.ts` (uses `getBackupsViaAPI` from Commit 1, `getCertificateViaAPI` already present). -- **Dependencies**: Commit 1. -- **Validation gate**: `npx playwright test tests/core/certificates.spec.ts --project=chromium` and `--project=firefox` both pass, zero flaky retries. Manual grep confirms zero `|| true).toBeTruthy()` remaining in this file. +Mirrors `/projects/go_notify_yourself/.github/workflows/release-please.yml` exactly, including the same pinned SHA. **Implementer note**: verify this SHA still resolves to `v5` (or a newer tag) immediately before implementation โ€” supply-chain pins should reflect the current best-available release, not be copied stale. + +## Modified Files + +| File | Change | +|---|---| +| `VERSION.md` | Full rewrite: remove "Canonical Release Process (Tag-Derived CI)" section describing `release-goreleaser.yml`/`docker-build.yml` as jointly building release artifacts from the tag (false, per Decision 2/5). Replace with a description of the release-please PR-based flow: commits land on `main` โ†’ release-please maintains a standing `chore(main): release X.Y.Z` PR โ†’ merging that PR tags + publishes the GitHub Release โ†’ `orthrus-build.yml` picks up the tag independently. Remove the "Legacy/Optional `.version` Path" section (Decision 4). Remove the "release-drafter workflow" changelog-generation mention (Additional Finding A). Keep the Container Image Tags / Nightly Versioning Format sections unchanged (still accurate, untouched by this migration). | +| `lefthook.yml` | Remove the `check-version-match: { glob: ".version", run: "bash scripts/check-version-match-tag.sh" }` hook entry (~line 99). | +| `.gitignore` | Remove the `# GoReleaser` section (`dist/` ignore rule, ~lines 163-167). | +| `.dockerignore` | Remove the `.goreleaser.yaml` line (~line 13). | +| `ARCHITECTURE.md` | **Was missing from the first draft of this plan โ€” added per Supervisor review.** Per CLAUDE.md's own rule ("update `ARCHITECTURE.md` when making changes to... deployment model... integration points"), and because it explicitly names a file this plan deletes: the "Release Workflow" section (~lines 1464-1479) opens with a 10-step "Automated Release (GitHub Actions)" list whose step 1 is `"Trigger: Push tag v1.2.0"` โ€” this describes the tag-push-triggers-everything model this migration retires (steps 2-8 describe `docker-build.yml`'s real, still-accurate build/scan/sign/publish pipeline, which is **not** actually tag-triggered per Decision 2's finding โ€” this section conflates `docker-build.yml`'s branch-push pipeline with a tag-triggered release flow that doesn't exist as described). Line 1479 states `scripts/generate-changelog.sh` "runs during the same `release-goreleaser.yml` workflow" โ€” that workflow is deleted by this PR (the script also independently runs from `nightly-build.yml`, per Research Findings, so this line is doubly inaccurate even before this migration). **Rewrite scope**: replace the "Trigger: Push tag" framing with the release-please push-to-main โ†’ standing-PR โ†’ merge-to-tag flow (mirror the Data Flow diagram below); correct the `generate-changelog.sh` sentence to name `nightly-build.yml` (its one remaining real caller) instead of the deleted workflow; leave steps 2-8's description of `docker-build.yml`'s actual build/scan/sign/publish pipeline untouched (still accurate, just re-anchor which trigger kicks it off). Assigned to Commit 6. | + +## Deleted Files + +| File | Reason | +|---|---| +| `.github/workflows/auto-versioning.yml` | Replaced by `release-please.yml` (Decision 1-3). | +| `.github/workflows/release-goreleaser.yml` | Never succeeded; no downstream consumer (Decision 5). | +| `.goreleaser.yaml` | Unused build/archive/nfpm config; version injection already duplicated in `Dockerfile` directly (Decision 5). | +| `.github/workflows/auto-changelog.yml` | Redundant with release-please's own PR/changelog mechanism (Additional Finding A). | +| `.github/release-drafter.yml` | Config for the file above; removed alongside it. | +| `.version` | Stale, non-canonical, superseded by `.release-please-manifest.json` (Decision 4). | +| `scripts/check-version-match-tag.sh` | Self-deprecated, circularly "replaced" by a wrapper that calls it, superseded by release-please's manifest (Decision 4). | +| `.github/skills/utility-version-check-scripts/run.sh` | Wraps the script above; dead once its target is gone. | +| `.github/skills/utility-version-check.SKILL.md` | Documents the now-removed skill. | + +**Partial edits (file kept, dead reference removed)** โ€” full detail and reasoning in Decision 4 and the Commit Slicing Strategy's Commit 5 row; listed here only as a pointer so this table isn't mistaken for the complete blast radius: + +| File | Edit | +|---|---| +| `.github/skills/README.md` | Remove the `utility-version-check` row (~line 72) and naming-example bullet (~line 267). | +| `.vscode/tasks.json` | Remove the "Utility: Check Version Match Tag" task block (~lines 694-698). | +| `.github/skills/utility-bump-beta.SKILL.md` | Remove the dead `utility-version-check` cross-link (~line 186). | +| `scripts/generate-changelog.sh` | Fix the stale header-comment reference to `release-goreleaser.yml` (line 5) โ€” points at `nightly-build.yml` instead. | +| `CLAUDE.md` | **Edited by this PR, per explicit user authorization** โ€” remove the `utility-version-check` Skills-table row (line 261). Folded into Commit 5. | + +## Data Flow: Before vs. After + +**Before:** +``` +push to main (non-chore commit) + -> docker-build.yml runs (branch push trigger) + -> auto-versioning.yml runs (workflow_run: on docker-build.yml completion) + -> paulhatch/semantic-version computes next tag + -> shell/grep builds release body from commit messages + -> softprops/action-gh-release creates tag + GitHub Release + -> release-goreleaser.yml runs (tag push trigger) -> FAILS at PR-2 gate, publishes nothing + -> orthrus-build.yml runs (tag push trigger) -> builds + publishes semver-tagged orthrus image + -> auto-changelog.yml runs (release published trigger) -> release-drafter updates a draft release +``` -### Commit 4 โ€” `fix: replace tautological assertions in proxy-hosts and access-lists E2E specs` -- **Scope**: Sub-issue 2, the two largest remaining CRUD-flow files. ยง2.2's `proxy-hosts.spec.ts` (8 lines) and `access-lists-crud.spec.ts` (13 lines) breakdowns. -- **Files**: `tests/core/proxy-hosts.spec.ts`, `tests/security-enforcement/zzz-security-ui/access-lists-crud.spec.ts`. -- **Dependencies**: none (uses `getProxyHostsViaAPI`/`getProxyHostViaAPI`/`getAccessListViaAPI`, all already present in `tests/utils/api-helpers.ts` prior to this PR โ€” does not depend on Commit 1's `getBackupsViaAPI` addition). -- **Validation gate**: `npx playwright test tests/core/proxy-hosts.spec.ts --project=chromium --project=firefox` and the access-lists spec via the `security-tests` project (`npx playwright test tests/security-enforcement/zzz-security-ui/access-lists-crud.spec.ts --project=chromium` per the config's security-shard routing) both pass. Zero `|| true).toBeTruthy()` remaining in either file. +**After:** +``` +push to main (any commit) + -> docker-build.yml runs (branch push trigger, skips build body if chore:) [unchanged] + -> release-please.yml runs (push trigger, independent of docker-build.yml) + -> release-please-action opens/updates a standing "chore(main): release X.Y.Z" PR + (accumulates all releasable commits since the last tag; no PR yet if nothing releasable) + +[separately, whenever a human/bot merges that standing release PR] + -> release-please-action creates the git tag (v, bare per include-component-in-tag:false) + -> release-please-action creates the GitHub Release (skip-changelog:true, so CHANGELOG.md untouched) + -> orthrus-build.yml runs (tag push trigger) -> builds + publishes semver-tagged orthrus image [unchanged] + -> generate-changelog.sh (next nightly-build.yml run) picks up the new tag via `git tag -l 'v*'` [unchanged] +``` -### Commit 4b โ€” `fix: correct Access List UUID usage and CGNAT warning i18n keys` (SCOPE ADDITION โ€” real app bugs found during Commit 4) +## Error Handling / Edge Cases -**Why this exists**: Commit 4's strengthened `access-lists-crud.spec.ts` assertions (no longer tautological) surfaced two genuine, previously-invisible production defects, confirmed via curl repro + Playwright network trace + source grep (not test artifacts): +| Scenario | Behavior | +|---|---| +| No releasable commits since last tag (only `chore:`/`docs:`/`test:`/`ci:`/`build:`/`style:`/`refactor:` land on `main`) | Release-please does not open/update a release PR at all. **Behavior change from today**: `auto-versioning.yml` currently bumps patch for *any* non-`feat:` commit, so today a `chore:`-only week still cuts a release; under release-please, it won't. This is a deliberate, disclosed change (arguably a correctness improvement โ€” no more "no-op" patch releases), not an oversight. Flagged for user sign-off. | +| `feat!:`/`BREAKING CHANGE:` commit lands pre-1.0 | Bumps minor, not major, per Decision/Additional-Finding D's explicit config. | +| Release PR sits open for a long time while more commits land | Release-please updates the existing PR's body/diff in place (its documented standard behavior) โ€” no duplicate PRs. | +| Someone merges the release PR via squash instead of the "Create a merge commit"/default GitHub merge release-please expects | Not explicitly tested in this plan (CI-config-only, no live GitHub run possible locally) โ€” flagged in Manual Post-Merge Follow-Ups as the first thing to verify by watching the first real release PR merge. | +| `.release-please-manifest.json` drifts from the real latest tag (e.g., someone force-pushes a tag manually) | Release-please reads the manifest as its source of truth for "last released version," not live tag state โ€” a manual tag push outside release-please's flow would desync them. Document this in `VERSION.md`'s rewrite as "don't manually tag `v*` releases going forward; let release-please do it." | -1. **Access List edit/update/delete/test-IP all 404 in production.** `frontend/src/pages/AccessLists.tsx`, `frontend/src/hooks/useAccessLists.ts`, `frontend/src/api/accessLists.ts`, and the ACL selector in `frontend/src/components/.../ProxyHostForm.tsx` all key mutations off `acl.id`. `backend/internal/models/access_list.go`'s `ID uint` has `json:"-"` โ€” never serialized; only `uuid` is sent. Every edit/delete/rename request currently resolves to `PUT/DELETE /api/v1/access-lists/undefined` โ†’ `404`. `rowKey={(acl) => String(acl.id)}` also collides to `"undefined"` for every table row. `ProxyHosts.tsx` already uses the correct `.uuid` pattern โ€” mirror it. -2. **CGNAT warning banner renders raw i18n keys to every user.** `AccessLists.tsx` calls `t('accessLists.cgnatWarningTitle')` etc. (flat keys) but `frontend/src/locales/en/translation.json` only defines the nested `accessLists.cgnatWarning.title/.message/.solutionsTitle/.solution1-5`. The rendered DOM literally shows concatenated raw key strings to users. Fix: correct the key paths to match the nested structure. +# Implementation Plan -This is a deliberate, narrow deviation from this plan's original ยง1.3 non-goal ("no production code changes") โ€” made because leaving the 3 tests these bugs broke permanently `test.skip()`-ed would directly contradict sub-issue 2's entire purpose (replacing fake-always-pass checks with real ones that actually catch defects). Both fixes are small, isolated, high-confidence, and directly required for `access-lists-crud.spec.ts`'s already-committed real assertions to pass. This addition must be called out explicitly in the PR description as a scope note, separate from the planned test-infra-only work, so reviewers can evaluate it on its own merits. +This is a CI/CD-configuration-only change: no Go code, no TypeScript/React code, no database migrations, no API surface. Phases below are adapted accordingly from the standard template. -- **Scope**: `.id` โ†’ `.uuid` swap across the Access List frontend mutation path (param types `number` โ†’ `string` to match); i18n key path correction in the CGNAT warning block. -- **Files**: `frontend/src/pages/AccessLists.tsx`, `frontend/src/hooks/useAccessLists.ts`, `frontend/src/api/accessLists.ts`, and the ACL selector in the proxy-host form component (exact file to be confirmed at implementation time โ€” grep for `.id` usage against access-list objects). No backend changes (backend already correctly omits `ID` from JSON; frontend must conform to the existing contract, not the other way around). -- **Dependencies**: Commit 4 (the tests that currently fail because of these bugs must already exist). -- **Validation gate**: `npx playwright test tests/security-enforcement/zzz-security-ui/access-lists-crud.spec.ts --project=security-tests` โ€” full pass, all 45 tests, zero flaky retries (up from 42/45 after Commit 4). `cd frontend && npm run type-check` passes. `cd frontend && npx vitest run` โ€” zero regressions (no unit tests should reference the old `.id` access-list field, but confirm). Manual smoke check: rename an ACL via the UI, confirm no `undefined` appears in any network request URL. +## Phase 1: Playwright Tests (spec behavior) โ€” N/A -### Commit 5 โ€” `fix: replace tautological assertions in remaining security-UI and settings E2E specs` -- **Scope**: Sub-issue 2, remainder. ยง2.2's `encryption-management.spec.ts` (8), `system-security-settings.spec.ts` (7), `navigation.spec.ts` (4), `smtp-settings.spec.ts` (4), `dashboard.spec.ts` (3), `system-settings-feature-toggles.spec.ts` (1), `account-settings.spec.ts` (1), `manual-dns-provider.spec.ts` (1). -- **Files**: the 8 files above. -- **Dependencies**: none (none of these 8 files use `getBackupsViaAPI`). -- **Validation gate**: each file passes under its correct project (security-shard files via `security-tests`/chromium; the rest via chromium + firefox). Zero `|| true).toBeTruthy()` remaining anywhere under `tests/`, verified via `grep -rn "|| true" tests/ --include=*.spec.ts` returning empty. +No user-facing behavior changes; nothing to spec as `test.fixme`. Explicitly out of scope โ€” see "CLAUDE.md Definition-of-Done Applicability" below. -### Commit 5b โ€” `fix: add aria-current to active navigation links` (SCOPE ADDITION โ€” real app bug found during Commit 5) +## Phase 2: Backend Implementation โ€” N/A -**Why this exists**: same pattern as Commit 4b. Commit 5's strengthened `navigation.spec.ts` assertions (`hasActiveCurrent || hasActiveClass`, `hasAriaCurrent || `, both converged onto "must signal active state via aria-current OR a discoverable active class") surfaced that neither exists: `frontend/src/components/Layout.tsx`'s primary sidebar nav ``s never set `aria-current`, and the active-state Tailwind classes (`bg-brand-700 text-content-primary`, `text-brand-500`, `bg-brand-500/10 text-brand-500`) contain no `"active"`/`"current"` substring an assistive-tech-oriented check (or a screen reader) could key off. Confirmed reproducible 100% across 3 runs, both chromium and firefox, by the implementing agent โ€” not flakiness. This is a genuine, previously-hidden accessibility gap: there is no programmatic way for assistive tech to identify the current page in the primary nav today. +No `backend/` changes. -- **Scope**: Add `aria-current="page"` to the active nav `` in `Layout.tsx`, conditioned on the existing active-route check already used to apply the active Tailwind classes (do not introduce a new route-matching mechanism โ€” reuse whatever comparison already decides which link gets the active classes). -- **Files**: `frontend/src/components/Layout.tsx`. No backend changes. -- **Dependencies**: Commit 5 (the two navigation tests that currently fail because of this gap must already exist). -- **Validation gate**: `npx playwright test tests/core/navigation.spec.ts --project=chromium --project=firefox` โ€” full pass, including `'should highlight active navigation item'` and `'should indicate current page with aria-current'`, zero flaky retries. `cd frontend && npm run type-check` passes. `cd frontend && npx vitest run` โ€” zero regressions. +## Phase 3: Frontend Implementation โ€” N/A -### Commit 6 โ€” `docs: close out issue #619 sub-issues 3-5 with coverage/config verification evidence` -- **Scope**: Hardening + docs. No source changes beyond capturing verification evidence. Update `docs/features.md` only if any test-visible behavior description changed (unlikely โ€” confirm at implementation time; if nothing user-facing changed, skip the `docs/features.md` edit and note that explicitly in the PR description instead of forcing an edit for its own sake). -- **Files**: none required; optionally `docs/features.md` if applicable. -- **Dependencies**: Commits 2-5 (needs the final, real test suite to attach real evidence to). -- **Validation gate**: this commit's job *is* the Definition of Done run โ€” see ยง5.1 below. All gates must be green before this commit closes the PR. +No `frontend/` changes. -### 5.1 Full DoD validation (runs once, after Commit 5, evidence captured in Commit 6 / PR description) +## Phase 4: CI/CD Configuration Changes (replaces "Integration and Testing" for this chore) -Per `CLAUDE.md`'s Task Completion Protocol, in order: +- GOAL-001: Stand up the release-please config/manifest/workflow and validate every JSON/YAML file for syntactic correctness and internal consistency (manifest version matches real latest tag; config's package key matches manifest's package key). -1. `npx playwright test --project=firefox` (full suite) โ€” must pass. -2. `npx playwright test --project=chromium` (full suite, includes the `security-tests` shard) โ€” must pass. -3. `npx playwright test --project=webkit` (full suite) โ€” **this is sub-issue 5's outstanding confirming run.** If it fails in a way traceable to one of this PR's newly-real assertions (most likely candidate: the keyboard-focus-order ones flagged "(a) preferred, (b) fallback" in ยง2.2/ยง2.5), fix by falling back to the (b) disposition for that specific line with an accurate WebKit-specific skip reason โ€” do not weaken back to `|| true`. If it fails for an unrelated, pre-existing reason, that is a **new finding** outside this PR's original scope and must be flagged back to the user/issue tracker rather than silently patched. -4. `bash scripts/local-patch-report.sh` โ€” patch coverage evidence. -5. `lefthook run pre-commit` (CodeQL Go + JS, staticcheck, etc.) โ€” zero high/critical findings. (No GORM-touching changes in this PR, so ยง1.5's conditional GORM scan is skipped โ€” confirmed no `backend/internal/models/**` or migration changes.) -6. `make trivy` (or equivalent Trivy container/dependency scan) โ€” zero Critical/High findings. Per `CLAUDE.md`'s Task Completion Protocol step 3, this is **mandatory, zero-tolerance, with no conditional exception** (unlike the GORM scan above, which is explicitly conditional on model/migration changes). This PR touches no dependencies, `go.mod`/`package.json`, or any `Dockerfile`, so no new findings are expected โ€” run and capture as evidence rather than skipping it. -7. `scripts/go-test-coverage.sh` โ€” confirm โ‰ฅ85%, capturing the ยง2.3 numbers as evidence (no regressions expected since no backend files changed). -8. `scripts/frontend-test-coverage.sh` โ€” confirm โ‰ฅ85%, now including the ~84 newly-unskipped tests. -9. `cd frontend && npm run type-check`. -10. `cd backend && go build ./...` and `cd frontend && npm run build`. -11. Full `npx vitest run` โ€” zero failures, zero unexpected skips (only the 4 out-of-scope CrowdSec skips remain). +| Task | Description | Completed | Date | +|------|-------------|-----------|------| +| TASK-001 | Create `release-please-config.json` per Technical Specifications, with `include-component-in-tag: false`, `bump-minor-pre-major: true`, `bump-patch-for-minor-pre-major: true`, `skip-changelog: true` all set explicitly. | | | +| TASK-002 | Create `.release-please-manifest.json`, seeded to the actual latest `v*` tag at implementation time (re-verify, don't copy `0.36.5` blindly). | | | +| TASK-003 | Create `.github/workflows/release-please.yml`, pinned-SHA `googleapis/release-please-action`, `push: branches: [main]` trigger, `contents: write` + `pull-requests: write` permissions. | | | +| TASK-004 | Validate all three new/changed files with `jq empty` (JSON) / a YAML parser (`yamllint` or `python -c "import yaml,sys; yaml.safe_load(open(sys.argv[1]))"`) before commit. | | | -### 5.2 Rollback / contingency +## Phase 5: Retirement + Documentation -- Each commit is independently revertable without breaking `development` โ€” none introduce cross-file coupling beyond Commit 1's helper (used only by Commit 3+). -- If the WebKit run (ยง5.1 step 3) surfaces a **pre-existing, unrelated** failure (not caused by this PR's changes), the contingency is: do not block this PR on it โ€” capture the failure, note it explicitly in the PR description as a newly-discovered, out-of-scope finding, and open a follow-up issue (matching the precedent set by this same investigation's sibling fix, which filed `#1221` for an out-of-scope race condition rather than scope-creeping the original fix). -- If any single sub-issue-2 file proves substantially harder than estimated during implementation (most likely: `certificates.spec.ts`'s custom-modal rewrite), it is already isolated to its own commit (Commit 3) specifically so it can be iterated on without blocking Commits 4-5. -- If full-suite Vitest coverage drops below 85% after unskipping (unlikely, since unskipping only adds passing tests, never removes coverage), do not merge โ€” investigate whether any of the newly-active tests are masking a real component defect (per Root Cause Analysis Protocol) rather than adjusting the threshold. +- GOAL-002: Remove the superseded workflows/config/scripts and rewrite `VERSION.md` to describe the new flow accurately. ---- +| Task | Description | Completed | Date | +|------|-------------|-----------|------| +| TASK-005 | Delete `.github/workflows/auto-versioning.yml`, `.github/workflows/release-goreleaser.yml`, `.goreleaser.yaml`. | | | +| TASK-006 | Delete `.github/workflows/auto-changelog.yml`, `.github/release-drafter.yml`. | | | +| TASK-007 | Delete `.version`, `scripts/check-version-match-tag.sh`, `.github/skills/utility-version-check-scripts/run.sh`, `.github/skills/utility-version-check.SKILL.md`; remove the `check-version-match` entry from `lefthook.yml`. | | | +| TASK-008 | Remove the `# GoReleaser` section from `.gitignore`; remove the `.goreleaser.yaml` line from `.dockerignore`. | | | +| TASK-009 | Rewrite `VERSION.md` per Technical Specifications. | | | +| TASK-010 | Run `lefthook run pre-commit` to confirm no hook references a now-deleted file/glob and everything still passes. | | | -## 6. PR Description Scaffolding - -```markdown -## Summary - -Closes #619 (Phase 3 Technical Debt Issues). Verifies and resolves all 5 bundled sub-issues: - -- **Sub-issue 1 (undici/WebSocket jsdom blocker)** โ€” FIXED. Confirmed stale on jsdom@30.0.1/undici@8.10.0 - (upstream nodejs/undici#1671 long resolved). Unskipped 83 tests across 5 Security.*.test.tsx suites. - The 6th related skip (Security.functional.test.tsx notification-modal test) was not a WebSocket issue at - all โ€” root-caused to stale test code describing a modal that was replaced by a router navigation; deleted - as dead code since equivalent, correct, passing coverage already exists in the same file. -- **Sub-issue 2 (weak/tautological E2E assertions)** โ€” FIXED. 59 `expect(x || true).toBeTruthy()` occurrences - across 11 spec files replaced with real deterministic assertions, backend-state-verified checks (certificate - deletion in-use/backup/cancel flows, ACL rename, proxy-host creation), or explicit `test.skip()` calls with - accurate reasons where genuinely environment-dependent โ€” reusing this repo's existing skip convention. - certificates.spec.ts's certificate-deletion tests additionally required a root-cause interaction-model fix: - they drove a native `window.confirm()` that the app no longer uses (replaced by a custom React modal), - meaning they were exercising almost none of the real delete flow. -- **Sub-issue 3 (backend coverage gaps)** โ€” STALE, already resolved, no code changes. Re-verified: - internal/services 88.4% (target 85%), remotestorage 90.3%, backend/pkg/dnsprovider/builtin 91.8% - (target 50% incremental). -- **Sub-issue 4 (feature flag async propagation tests)** โ€” STALE, already resolved, no code changes. - Re-verified: tests/security/system-settings-feature-toggles.spec.ts already uses - waitForFeatureFlagPropagation() at 9 call sites, zero .skip/.fixme. -- **Sub-issue 5 (WebKit E2E not executing)** โ€” Config confirmed healthy (963 tests / 86 files discovered, - no webkit-specific exclusions or browserName-conditioned skips). Full passing run captured as this PR's - DoD evidence (see Test Plan). - -## Test Plan -- [ ] Full `npx vitest run` โ€” zero failures, only the 4 out-of-scope CrowdSecBouncerKeyDisplay skips remain -- [ ] `npx playwright test --project=chromium` (incl. security-tests shard) โ€” full pass -- [ ] `npx playwright test --project=firefox` โ€” full pass -- [ ] `npx playwright test --project=webkit` โ€” full pass (sub-issue 5 confirming run) -- [ ] `scripts/go-test-coverage.sh` โ‰ฅ 85% -- [ ] `scripts/frontend-test-coverage.sh` โ‰ฅ 85% -- [ ] `lefthook run pre-commit` โ€” zero high/critical CodeQL findings -- [ ] `make trivy` (or equivalent) โ€” zero Critical/High findings -- [ ] `grep -rn "|| true" tests/ --include=*.spec.ts` returns empty -``` +# CLAUDE.md Definition-of-Done Applicability ---- +This PR is CI-config/YAML/JSON/Markdown-only. Mapped explicitly against the standard DoD: -## 7. Acceptance Criteria - -1. Zero `describe.skip`/`it.skip` remain in the 6 sub-issue-1 files except the intentional deletion (not skip) of the stale notification-modal test. -2. `grep -rn "|| true).toBeTruthy()" tests/ --include=*.spec.ts` (or equivalent pattern check) returns **zero** matches. -3. Every occurrence converted to `test.skip()` includes a specific, accurate reason string (no generic "may not apply" left over from the tautology comments). -4. `certificates.spec.ts`'s 5 deletion tests interact with the real custom `DeleteCertificateDialog` modal, not a native `confirm()`. -5. `tests/utils/api-helpers.ts` gains exactly one new function (`getBackupsViaAPI`), matching existing conventions. -6. Full Vitest suite: 0 failures, coverage โ‰ฅ 85%. -7. Full Playwright suite on chromium, firefox, **and** webkit: 0 failures (or any webkit-specific failures are explicitly triaged per ยง5.2's contingency, not silently skipped). -8. Backend coverage unchanged and re-confirmed โ‰ฅ targets (no backend files touched). -9. `lefthook run pre-commit` clean. -10. PR description matches the ยง6 scaffolding, giving issue #619 a complete, accurate paper trail per sub-issue. -11. No changes to `.gitignore`, `.dockerignore`, `codecov.yml`, or any `Dockerfile`. +| DoD Item | Applies? | Notes | +|---|---|---| +| 1. Playwright E2E Tests | **N/A** | No user-facing behavior; no frontend/backend code path changes. | +| 1.5. GORM Security Scan | **N/A** | No `backend/internal/models/**`, no GORM queries/migrations touched. | +| 2. Local Patch Coverage Preflight (`scripts/local-patch-report.sh`) | **Run it anyway** | It's mandatory regardless of change type per CLAUDE.md; expect it to report ~0% "patch" surface since no `.go`/`.ts`/`.tsx` lines are touched โ€” confirm it doesn't error out on a code-less diff rather than skip it. | +| 3. Security Scans (CodeQL/Trivy) | **Defer to CI** | This is a `chore:`-scoped change with no new application code path โ€” per CLAUDE.md's own rule ("Defer to CI for fix/test/chore/refactor-scoped changes with no new feature surface"), do not run these locally; CI runs both unconditionally regardless. **Caveat**: neither CodeQL nor Trivy actually provides coverage for this PR's one genuinely new risk surface โ€” `googleapis/release-please-action` is a new third-party Action granted `contents: write` + `pull-requests: write` on this repo. CodeQL scans Go/JS source; Trivy scans container images/dependencies; neither evaluates GitHub Actions permission scopes or third-party Action supply-chain trust. "Defer to CI" is accurate for what those two tools actually check, but should not be read as "this PR's permissions posture is covered" โ€” the mitigation here is the existing pinned-SHA convention (matching every other Action reference in this repo) plus the fact that the sibling repo already runs the identical Action/permissions combination without incident, not CodeQL/Trivy. | +| 4. Lefthook Triage | **Applies** | Run `lefthook run pre-commit` โ€” should no-op past YAML/JSON formatting-class hooks (no `.go`/`.ts` glob matches), but must still pass cleanly, especially after removing the `check-version-match` hook entry (verify lefthook config itself is still valid YAML). | +| 5. Staticcheck | **N/A** (no-op) | No `.go` files touched; hook glob won't match anything. | +| 6. Coverage Testing (85% backend/frontend) | **N/A** | No code touched; nothing for `go-test-coverage.sh`/`frontend-test-coverage.sh` to measure against this diff. | +| 7. Type Safety (`npm run type-check`) | **N/A** | No `.ts`/`.tsx` files touched. | +| 8. Verify Build (`go build`, `npm run build`) | **Recommended as a sanity check, not a real gate** | Neither build path is touched by this diff; running them just confirms the repo wasn't already broken. Not blocking for this PR specifically. | +| 9. Fixed/New Code Testing | **N/A** | No unit-testable code changed. | +| 10. Clean Up (debug prints, dead code) | **Applies in spirit** | The primary output of this PR *is* dead-code removal (Decisions 4-6, Additional Finding A) โ€” this is effectively the main content of the PR, not a final pass. | + +**The real gates for this PR** (called out explicitly per the task brief, since the standard DoD doesn't fit a CI-config change well): +- Every new/modified JSON file parses (`jq empty release-please-config.json .release-please-manifest.json`). +- Every new/modified YAML file parses (`.github/workflows/release-please.yml`, and re-validate `lefthook.yml` after the hook-entry removal). +- `lefthook run pre-commit` passes cleanly end-to-end. +- **GitHub Actions workflow behavior (does `release-please.yml` actually open a correct PR, does merging it actually tag+release correctly, does `orthrus-build.yml` actually still fire on that tag) can only be fully verified by a live run on GitHub after merge โ€” not locally, not in this plan.** This is the single biggest residual-risk category for this PR and is why the Manual Post-Merge Follow-Ups section below exists. + +# Manual Post-Merge Follow-Ups (cannot be validated without a live push to GitHub) + +1. **Confirm repo-level permissions**: verify `pull-requests: write` is actually honored for the `GITHUB_TOKEN` used by Actions in this repo (Settings โ†’ Actions โ†’ General โ†’ Workflow permissions). If the repo/org default is read-only, the `release-please.yml` workflow's explicit `permissions:` block should override it, but confirm on the first run rather than assume. +2. **Watch the first `release-please.yml` run on `main`** after this PR merges: confirm it either (a) opens a `chore(main): release X.Y.Z` PR if there are releasable commits since `v0.36.5`, or (b) does nothing cleanly if there aren't โ€” don't assume silence means broken. +3. **Verify tag format on the first real release**: confirm the tag release-please creates is exactly `v` (no `charon-` prefix) โ€” this is the `include-component-in-tag: false` risk called out in Additional Finding C. If it's wrong, `orthrus-build.yml`'s `tags: ['v*']` trigger and `generate-changelog.sh`'s `git tag -l 'v*'` scan both silently stop matching. +4. **Verify `CHANGELOG.md` is untouched** by the first release-please PR (confirms `skip-changelog: true` behaves as expected) **and** verify the resulting GitHub Release still gets a populated body (confirms release-notes generation is independent of the changelog-file write path โ€” this exact interaction wasn't found explicitly documented during this session's research). +5. **Verify `orthrus-build.yml` still fires** on the first release-please-created tag and produces the expected `type=semver` Docker tags. +6. **Branch protection interaction**: check whether `main`'s branch protection rules (required reviews, required status checks) block release-please's own bot-authored release PR from being merged, and if so, decide whether to exempt it or just merge manually each time (release-please doesn't require any special exemption to function โ€” it just opens a normal PR). +7. **Decide who merges the release PR and how** (manual click each time vs. some auto-merge label) โ€” this plan does not configure auto-merge; that's a deliberate choice left to the user, since it directly controls when a real release goes out. This is the operational follow-through on the "User Decisions Required Before Implementation" #1 go/no-go above: if the user decides fully-automatic cadence is a hard requirement, a follow-up PR adding a release-please auto-merge label + workflow would be needed โ€” not something this plan implements. +8. **Clean up historical run records** (optional): the deleted workflows' historical Action run logs remain visible under "Actions" until manually deleted/archived if desired โ€” cosmetic only, not required. +9. **Sibling-repo note**: `/projects/go_notify_yourself/release-please-config.json` was independently corrected to `"release-type": "go"` during this session (see Reference Implementation section) โ€” no action needed here, noted only so the user isn't surprised by the diff if they look at that repo later. + +# Acceptance Criteria + +- [ ] `release-please-config.json`, `.release-please-manifest.json`, and `.github/workflows/release-please.yml` exist, are valid JSON/YAML, and match the Technical Specifications section (including the explicit `include-component-in-tag`, `bump-minor-pre-major`, `bump-patch-for-minor-pre-major`, `skip-changelog` settings). +- [ ] `.github/workflows/auto-versioning.yml`, `.github/workflows/release-goreleaser.yml`, `.goreleaser.yaml`, `.github/workflows/auto-changelog.yml`, `.github/release-drafter.yml`, `.version`, `scripts/check-version-match-tag.sh`, and the two `utility-version-check` skill files are all deleted. +- [ ] `.github/skills/README.md`, `.vscode/tasks.json`, and `.github/skills/utility-bump-beta.SKILL.md` no longer reference the deleted `utility-version-check` skill (Decision 4 blast-radius items). +- [ ] `CLAUDE.md:261`'s `utility-version-check` Skills-table row is removed in this PR (Commit 5), per the user's explicit authorization to edit CLAUDE.md as part of this migration. +- [ ] `lefthook.yml` no longer references `check-version-match-tag.sh`; `lefthook run pre-commit` passes. +- [ ] `.gitignore` and `.dockerignore` no longer reference GoReleaser artifacts/config. +- [ ] `scripts/generate-changelog.sh`'s header comment no longer references the deleted `release-goreleaser.yml`. +- [ ] `VERSION.md` accurately describes the release-please PR-based flow and no longer references `release-goreleaser.yml`, `.version` as a release trigger, or the release-drafter workflow. +- [ ] `ARCHITECTURE.md`'s "Release Workflow" section accurately describes the release-please push-to-main โ†’ standing-PR โ†’ merge-to-tag flow and no longer references `release-goreleaser.yml` or a tag-push trigger as step 1. +- [ ] `orthrus-build.yml` and `docker-build.yml` are **not modified** by this PR (both confirmed to need no changes per Decision 2). +- [ ] No Go, TypeScript, or database-schema files are touched. +- [ ] PR is opened from `chore/release-please-migration` against `main` (not `development`), with the branching deviation explicitly called out in the PR description per the Branching Note above, and merged via squash merge (per the Merge Strategy note in the Introduction). +- [ ] All commits use `chore:` (or `chore(ci):`) Conventional Commit prefixes, matching CLAUDE.md's CI-trigger convention (so this PR's own merge does not trigger a Docker build). +- [x] The user has explicitly signed off on both items in "User Decisions Required Before Implementation" (manual release-cadence gating: APPROVED; chore-only-weeks/`perf:`-no-longer-releasable: ACCEPT release-please defaults, no special-case override) โ€” see that section for the recorded decisions. +- [x] The user has explicitly authorized editing `CLAUDE.md` as part of this PR to remove the stale `utility-version-check` Skills-table row (line 261), folded into Commit 5 โ€” see Decision 4's blast-radius table. + +# Commit Slicing Strategy + +**Decision: single PR, `chore/release-please-migration` โ†’ `main`, with ordered logical commits.** Per CLAUDE.md's "One Feature = One PR" rule โ€” this is one cohesive infrastructure change and must not be split across multiple PRs (e.g., "add release-please" in one PR and "remove old workflows" in another would leave the repo running two competing release mechanisms simultaneously in the gap between merges, which is strictly worse than doing it atomically). + +| Commit | Scope | Files | Depends on | Validation gate | +|---|---|---|---|---| +| **1** | Add release-please config + manifest (no workflow yet โ€” inert until Commit 2) | `release-please-config.json`, `.release-please-manifest.json` | โ€” | `jq empty release-please-config.json .release-please-manifest.json`; manually diff the manifest's seed version against `git tag --sort=-v:refname \| head -1` to confirm it's current. | +| **2** | Add the release-please workflow | `.github/workflows/release-please.yml` | Commit 1 | YAML parses; `lefthook run pre-commit` passes; manual read-through confirming the pinned action SHA/tag comment matches the sibling repo's convention and is a real, current release. | +| **3** | Retire the superseded auto-versioning/GoReleaser pipeline | `.github/workflows/auto-versioning.yml` (delete), `.github/workflows/release-goreleaser.yml` (delete), `.goreleaser.yaml` (delete), `.gitignore` (remove GoReleaser section), `.dockerignore` (remove `.goreleaser.yaml` line), `scripts/generate-changelog.sh` (fix stale header-comment reference at line 5 from `release-goreleaser.yml` to `nightly-build.yml`, its one remaining real caller) | Commits 1-2 (don't remove the old path until the new one exists) | `lefthook run pre-commit` passes; confirm no remaining reference to `.goreleaser.yaml` anywhere (`grep -rn goreleaser --include=*.yml --include=*.md .` minus this plan file itself); confirm `generate-changelog.sh` still runs correctly after the comment-only edit (no functional change, but re-run it locally against a small tag range as a sanity check). | +| **4** | Retire the redundant release-drafter changelog automation | `.github/workflows/auto-changelog.yml` (delete), `.github/release-drafter.yml` (delete) | Commit 2 (release-please must exist as the replacement before removing this) | `lefthook run pre-commit` passes. | +| **5** | Retire the legacy `.version` parity check and its full reference surface | `.version` (delete), `scripts/check-version-match-tag.sh` (delete), `.github/skills/utility-version-check-scripts/run.sh` (delete), `.github/skills/utility-version-check.SKILL.md` (delete), `lefthook.yml` (remove `check-version-match` hook entry), `.github/skills/README.md` (remove the `utility-version-check` row from the Utility Skills table at ~line 72, and the `utility-version-check` bullet from the naming-convention examples at ~line 267), `.vscode/tasks.json` (remove the "Utility: Check Version Match Tag" task block, ~lines 694-698), `.github/skills/utility-bump-beta.SKILL.md` (remove the dead `utility-version-check` cross-link from its "Related Skills" section, ~line 186, keep the rest of that list), `CLAUDE.md` (remove the `utility-version-check` row from the Skills table at ~line 261 โ€” per explicit user authorization to edit CLAUDE.md as part of this PR) | Commit 1 (manifest is the intended replacement source of truth) | `lefthook run pre-commit` passes with no dangling glob/hook referencing a deleted script; confirm `lefthook.yml` and `.vscode/tasks.json` are still valid YAML/JSON respectively; confirm `CLAUDE.md` remains valid Markdown with only the one table row removed, no other governance text touched; `grep -rn "utility-version-check\|check-version-match-tag" .` (excluding this plan file and `.git/`) returns **zero** hits anywhere, including `CLAUDE.md`. | +| **6** | Documentation rewrite | `VERSION.md` (full rewrite per Technical Specifications), `ARCHITECTURE.md` (rewrite the "Release Workflow" section per Modified Files above: replace tag-push-triggers-everything framing with the release-please flow, fix the `generate-changelog.sh`/`release-goreleaser.yml` reference) | Commits 1-5 (must describe the end state, not the transition) | Manual proofread against the final state of every file above; confirm no reference to any deleted file/workflow remains in either doc. | + +**Rollback / contingency for the PR as a whole**: since this is entirely additive-then-subtractive CI configuration with no code or schema changes, rollback is a plain `git revert` of the merge commit (or of the whole PR range) โ€” no data migrations, no forward-only state changes are introduced. The one piece of *external* (not-in-git) state this PR's downstream behavior touches is the standing release-please PR itself and any tag it creates after merge; if the migration needs to be rolled back after a real release-please release has already gone out, `.release-please-manifest.json` should be re-seeded to match whatever the real latest tag is at rollback time (not blindly reverted to the pre-migration value), and the old `auto-versioning.yml`/`release-goreleaser.yml` files restored via revert will resume exactly their prior (partially broken) behavior with no additional cleanup needed, since neither of them depended on anything release-please would have introduced. + +# Dependencies + +- **DEP-001**: `googleapis/release-please-action` (pinned by SHA, `# v5` comment) โ€” new external GitHub Action dependency, matching the pattern already trusted in `/projects/go_notify_yourself`. +- **DEP-002**: No new npm/Go module dependencies. No `package.json`/`go.mod` changes. + +# Risks & Assumptions + +- **RISK-001**: `include-component-in-tag` default-vs-explicit-`false` mismatch could produce a wrongly-prefixed tag on the first real release, silently breaking `orthrus-build.yml`'s trigger and `generate-changelog.sh`'s tag scan until noticed. Mitigated by explicit config (Additional Finding C) and flagged as the #1 manual-verification item post-merge. +- **RISK-002**: `skip-changelog: true`'s interaction with GitHub Release notes generation is not fully documented in the sources available this session โ€” small chance the Release body comes out empty rather than independently populated. Flagged in Manual Post-Merge Follow-Ups. +- **RISK-003**: Branch protection on `main` could block release-please's bot-authored release PR from merging cleanly (required reviewers, required status checks that don't apply to a docs/manifest-only PR). Flagged in Manual Post-Merge Follow-Ups; no code change can pre-empt this, it must be checked live. +- **RISK-004**: This PR targets `main` directly, bypassing the normal `development` โ†’ `nightly` โ†’ `main` soak cycle by design (see Branching Note). Slightly higher blast-radius-per-mistake than the repo's usual flow, mitigated by the fact that the change is inert until the *next* real release-worthy commit lands on `main` (release-please won't retroactively do anything to already-tagged history). +- **ASSUMPTION-001**: The latest tag at plan-authoring time (`v0.36.5`) is still the latest tag at implementation time. Re-verify before seeding `.release-please-manifest.json` (explicitly called out as an implementer task, not assumed). +- **ASSUMPTION-002** (revised โ€” the original wording overclaimed completeness; corrected per Supervisor's independent re-run of the same grep, which surfaced two more references this plan now accounts for rather than leaves implicit): a repo-wide grep for `auto-versioning`, `release-goreleaser`, `CHARON_PR2_GATES_PASSED`, `softprops/action-gh-release`, and `paulhatch` does **not** guarantee full coverage of every reference to the files this plan deletes โ€” it only checked those specific literal strings, and it missed `ARCHITECTURE.md` (now added to Modified Files above) and a stale header comment in `scripts/generate-changelog.sh:5` (`"see .github/workflows/release-goreleaser.yml"` โ€” functionally harmless, since the script's actual behavior doesn't depend on GoReleaser, but a dead pointer once that workflow is deleted; corrected as part of Commit 3, since it's tied directly to the GoReleaser removal). Grep-based "nothing else references this" claims in this plan should be read as "no hits for the specific strings searched," not as an exhaustive guarantee โ€” the actual assumption being made is that the Research Findings section's enumerated consumer list is complete, which was cross-checked by Supervisor's independent review and found to need these two additions plus the Decision 4 blast-radius additions above, and no others. + +# Related Specifications / Further Reading + +- `/projects/go_notify_yourself/release-please-config.json`, `.release-please-manifest.json`, `.github/workflows/release-please.yml` โ€” reference implementation. +- `docs/reports/caddy-security-posture.md` โ€” origin/closure evidence for the "PR-2" gate (Decision 6). +- release-please documentation: `docs/customizing.md`, `docs/design.md`, `schemas/config.json` in `googleapis/release-please` (all fetched and cited directly in this plan). diff --git a/docs/reports/archive/2026-08-14_semgrep-ci-security-scan-qa_report.md b/docs/reports/archive/2026-08-14_semgrep-ci-security-scan-qa_report.md new file mode 100644 index 000000000..46f5606ec --- /dev/null +++ b/docs/reports/archive/2026-08-14_semgrep-ci-security-scan-qa_report.md @@ -0,0 +1,108 @@ +# QA Report โ€” Semgrep CI Security Scan (Independent Verification) + +**Branch**: `development` +**Commits reviewed**: `6bf066f8`, `2fbecf07`, `7c6fb04f` +**Reviewed by**: qa-security agent +**Date**: 2026-08-14 +**Scope**: CI/CD-only feature โ€” no application code, models, or frontend/UI surface touched. +**Prior review**: Supervisor code review โ€” approved, no blocking issues. +**Purpose**: Independent functional/security verification per Phase 6 of the management pipeline, ahead of a final "done" determination. + +## Summary Verdict: **PASS** โ€” no blocking defects found. + +The Semgrep CI gate is functionally real (confirmed to fail on findings and pass when clean, via a positive-control test), the parity guard genuinely detects drift (confirmed via four separate intentional-break tests), the container image is correctly digest-pinned, and all local DoD-relevant checks scoped to a CI/shell-script-only change are clean. Two pre-existing environment/documentation gaps were identified and are explicitly **not** attributed to this feature (see ยง4 and ยง6). + +--- + +## 1. Functional Correctness of the Scan (PASS) + +Installed Semgrep 1.173.0 into a throwaway venv (`/tmp/.../scratchpad/semgrep-venv`, exact version match to the pinned CI image) and ran the actual wrapper script `scripts/pre-commit-hooks/semgrep-scan.sh` exactly as CI invokes it. + +| Check | Result | +|---|---| +| `SEMGREP_SARIF_OUTPUT= bash scripts/pre-commit-hooks/semgrep-scan.sh` (full repo, no targets) | Exit 0. Produced a valid SARIF file (`version`, `runs`, `results`, `$schema` present; parsed cleanly as JSON). | +| `bash scripts/pre-commit-hooks/semgrep-scan.sh` (no env var, full repo) | Exit 0. `--error` semantics confirmed live (see ยง1.1). | +| Repo clean under full scan | Reproduced: 974 files tracked by git, 160 rules run, **0 findings** โ€” matches both prior QA/DevOps reports exactly. Two suppressed (`nosemgrep`-annotated) `websocket-missing-origin-check` findings appear in the SARIF's `results` array with `suppressions: [{kind: inSource}]` โ€” this is correct SARIF behavior (audit trail for suppressed findings) and does not affect the "0 findings / 0 blocking" scan summary or exit code. | +| Runtime | ~45โ€“48s per full-repo pass locally (single-threaded venv on this sandbox; CI's dedicated `semgrep/semgrep` container should be comparable or faster). | + +### 1.1 Positive-control test: does the gate actually gate? (Most important check โ€” PASS) + +Constructed a minimal Go file containing an unguarded `websocket.Upgrader{}.Upgrade()` call (the same rule ID, `go.gorilla.security.audit.websocket-missing-origin-check`, that appears โ€” suppressed โ€” in the real codebase), and ran it through the **actual, unmodified** wrapper script with a single-file target: + +``` +SEMGREP_SARIF_OUTPUT=out.sarif bash scripts/pre-commit-hooks/semgrep-scan.sh + โ†’ Findings: 1 (1 blocking) โ†’ exit 0 (SARIF mode does not hard-fail) + +bash scripts/pre-commit-hooks/semgrep-scan.sh + โ†’ Findings: 1 (1 blocking) โ†’ exit 1 (--error mode hard-fails) +``` + +This is the critical distinction the task flagged as the top risk: a gate that always exits 0 regardless of findings would be a silent no-op. **Confirmed not the case.** The `SEMGREP_SARIF_OUTPUT` toggle in `scripts/pre-commit-hooks/semgrep-scan.sh:42-46` genuinely swaps `--error` for `--sarif --output `, and only the `--error` invocation (the CI workflow's "hard-fail gate" step, `semgrep.yml:75-76`) enforces blocking. The SARIF-producing pass (`semgrep.yml:49-54`) is additionally wrapped in `continue-on-error: true` at the workflow level, which is defense-in-depth on top of the script's own non-blocking `--sarif` exit code โ€” belt and suspenders, not a substitute for the real gate. + +--- + +## 2. Workflow YAML Structural Validity (PASS) + +- `actionlint .github/workflows/semgrep.yml` (installed via `go install github.com/rhysd/actionlint@latest` into a throwaway `GOBIN`): **0 findings, exit 0.** +- Container image resolution: `docker buildx imagetools inspect semgrep/semgrep:1.173.0@sha256:67319956da3dcb58baf5b322899c15458e3963e7018a86aeeb5cd224e69cb77a` (the exact digest read fresh from the committed file, `semgrep.yml:33`) resolved successfully against the registry, returning a multi-platform manifest list whose index digest matches the pinned digest exactly. The pin is real and correct, not a stale/copy-pasted digest. + +--- + +## 3. Parity Guards (PASS, and confirmed non-trivial) + +- `bash scripts/ci/check-semgrep-parity.sh` โ€” exit 0. +- `bash scripts/ci/check-codeql-parity.sh` โ€” exit 0 (unaffected by the refactor that extracted `scripts/ci/lib/workflow-yaml-asserts.sh`; this file was verified as a correct behavior-preserving extraction, not a modification of the CodeQL guard's assertions). + +**Adversarial drift tests** (performed against throwaway copies in `/tmp/.../scratchpad/parity-break-test/`, never against the real repo files; all discarded after each test, working tree confirmed clean of these edits afterward): + +| Simulated drift | Guard result | +|---|---| +| Inline `semgrep scan --config p/golang` reintroduced in place of `bash scripts/pre-commit-hooks/semgrep-scan.sh` delegation | **Caught.** `must delegate to scripts/pre-commit-hooks/semgrep-scan.sh instead of reimplementing the semgrep scan invocation inline` โ€” exit 1. | +| `SEMGREP_SARIF_OUTPUT` hook deleted from the local script | **Caught.** `must retain the SEMGREP_SARIF_OUTPUT hook so CI can produce SARIF via the same script` โ€” exit 1. | +| Image pin degraded from `semgrep/semgrep:1.173.0@sha256:...` to `semgrep/semgrep:latest` | **Caught.** `must pin the semgrep/semgrep image with both an exact tag and a sha256 digest` โ€” exit 1. | +| `pull_request` branch list narrowed from `[main, nightly, development]` to `[main, nightly]` | **Caught.** `pull_request branches must be [main, nightly, development]` โ€” exit 1. | + +All four drift classes are detected. The guard is a real structural check, not a no-op that always passes. + +--- + +## 4. Local DoD-Relevant Checks (PASS, with one noted pre-existing environment gap) + +Scoped per the task's guidance: no Playwright E2E (no user-facing behavior), no GORM scan (confirmed zero files under `backend/internal/models/**` or any `.go` files touched โ€” `git diff --name-only c510085f 7c6fb04f` shows only workflow/doc/shell files), no frontend type-check/build (zero `frontend/` files touched). + +- **shellcheck** (installed a static v0.10.0 binary into a throwaway location, no sudo/apt available) on all touched/new shell scripts (`semgrep-scan.sh`, `check-semgrep-parity.sh`, `workflow-yaml-asserts.sh`, `check-codeql-parity.sh`), using the project's actual severity threshold from `lefthook.yml` (`shellcheck --severity=error`): **0 findings.** (Default-severity mode surfaces two SC1091 "info"-level "not following sourced file" notices caused by the scripts' dynamic `SCRIPT_DIR` resolution pattern โ€” expected and filtered out by the repo's own configured threshold, not a defect.) +- **`lefthook run pre-commit`**, targeted against this feature's exact changed-file set (`--file <7 files>`, since nothing was staged in this session): all hooks pass โ€” `trailing-whitespace`, `end-of-file-fixer`, `actionlint`, `check-lfs-large-files`, `block-codeql-db`, `block-data-backups`, `semgrep` (0 findings, `semgrep` installed into PATH via the throwaway venv for this run only). One hook, **`check-yaml`, failed** with `ModuleNotFoundError: No module named 'yaml'` โ€” **confirmed pre-existing/environmental, not introduced by this feature**: the hook shells out to system `python3 -c "import yaml..."` (`lefthook.yml:54`), and this sandbox's system Python lacks PyYAML. Verified by installing PyYAML into the throwaway venv and re-running the identical parse command directly against `.github/workflows/semgrep.yml` โ€” it parsed cleanly (exit 0), proving the YAML itself is valid and the failure is purely a missing sandbox dependency, structurally identical to the already-documented `gitleaks`-unavailable gap below. +- **`gitleaks`**: confirmed absent from PATH (`which gitleaks` โ†’ exit 1) in this sandbox, matching the DevOps report. This predates the Semgrep feature entirely (a secrets-scanning tool unrelated to Semgrep) and is not something this feature could plausibly mask โ€” the feature adds zero new secret-bearing surface (see ยง5). + +--- + +## 5. Security-Specific Checks (PASS) + +- **`permissions:`** โ€” both the workflow-level and job-level blocks are exactly `contents: read`, `security-events: write`, `actions: read`, `pull-requests: read` โ€” least-privilege, no broader scope (no `write` on `contents`, no `id-token`, no `packages`, etc.). +- **Triggers** โ€” `pull_request` (not `pull_request_target`) confirmed at `semgrep.yml:4`. No fork-PR privilege-escalation risk. +- **Secrets/tokens** โ€” `grep -in "secrets\.\|token\|GITHUB_TOKEN"` across `semgrep.yml`, `semgrep-scan.sh`, `check-semgrep-parity.sh`, `workflow-yaml-asserts.sh` returns no matches (the only "token" hits were unrelated word fragments in comments, none present). The SARIF-upload step uses `github/codeql-action/upload-sarif`, which relies on the workflow's implicit default `GITHUB_TOKEN` scoped by the `permissions:` block above โ€” no custom secret is declared or required anywhere in this feature. +- **Container image pin** โ€” digest-pinned (`semgrep/semgrep:1.173.0@sha256:67319956...`), confirmed resolvable and matching the registry (ยง2). Not a floating tag. + +--- + +## 6. Documentation Lint (PASS โ€” no regression introduced) + +Ran `markdownlint-cli2@0.23.2` (the exact tool/version pinned in `package.json`, not the unrelated `markdownlint-cli` package) against `SECURITY.md` and `ARCHITECTURE.md`. + +- **SECURITY.md**: 114 findings (MD036 emphasis-as-heading, MD060 table-column-style, one MD034 bare-URL) โ€” **identical count before and after** commit `7c6fb04f` (verified via `git show 7c6fb04f~1:SECURITY.md` piped through the same linter: 114 findings on the pre-commit version too). All findings are pre-existing formatting debt scattered across unrelated CVE-entry sections (lines 407โ€“813); zero findings land in the new Semgrep paragraph or table row this PR added (~line 1000โ€“1032). +- **ARCHITECTURE.md**: 93 findings, likewise identical before/after `7c6fb04f`. + +This PR's doc changes introduce zero new lint findings. Pre-existing lint debt is a separate, out-of-scope cleanup item and not this feature's responsibility. + +--- + +## Blocking Issues + +**None.** + +## Non-Blocking Observations (informational only, no action required for this PR) + +1. `check-yaml` and `gitleaks` are unavailable in this local sandbox due to missing system dependencies (PyYAML, gitleaks binary). Both are pre-existing environment gaps unrelated to this feature; CI's environment has these tools installed and is authoritative. No masking risk identified โ€” the underlying YAML was independently verified valid, and this feature introduces no new secret-bearing surface for `gitleaks` to have caught. +2. SECURITY.md/ARCHITECTURE.md carry substantial pre-existing markdownlint debt (207 combined findings) unrelated to this PR. Worth a future standalone cleanup pass, but explicitly out of scope here per this feature's CI/shell-script-only mandate. + +## Final Overall Verdict: **PASS โ€” ready to be marked done.** diff --git a/docs/reports/qa_report.md b/docs/reports/qa_report.md index ae96a722d..5c6a23fd8 100644 --- a/docs/reports/qa_report.md +++ b/docs/reports/qa_report.md @@ -92,3 +92,76 @@ Per the now-codified CI-only policy for full-suite/multi-browser Playwright runs 3. Backend/frontend coverage, local patch coverage, lefthook (staticcheck/CodeQL/semgrep), and Trivy were not re-run this pass โ€” either confirm via CI or, per the updated agent guidance, run a narrow *targeted* local check (not a full suite re-run) if CI can't cover one of them. 4. New a11y findings (ยง1) are real and actionable but independent of this branch's scope โ€” recommend a separate small fix (2 `aria-label` additions) rather than blocking this PR, unless project policy (as applied in the prior pass) treats "any failing test blocks merge" as still in force, in which case these 2 need triage too. 5. Going forward, any local E2E work by this agent will use targeted single-spec runs under `--project=firefox` only, per the updated `qa-security.md`. + +--- + +# QA Report โ€” release-please Migration (CI/CD Config Only) + +**Branch**: `chore/release-please-migration` +**Base**: `origin/main` @ `67b4f2da` +**Tip reviewed**: `aa7f2135` (6 commits) +**Reviewed by**: qa-security agent +**Date**: 2026-08-17 +**Plan reference**: `docs/plans/current_spec.md` ("CLAUDE.md Definition-of-Done Applicability" section) + +## Scope confirmation + +Verified independently (not just taking the plan's word for it) that this branch touches zero Go/TypeScript/React/database-schema files: `git diff --stat origin/main..HEAD` shows 22 files changed, all `.yml`/`.yaml`/`.json`/`.md`/`.sh`(shell wrapper only)/dotfiles under `.github/`, root config, `.vscode/`, `ARCHITECTURE.md`, `CLAUDE.md`, `VERSION.md`. Per the plan's own DoD-applicability table, this correctly makes Playwright E2E, GORM scan, backend/frontend 85% coverage gates, frontend type-check, and staticcheck all N/A โ€” none were run, per instruction. + +## Gate-by-Gate Status + +| # | Gate | Status | Detail | +|---|------|--------|--------| +| 1 | `bash scripts/local-patch-report.sh` | โœ… **PASS** | Ran cleanly on a code-less diff, no errors. Both `test-results/local-patch-report.md` and `test-results/local-patch-report.json` produced. 0 changed `.go`/`.ts`/`.tsx` lines detected in all four scopes (overall/backend/frontend/agent) โ†’ reported as 100% (0/0) "pass" per the script's own convention. Confirms the mandatory-regardless-of-change-type gate is satisfied and doesn't error on this diff shape. | +| 2 | `lefthook run pre-commit` | โœ… **PASS** | A bare `lefthook run pre-commit` no-ops (nothing staged in the working tree โ€” the branch's changes are already committed). Re-ran explicitly against the branch's actual changed-file list via `lefthook run pre-commit --force --file ` to get real signal. Exit code 0, no `โŒ` in output. `actionlint` validated `.github/workflows/release-please.yml` cleanly; `check-yaml` validated all touched YAML/JSON; `shellcheck`, `dockerfile-check`, `muzzle-allowlist-parity`, `go-vet` (backend+agent), `golangci-lint-fast` (0 issues), `frontend-type-check`, and `frontend-lint` all passed. Note: `--force` caused glob-matched hooks to run repo-wide rather than scoped strictly to the branch's files (e.g. `frontend-lint` reported 1188 pre-existing warnings, 0 errors, across the whole frontend tree) โ€” this is a stronger check than required, not a false pass; all relevant hooks reported clean/zero-error results. | +| 3 | JSON/YAML parse validation | โœ… **PASS** | `jq empty release-please-config.json .release-please-manifest.json` โ€” both valid JSON. `python3 -c "import yaml; yaml.safe_load(...)"` on `.github/workflows/release-please.yml` and `lefthook.yml` โ€” both valid YAML. | +| 4 | CodeQL / Trivy (local) | โžก๏ธ **Correctly deferred to CI** | `chore:`-scoped, no new application code path โ€” per CLAUDE.md's own rule, not run locally. CI runs both unconditionally on every PR regardless, so nothing is skipped, only not duplicated. | +| 5 | `cd backend && go build ./...` | โœ… **PASS (sanity only)** | Clean build, exit 0. Not a real gate for this diff (no backend files touched) โ€” confirms the tree wasn't already broken. | +| 6 | `cd frontend && npm run build` | โœ… **PASS (sanity only)** | Clean build (`โœ“ built in 10.86s`), exit 0. Not a real gate for this diff (no frontend files touched). | +| 7 | Dangling-reference sweep for deleted release-pipeline assets | โœ… **PASS** | `git grep` (HEAD, tracked files only) for `utility-version-check`, `check-version-match-tag`, `release-goreleaser`, `goreleaser.yaml`, `auto-versioning.yml`, `release-drafter`, `auto-changelog.yml` returns hits only in `docs/implementation/`, `docs/plans/archive/`, `docs/reports/archive/`, and `docs/superpowers/specs/` โ€” all historical/archived implementation records of past work, not live guidance. The four "blast radius" files the plan calls out as needing updates (`.github/skills/README.md`, `.vscode/tasks.json`, `.github/skills/utility-bump-beta.SKILL.md`, `lefthook.yml`) were individually re-checked post-diff and are clean (no matches). | + +## Security Assessment: `googleapis/release-please-action` permissions & supply chain + +**File**: `.github/workflows/release-please.yml` + +```yaml +on: + push: + branches: [main] +permissions: + contents: write + pull-requests: write +``` + +- **Permission scope โ€” appropriately minimal.** `contents: write` is required to create tags and GitHub Releases; `pull-requests: write` is required to open/update the standing release PR. This is exactly the documented minimal permission set for `release-please-action` (no `packages:`, `actions:`, `id-token:`, or other elevated scopes granted). No blanket/default `permissions: write-all` used. +- **SHA-pinning convention โ€” consistent with the rest of the repo.** Verified via `git ls-remote --tags` that `45996ed1f6d02564a971a2fa1b5860e934307cf7` resolves directly to the genuine `googleapis/release-please-action` `v5.0.0` tag (same commit object as `refs/tags/v5`, `refs/tags/v5.0`, `refs/tags/v5.0.0`). Spot-checked repo-wide: every `uses:` line across all 35 workflow files (246 total references) is SHA-pinned with a `# vX` trailer comment in the same style (e.g. `actions/checkout@3d3c42e5...# v7`); grepping for any `uses:` line *without* a 40-char SHA turned up zero real matches (one false hit was a comment, not a `uses:` line). The new action reference follows the established convention exactly. +- **Fork/untrusted-contributor exposure โ€” none.** The workflow triggers only on `push: branches: [main]`, not `pull_request` or `pull_request_target`. An external/untrusted contributor cannot invoke this workflow (with its `contents: write` + `pull-requests: write` grant) directly via a fork PR โ€” it only ever runs after code has already been merged to `main` by a maintainer, which is the standard/expected trust boundary for a release-cutting workflow. Repo-wide grep for `pull_request_target` (the genuinely dangerous pattern for granting write perms to fork-triggered runs) found it only in `codecov-upload.yml` and `quality-checks.yml`, neither touched by this branch โ€” out of scope for this review but noted as already-existing, unrelated surface. +- **SECURITY.md alignment**: the repo's documented "Digest Pinning Policy" (SECURITY.md, ~line 959) requires digest-pinned refs for `.github/workflows/*.yml`; the new action satisfies this. +- **No CodeQL/Trivy coverage gap left unaddressed**: as the plan itself notes, neither tool evaluates GitHub Actions permission scopes or third-party Action supply-chain trust โ€” this manual review is the actual mitigation for that gap, not a CI tool. Assessment above stands in for that coverage. + +**Verdict: no CRITICAL/HIGH findings on the permissions/supply-chain surface.** Scope is minimal, pinning is consistent and verified genuine, and the trigger surface excludes untrusted fork contributions. + +## CLAUDE.md diff verification (independent re-check of governance-sensitive file) + +`git diff origin/main..chore/release-please-migration -- CLAUDE.md` shows exactly **one line removed**, at the former Skills-table row: + +```diff +-| `utility-version-check` | Check tool versions | +``` + +No other line in `CLAUDE.md` is touched โ€” confirmed by inspecting the full diff output directly (not by re-reading Supervisor's prior conclusion). No governance/precedence text, security requirement, DoD gate, commit-convention rule, or any other policy statement in `CLAUDE.md` is altered by this branch. This independently confirms Supervisor's earlier finding. + +## Minor / non-blocking findings + +**[LOW] `.dockerignore` retains a stale "GoReleaser" comment label after this PR removes GoReleaser itself.** + +- `git diff origin/main..HEAD -- .gitignore .dockerignore` shows `.gitignore` fully removed its `# GoReleaser` section (comment header + the root-level `dist/` rule that was GoReleaser-specific โ€” confirmed redundant, since `frontend/dist/` already has its own explicit ignore rule at `.gitignore:37`). +- `.dockerignore`, by contrast, only removed the standalone `.goreleaser.yaml` config-file exclusion line, but left an untouched section a few lines down still headed `# GoReleaser & dist artifacts` (`.dockerignore:157-159`) guarding a `dist/` rule. +- This is **not a functional bug** โ€” the underlying `dist/` pattern is not purely dead weight; unanchored `dist/` in `.dockerignore` also incidentally excludes `frontend/dist/` (and any other `dist` directory) from the Docker build context, which remains legitimately useful independent of GoReleaser. The issue is purely the comment label now name-dropping a tool this very PR retires, which is a minor accuracy/consistency gap against the plan's own acceptance criterion ("`.gitignore` and `.dockerignore` no longer reference GoReleaser artifacts/config" โ€” `.gitignore` fully satisfies this, `.dockerignore` only partially does). +- **Remediation**: rename the comment to something like `# Build/dist artifacts` (or fold the `dist/` line into the existing generic exclusions block) in a follow-up commit or this PR's next revision. Not blocking โ€” does not affect security, correctness, or CI behavior. + +No other findings. No secrets, tokens, or credentials observed in any file touched by this branch (also consistent with this being a pure CI-config/docs change with no logging/API-example surface). + +## Summary + +All applicable gates pass. The one new risk surface introduced by this PR โ€” third-party Action with `contents: write` + `pull-requests: write` โ€” has appropriately minimal scope, a correctly-verified SHA pin matching the repo-wide convention, and no fork/untrusted-contributor trigger exposure. One LOW/cosmetic finding (stale `.dockerignore` comment label) does not block. Recommend proceeding to PR/merge; per the plan's own residual-risk framing, the remaining verification (does the workflow actually open/merge/tag/release correctly end-to-end) can only be confirmed by a live run on GitHub after merge โ€” tracked in the plan's "Manual Post-Merge Follow-Ups" section. diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 7d11e0f6e..083f2e39c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -44,7 +44,7 @@ "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^7.0.1", "@testing-library/react": "^16.3.2", - "@testing-library/user-event": "^14.6.3", + "@testing-library/user-event": "^14.6.4", "@types/eslint-plugin-jsx-a11y": "6.10.0", "@types/node": "^26.2.0", "@types/react": "^19.2.18", @@ -73,7 +73,7 @@ "eslint-plugin-unicorn": "^73.0.0", "eslint-plugin-unused-imports": "^4.4.1", "jsdom": "^30.0.1", - "knip": "^6.32.1", + "knip": "^6.32.2", "postcss": "^8.5.26", "tailwindcss": "^4.3.3", "typescript": "^6.0.3", @@ -1040,9 +1040,9 @@ } }, "node_modules/@napi-rs/wasm-runtime": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.2.tgz", - "integrity": "sha512-JfB4kuJQjaoHuCTseIINHtHWeJnvgEcxjwA5t/Y00ZgaOO1Crz3fjT/p8kT28zA/Caz7oiUMn3d6H2yOVCVwuw==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.3.tgz", + "integrity": "sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q==", "dev": true, "license": "MIT", "optional": true, @@ -1057,14 +1057,14 @@ "url": "https://github.com/sponsors/Brooooooklyn" }, "peerDependencies": { - "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.3", - "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.3" + "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", + "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" } }, "node_modules/@oxc-parser/binding-android-arm-eabi": { - "version": "0.142.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.142.0.tgz", - "integrity": "sha512-ZiRGDutGsv1G6bL/ozy/koC0Sv39T1DqyoC4KD1DOy9ZoACm1O5UWhEK2c02Qdk+4lfLVkvFa/mQ0fm/4h1BtQ==", + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.143.0.tgz", + "integrity": "sha512-n9uozULWflPqBtdmI8lAabLqGKNgLVNN0ZH8HfgCwpKGNtzRzauB76jTiW/3YLkcA7N1zskpi9GdVnZuu1SAvg==", "cpu": [ "arm" ], @@ -1079,9 +1079,9 @@ } }, "node_modules/@oxc-parser/binding-android-arm64": { - "version": "0.142.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.142.0.tgz", - "integrity": "sha512-WZkvGRLNQTz8lR9zP5nLjUdlroRCopBu3g9zF1p/laE6DzT1UbQo8Rdz5MWhaJUPYg/6gp+jo7HUgsyKaN1FtQ==", + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.143.0.tgz", + "integrity": "sha512-9BbdjHETk6O3zH/DDid9IgBtF0GlpLabNKN231uraXpRDSfY+iiZxTP5bk1Z63GBownVdhdINFIeddmMz4MzpQ==", "cpu": [ "arm64" ], @@ -1096,9 +1096,9 @@ } }, "node_modules/@oxc-parser/binding-darwin-arm64": { - "version": "0.142.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.142.0.tgz", - "integrity": "sha512-l4khS8LQOOVYsGRVARo1gSaCT/aBSceUVXgtovWc2+drnxVuDr082WA3OCHVdVzIz5JIrP/y9CWsSKxBDNmYGg==", + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.143.0.tgz", + "integrity": "sha512-gh+6ecoHUy4/sUcolBl/1qPXKBbYNxFY0Pk0ujgQvINTMSftJY7o4yb8gOkDJPeZeB8+a+u7xTe6umoP8N5HFA==", "cpu": [ "arm64" ], @@ -1113,9 +1113,9 @@ } }, "node_modules/@oxc-parser/binding-darwin-x64": { - "version": "0.142.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.142.0.tgz", - "integrity": "sha512-QBsNF3nqlXmcH2B1YOPqQYmCJoy4HuIjUxGbBO/k5JAJUl68ghU2psRY2zPk+RyBaWqKP/qfL4oaFgEMCdwskA==", + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.143.0.tgz", + "integrity": "sha512-qd1hl2d+lXgHv/VQ/M9qm8TrMC5T4RqDBwtOnl+1D0QMjwcz+8AaB4JSg8STgeag0GP6a6L74XEGAsrTSJWNzQ==", "cpu": [ "x64" ], @@ -1130,9 +1130,9 @@ } }, "node_modules/@oxc-parser/binding-freebsd-x64": { - "version": "0.142.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.142.0.tgz", - "integrity": "sha512-b7Q7m4Cqc6XqNhri3R+QhU+GVy646Pn+bkdhrDdWym/Fdi0ZUa+d73H9dm5H91JtbtAQ/z1d8XKMW3oOV8a4tQ==", + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.143.0.tgz", + "integrity": "sha512-M5XXcNa7aOqLPKTR41msfghKu2yQ4xWvCm11/gwU0JzOzHNk5sgW//rVEjJ+LO48+VDAMzXTSzurUVxIDKwozw==", "cpu": [ "x64" ], @@ -1147,9 +1147,9 @@ } }, "node_modules/@oxc-parser/binding-linux-arm-gnueabihf": { - "version": "0.142.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.142.0.tgz", - "integrity": "sha512-3riVS5IhdH3uCZj1Y9ftDQlR0dvLsIlw/edrRqk8JhgNd5K0XSs+UBtgh50N13CAlW9/TXj6sVGXaKNBocd0Yg==", + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.143.0.tgz", + "integrity": "sha512-T/GXusuOkPNQhCQCSBbcU/N8j0rAypuDBl1IyFK+lyYT594XsVz80clPC/OtbSSpBGyJxj8uYEfctxVuxVYoww==", "cpu": [ "arm" ], @@ -1164,9 +1164,9 @@ } }, "node_modules/@oxc-parser/binding-linux-arm-musleabihf": { - "version": "0.142.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.142.0.tgz", - "integrity": "sha512-NmXUOpgpTSkhl795TiXmWppTwmSJ92RC1qvD6e4XOF+slgmo3e6Ah+kEu+6AN8s7NAOEwqGmir58MgSQSWmBSA==", + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.143.0.tgz", + "integrity": "sha512-oKu4RcBlXSqo3OC62dp6YTnQaZIurNDpCX3BnAM3+bJxt7s8J2TJKMnC0UYer1qhlRaDCg6wkTaTw+2IlsZ12w==", "cpu": [ "arm" ], @@ -1181,9 +1181,9 @@ } }, "node_modules/@oxc-parser/binding-linux-arm64-gnu": { - "version": "0.142.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.142.0.tgz", - "integrity": "sha512-gc0EXsKtXgerujmU2Bql3u1L1HsSQ2774R83idq/FoNMPVV/RY/1ErFsvnit7KoiP/sLvzQixeUo4Ut0ic0wmw==", + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.143.0.tgz", + "integrity": "sha512-WJBbD186AZmMGaSIhlktC+rPl8L3peCTXAh88Ih9uEvK0en2mPojGyCGYiL6mHtV1RPV3JyfJW5t6n5hh0lXhA==", "cpu": [ "arm64" ], @@ -1201,9 +1201,9 @@ } }, "node_modules/@oxc-parser/binding-linux-arm64-musl": { - "version": "0.142.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.142.0.tgz", - "integrity": "sha512-F2XvmWSE0uWpie+jHKKIFgdVOe9ypGhkEZxKx5DuW215K6cbAC274yYaPkcM7EqY4Df3Weyhpcz3lsURyH2LVg==", + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.143.0.tgz", + "integrity": "sha512-t1AcYOwEzgceadT4v5e+vaCCb0AncCA3v5AyzfBAz/tMq11qzVccXKzNHtkWdjBsgvTKwRkaUF3QvT4kot8vcQ==", "cpu": [ "arm64" ], @@ -1221,9 +1221,9 @@ } }, "node_modules/@oxc-parser/binding-linux-ppc64-gnu": { - "version": "0.142.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.142.0.tgz", - "integrity": "sha512-wLMbT21U/QxknQsk+VvNF0b9D2/aGWhcaQQQ+VYlE8FwD5+GoWZIPPXNzyHmkYyhm0KB3itL+TBavjMatqNnYA==", + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.143.0.tgz", + "integrity": "sha512-RsnO/NoD8376LMJq8JS8TwI0ieNaFRTuNe2GVJntQg6gwZNMENZsEbknHdVwjpOmxdGLGodcwaGSbAeRr5Bgjw==", "cpu": [ "ppc64" ], @@ -1241,9 +1241,9 @@ } }, "node_modules/@oxc-parser/binding-linux-riscv64-gnu": { - "version": "0.142.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.142.0.tgz", - "integrity": "sha512-+G8F/4ckwT7FCJV4H2bt09xEzJbjNCfuL4Sp1AYNaFtFMVtgIGMuJlteT82U+K0UIZ/DzAR/LDlMFnEuajG7Kw==", + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.143.0.tgz", + "integrity": "sha512-48fSVfR9TZi5CASZFyv0VC6z6BCoeihFsX031mAD/oSH7d9PYsPgIqza7d9mjP7Z2KTEpTFyH6SIu0Ui6R1vdg==", "cpu": [ "riscv64" ], @@ -1261,9 +1261,9 @@ } }, "node_modules/@oxc-parser/binding-linux-riscv64-musl": { - "version": "0.142.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.142.0.tgz", - "integrity": "sha512-hTsHtTLxMAfCo+rpF5K3qZJKW2NpPN/CHd4mYB3y7XlSdspHkd2gehDIofP64AacA9nWQw2tY3O7wR6UY8IVOA==", + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.143.0.tgz", + "integrity": "sha512-T8CpdD+SfE01DnIOD4HpVxu0ZJOfMJ/VhCvikKfaXAxkZ+9veyLM/D2hpi7Y2hFUyPmVQO3FNZHmYzV/WlVR4g==", "cpu": [ "riscv64" ], @@ -1281,9 +1281,9 @@ } }, "node_modules/@oxc-parser/binding-linux-s390x-gnu": { - "version": "0.142.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.142.0.tgz", - "integrity": "sha512-6y7qYY3TCUDYjqswImdTGl92y+KA/80twALegQPN27kfY+bG7Ib1+L3jbmrCZQx6wrVnai9IPsEZp07I0hx7JQ==", + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.143.0.tgz", + "integrity": "sha512-QLdeMsCcacenPEFsfxnBUDF1y6opyz5+fmOz9bfD5Y7fiGCMupUCuB3KTPQhNwshIG1P9fPqar9MHxuBDd4bwQ==", "cpu": [ "s390x" ], @@ -1301,9 +1301,9 @@ } }, "node_modules/@oxc-parser/binding-linux-x64-gnu": { - "version": "0.142.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.142.0.tgz", - "integrity": "sha512-i69kAWU+2LgoH5bR+zWiiu+UzAw7Oxkwv7COeJTeY19pn4e70nKQcr9Pm6cL2Z0Z54d+gl9qADlK/0yyuCPiBA==", + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.143.0.tgz", + "integrity": "sha512-659ujfqLy6k7cuH3sbzhd8b+ztSq+i6E2E9pG78Q0BmHjAExfGIdgc8cGgMdwAozDXeZFHkJ+LXYJdWsaGdgyw==", "cpu": [ "x64" ], @@ -1321,9 +1321,9 @@ } }, "node_modules/@oxc-parser/binding-linux-x64-musl": { - "version": "0.142.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.142.0.tgz", - "integrity": "sha512-4SQs678MmjYVrmhAgCWD4o0vpaFszXw9xLX5p2Z9MMFcltxiLkA88wQjh80YHjPrXtpyZ2CWI5m+1yNKM0m2Pw==", + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.143.0.tgz", + "integrity": "sha512-/Mw/9j4TfZcnKphPrzOE6t4MMknXadcAAuVUlDRTF/ETWB5xOgQvOJV2Mh9We/bWxZdoxaGAdc+hy4GuYwQ2yQ==", "cpu": [ "x64" ], @@ -1341,9 +1341,9 @@ } }, "node_modules/@oxc-parser/binding-openharmony-arm64": { - "version": "0.142.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-openharmony-arm64/-/binding-openharmony-arm64-0.142.0.tgz", - "integrity": "sha512-YHpx9N7Ln3a++Tc8rv+H7mrK1zyJQOAwCFg8LZ3lTs1T5afGWeZrLPhPT9HLnIwSjCyJqPWVMIrMxbjcmBr2oQ==", + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-openharmony-arm64/-/binding-openharmony-arm64-0.143.0.tgz", + "integrity": "sha512-8rIKWR2BFuifbIK/1XB9wTaSdtuJ25dlE7ZQYDnEwj/2xH2vHsxnvIjHT3ZjSVuLLwGGlSslIG/fbOJ8TV8rTw==", "cpu": [ "arm64" ], @@ -1357,29 +1357,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxc-parser/binding-wasm32-wasi": { - "version": "0.142.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-wasm32-wasi/-/binding-wasm32-wasi-0.142.0.tgz", - "integrity": "sha512-3pLDyY3+oogW73RM5uehNgAiR/Xfb7fvO2Q1Z1gIqZ2+50XDVQmBVlRkHXZTU4gKnQHpwETNsYQVsJ3joVB2iA==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "1.11.2", - "@emnapi/runtime": "1.11.2", - "@napi-rs/wasm-runtime": "^1.1.6" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, "node_modules/@oxc-parser/binding-win32-arm64-msvc": { - "version": "0.142.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.142.0.tgz", - "integrity": "sha512-Had/VeVY28Oyb0K+Q4FV8KCzoBycIh93oDK6pCbya9lkzdq+ikMHMgBubsdqqlybjJmQRawCQRrnBRHyQwYvcQ==", + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.143.0.tgz", + "integrity": "sha512-5U9kQYMfRRI6Zq7KDxgbIP0RMnKrfn3gLepRMgJuRkPSUALTiRCk9d/uyhb4lGDjUdzwK7mBkKqhLgzBPCmLpQ==", "cpu": [ "arm64" ], @@ -1394,9 +1375,9 @@ } }, "node_modules/@oxc-parser/binding-win32-ia32-msvc": { - "version": "0.142.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.142.0.tgz", - "integrity": "sha512-GGi3+YphVHavvgs6gum2UXoNCqzHAmPt/nXkn8ZQZstV2Q1qZD1Mn8fz/nWrDkefHQtrG/+1/XrbMxsBTo6Svw==", + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.143.0.tgz", + "integrity": "sha512-25P7AaHk4R88Yv2XH4gToDVmh0cOu+bEURQU10CRrmvgabfRArSGAP5osmwUKeSUHj0VS50upbpbRWWW/m7mHA==", "cpu": [ "ia32" ], @@ -1411,9 +1392,9 @@ } }, "node_modules/@oxc-parser/binding-win32-x64-msvc": { - "version": "0.142.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.142.0.tgz", - "integrity": "sha512-Ny/Wv4Us1LGC/ljwNTp+Hx3r/pH15EFfeDF0p+n898gt+TtRd6C9SccHcuUhDiNTb8s5tt7jdeAMDRQZ4Vq6hg==", + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.143.0.tgz", + "integrity": "sha512-ORMh3JE1s6V7ySicdRK7vgaDQnn5o+UHg9ct989PlWHbel8O9ARrmWXM6kZjrBMtNucxNayQ8g69G0VfWzhANw==", "cpu": [ "x64" ], @@ -1428,9 +1409,9 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.142.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.142.0.tgz", - "integrity": "sha512-7W+2q5AKQVU36fkaryontrHn3YDt1RyUYXatw9i5H8ocYe2sPKSFB6eS8WNPeRKiN1qAWWZUPm7gwFzJGrccqQ==", + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.143.0.tgz", + "integrity": "sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA==", "dev": true, "license": "MIT", "funding": { @@ -2488,9 +2469,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.3.tgz", - "integrity": "sha512-zrJtHDcaZJ1Fp7xf4hNl+7seH9Cn/N5TwLYkhgXREtBwAd/jaqW3uqeHxpDugJLVICWg4eW44kOQEGJ1r6jCGw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.4.tgz", + "integrity": "sha512-jHC2cnyKz5xU2fhECtFl8OZ83cYNt13GZQD+0uMJ/X3o+ijmd56okHhTUwxVSHPx1IRVIJEZ1/1pPzeLCU6XKA==", "cpu": [ "arm64" ], @@ -2505,9 +2486,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.3.tgz", - "integrity": "sha512-ieIiibVCp0tX7TLu2cafoNPv8wJyYi01ekXpbf8q2j7F4rGAhhXb/eQh7ge9DRBY78GwmRQtvjZDux7EDbA8kA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-Dc5mPD8F5F/FS8i01syd7FTF6yB2fVthH/TRkjwJkzUK6EpoxHtqvZQP5Zwq80/5z19TWYHIg1KOHboCgVx/aQ==", "cpu": [ "arm64" ], @@ -2522,9 +2503,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.3.tgz", - "integrity": "sha512-Zh9tCon19eDXJoihx0rqKhMUlMYqzwj3aPsSuHmI4RWZh62dWUL+DJN4C5YQya5TcQBJU/Fe8+rY0jhXTQITqA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.4.tgz", + "integrity": "sha512-fpDm4oBo6SqLvWUYCmFhdde3U9KH2fRNNMeAnAPAIwxRL345xutL0EtEUcuoxsoazdJGv/MuDBQHlCDrtbvqOg==", "cpu": [ "x64" ], @@ -2539,9 +2520,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.3.tgz", - "integrity": "sha512-nGbJWewA1wrXXZiQhjAT5rhibGfns5ZNkDVqxsO6zJ3f3YvpoDNNmGMSbbhLuXKjNScaBJVOAboztAWVespQMg==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.4.tgz", + "integrity": "sha512-rSJoreDE/HoIzoaib6MTp5jQtCTdMHKIvItAKT/ImS6Y6Ww76oUaeMyp4Vc/fAgd/ehji068IxetHXAnqUwN9A==", "cpu": [ "x64" ], @@ -2556,9 +2537,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.3.tgz", - "integrity": "sha512-QNniJr5Kml0kDEB98jiDOJjXNroxIIi0IXIbdYzY26Xt1pVbeP62+KnoIZLwirOymX/0jDk/2gI/bNUv7A7OIw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.4.tgz", + "integrity": "sha512-/jm8OGHgn7oGaJu3i/qZI9spUGcJ+y/lk43ttQ/iO1tOd9NissG6o97bighBCiL+BKRngmcDuR6ikfwYdJmVuQ==", "cpu": [ "arm" ], @@ -2573,9 +2554,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.3.tgz", - "integrity": "sha512-TkqEAcmmvH3I/q4114NB4RVt6241Dao48pF45uLcFGrwAaIn0iITgTAKP/dLjbN0R4buJjGb91+UHSoFmpgIWw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.4.tgz", + "integrity": "sha512-tIP06BeD9EqvECBrPZ+sqdPlYrT+aYaAiu1wYziVx5elRK/ftm33JxVDy2bXGbr6J0CrtirCkR87/X5a2euEng==", "cpu": [ "arm64" ], @@ -2593,9 +2574,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.3.tgz", - "integrity": "sha512-NHqjnxpsndf4MPymxteFAWHHfkTL8HjWh1KB7z23ofZ6QO2euONuxDXjat69dKZRALnGypg8k8SsK8vZJoXv1Q==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.4.tgz", + "integrity": "sha512-Ql1Q0EQqVThvn9VAVlwNzsUvbSFtCMGjLpRRi4pk5i7NZZ4n5ISiLMjHYtus4VQ2PvkSw24zyaCVsiS+sXPj1w==", "cpu": [ "arm64" ], @@ -2613,9 +2594,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.3.tgz", - "integrity": "sha512-6tbrbwfz5GB9DQ4Jwo6hy9v+vR31xZlvzZ6n5Xut6Hhx5PvrA9q/HsK8KMaYQp063iqZGXwNvZtYNLD7EM/x0w==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.4.tgz", + "integrity": "sha512-GjbjXD4XXfN19D0LZNbmiCBUoDiRACsYHr0yaIbbn8aFsXjHZifcYqu/W5Er5X2X990WjHXFrxarn5chzItorQ==", "cpu": [ "ppc64" ], @@ -2633,9 +2614,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.3.tgz", - "integrity": "sha512-oyuXxXmoZHjXC917IAPFAAv4wWAa0cM9afk8nx1+9/jNNOX1uPf8yDA6p7G0RypOfw/X0PQt5IfoquY1um+zSg==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.4.tgz", + "integrity": "sha512-p5WR0NOwaRmJ/B1b6IjEFLLivwEsf3PrdBIhRbhTCQisbo2SvHHpG4ELB/+FgQNnB88LTOF86upmJmbvZdQ2lw==", "cpu": [ "s390x" ], @@ -2653,9 +2634,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.3.tgz", - "integrity": "sha512-TytMwF2KVGqP2tgd0I1OY0PAv78dZRAYcF5ssDzjM34SUXCED3uXvSd5+lHoC0bTD6eEdFz7LdQNCO1y0oVk9w==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.4.tgz", + "integrity": "sha512-4/GyVjmhR+Tc6HLJvwc1sOhPqAZtySiSMesOZyX6JQ5XBxoTDEMKQzvo07NIK6nTon/SivlZqvhzvuVBNQhObQ==", "cpu": [ "x64" ], @@ -2673,9 +2654,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.3.tgz", - "integrity": "sha512-/E9m3qstrJFVPoULV25mVQblSNExY2+kBsYe4sy0Tn0yOOgJ8wZbZt3KnRbF/XeU2Gl1STKUQnDNTqhIE5MD4A==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.4.tgz", + "integrity": "sha512-l9eeLsCNvPpmSXUej0etw/J1eqV0Jj1D5G/xG6YTijmE6dkv6E2QezgWbTfQk63v952DPqrjOCoiqxq7Bw0YUQ==", "cpu": [ "x64" ], @@ -2693,9 +2674,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.3.tgz", - "integrity": "sha512-Kr0OcsoQI816i6HOl3vFHpd1K0eZyh76zgfj4c1nTyaTsd5r2Mj1lwM4R90y/qaCfmTn9eHy0SKwi98eitRxug==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.4.tgz", + "integrity": "sha512-e0F355MSTMm3+UOqtV3L24gFUp2N5m1f8L/7d56deik6va+AXdrt9F8LbzGpeWGWRbZEDq4m8NVnJDeBtf9DZg==", "cpu": [ "arm64" ], @@ -2710,9 +2691,9 @@ } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.3.tgz", - "integrity": "sha512-hOtMwTqnME+/gJcH/PCZ0wn0zPUjiWOgkHpxbSJpfGKMezHltx1S7/k1SitzVa7Ww2cqrDDaFbZEhcJZO8o+Jw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.4.tgz", + "integrity": "sha512-AWLi0uBRYh6QlE7OKhiz+phZC0qwtij2QZmhmOdsLdFn64m7oMpooE9ICE3lhm9xMb4SpDo2WbHcxX1iFLFtqw==", "cpu": [ "arm64" ], @@ -2727,9 +2708,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.3.tgz", - "integrity": "sha512-ekcqMMkI2PlhYnfzQnB/cEdYUVVJViWvoUyLrbzgDoi3Snfc1mVBwdnc306ufA5ejy8JSPjT2RlW1nQSjW7efg==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.4.tgz", + "integrity": "sha512-UwSDJOg3dqCAejWdxclJjCsh3Qq4vLYMDxmyHqo1btz3stK2VqgwNd3mm5tuIwzSlGIQ/1H9Hr+Zn09mrezNqQ==", "cpu": [ "x64" ], @@ -3156,9 +3137,9 @@ } }, "node_modules/@testing-library/user-event": { - "version": "14.6.3", - "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.3.tgz", - "integrity": "sha512-6dBq67jT8lE+JTE8Exm02Kt6ze43hz1jdiSpSJwtTZiT1xQQ6b7nZYTTQ9njdArdU8XklOwaDp/AbT/eYSKF4g==", + "version": "14.6.4", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.4.tgz", + "integrity": "sha512-QCGwP6QrjypBLwyj5cuyfVamkaIEy/XGY+1VDehbtbQqOggYmTFpFOdWR5mPz14vX8vXLMVjDHlRNBcClyO9ew==", "dev": true, "license": "MIT", "engines": { @@ -4587,9 +4568,9 @@ } }, "node_modules/baseline-browser-mapping": { - "version": "2.11.13", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.13.tgz", - "integrity": "sha512-k9HNuUVMlqVjQ9UHzfPjIqiDbWw7WqT1AoT7GL8VwvF3r0ZfArtgiSPAlmupyNquNgOJHTuH4CKYf8ttMTWBTQ==", + "version": "2.11.14", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.14.tgz", + "integrity": "sha512-JyJ954WzuIR8/FFzX0o5krdSTrBAkcCSRfWSleRsIHSWV+cZe2FI1PKggVkFke1hBldRs+LRxUczzE9iPmgZww==", "dev": true, "license": "Apache-2.0", "bin": { @@ -5390,9 +5371,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.403", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.403.tgz", - "integrity": "sha512-MQsYmdaLzvaCX5j+ZZBr5Fm6uCCnPQcRtlvmvRlWqrXy+BH2O4ffXIAScF+JQznQWB9brWp4lSD9Z4yNmaf2BA==", + "version": "1.5.406", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.406.tgz", + "integrity": "sha512-hWH5ORBi3d0IipnMh7BN5GDTaAmrSSSWmznwt2zltdiRNEWoEQyTwF0FFSBxzHO7hLSRT6loQu3IQGV0wg/Tvg==", "dev": true, "license": "ISC" }, @@ -6657,9 +6638,9 @@ } }, "node_modules/globals": { - "version": "17.9.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.9.0.tgz", - "integrity": "sha512-m/MvAW61QVU5VDNF1Vj8axt016h8w7L5TU1e9zlab7XIttAT2YAlCwl75K1fOqvMM9apmD7lbCIRhpfkhmxhCg==", + "version": "17.11.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.11.0.tgz", + "integrity": "sha512-Z2I8hM+PbJDXQDq3Icgpzv+mPdwr68iZUU9d5WW4FuXfDUQfkZaZuvjMv42/5crNyw154+9+VWXbYrUgDXbxNw==", "dev": true, "license": "MIT", "engines": { @@ -7702,9 +7683,9 @@ } }, "node_modules/knip": { - "version": "6.32.1", - "resolved": "https://registry.npmjs.org/knip/-/knip-6.32.1.tgz", - "integrity": "sha512-mIiIHMTJVUgSlz0mxEgPt7wg8DmfbCp1Txqab3WpbMCJF7YHvHtC9jeAHHXfISMl72N8WzhyG71SQlaqCOGZtg==", + "version": "6.32.2", + "resolved": "https://registry.npmjs.org/knip/-/knip-6.32.2.tgz", + "integrity": "sha512-WXTXbmocrw7gqm1A1TQvFN0OgJ7hUSU6E1g6SPRIzzHFogUBhXByc7cYeOFVtJ2uODg7DP4VbESYBYnfbtBYsg==", "dev": true, "funding": [ { @@ -7722,13 +7703,13 @@ "formatly": "^0.3.0", "get-tsconfig": "4.14.1", "jiti": "^2.7.0", - "oxc-parser": "^0.142.0", + "oxc-parser": "^0.143.0", "oxc-resolver": "11.24.2", "picomatch": "^4.0.5", "smol-toml": "^1.7.1", "strip-json-comments": "5.0.3", "tinyglobby": "^0.2.17", - "unbash": "^4.0.4", + "unbash": "^4.0.9", "yaml": "^2.9.0", "zod": "^4.4.3" }, @@ -9342,13 +9323,13 @@ } }, "node_modules/oxc-parser": { - "version": "0.142.0", - "resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.142.0.tgz", - "integrity": "sha512-kKR+jPiRJYJDexVoziIg/FVGvr1fT1FZSSJOk6tVoMKKSlsf1Cso+cgGCJkOEDWOP174vRntCPFKg+AS7InWvw==", + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.143.0.tgz", + "integrity": "sha512-ov0NzaDCOInknS7mP1cwKdJERt3utPW8ldjtdUXQ8Ty0GEFD08wk422vCUN0d7pST6kqtV7dxoI9w1Zi0l/9TA==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "^0.142.0" + "@oxc-project/types": "^0.143.0" }, "engines": { "node": "^20.19.0 || >=22.12.0" @@ -9357,26 +9338,25 @@ "url": "https://github.com/sponsors/Boshen" }, "optionalDependencies": { - "@oxc-parser/binding-android-arm-eabi": "0.142.0", - "@oxc-parser/binding-android-arm64": "0.142.0", - "@oxc-parser/binding-darwin-arm64": "0.142.0", - "@oxc-parser/binding-darwin-x64": "0.142.0", - "@oxc-parser/binding-freebsd-x64": "0.142.0", - "@oxc-parser/binding-linux-arm-gnueabihf": "0.142.0", - "@oxc-parser/binding-linux-arm-musleabihf": "0.142.0", - "@oxc-parser/binding-linux-arm64-gnu": "0.142.0", - "@oxc-parser/binding-linux-arm64-musl": "0.142.0", - "@oxc-parser/binding-linux-ppc64-gnu": "0.142.0", - "@oxc-parser/binding-linux-riscv64-gnu": "0.142.0", - "@oxc-parser/binding-linux-riscv64-musl": "0.142.0", - "@oxc-parser/binding-linux-s390x-gnu": "0.142.0", - "@oxc-parser/binding-linux-x64-gnu": "0.142.0", - "@oxc-parser/binding-linux-x64-musl": "0.142.0", - "@oxc-parser/binding-openharmony-arm64": "0.142.0", - "@oxc-parser/binding-wasm32-wasi": "0.142.0", - "@oxc-parser/binding-win32-arm64-msvc": "0.142.0", - "@oxc-parser/binding-win32-ia32-msvc": "0.142.0", - "@oxc-parser/binding-win32-x64-msvc": "0.142.0" + "@oxc-parser/binding-android-arm-eabi": "0.143.0", + "@oxc-parser/binding-android-arm64": "0.143.0", + "@oxc-parser/binding-darwin-arm64": "0.143.0", + "@oxc-parser/binding-darwin-x64": "0.143.0", + "@oxc-parser/binding-freebsd-x64": "0.143.0", + "@oxc-parser/binding-linux-arm-gnueabihf": "0.143.0", + "@oxc-parser/binding-linux-arm-musleabihf": "0.143.0", + "@oxc-parser/binding-linux-arm64-gnu": "0.143.0", + "@oxc-parser/binding-linux-arm64-musl": "0.143.0", + "@oxc-parser/binding-linux-ppc64-gnu": "0.143.0", + "@oxc-parser/binding-linux-riscv64-gnu": "0.143.0", + "@oxc-parser/binding-linux-riscv64-musl": "0.143.0", + "@oxc-parser/binding-linux-s390x-gnu": "0.143.0", + "@oxc-parser/binding-linux-x64-gnu": "0.143.0", + "@oxc-parser/binding-linux-x64-musl": "0.143.0", + "@oxc-parser/binding-openharmony-arm64": "0.143.0", + "@oxc-parser/binding-win32-arm64-msvc": "0.143.0", + "@oxc-parser/binding-win32-ia32-msvc": "0.143.0", + "@oxc-parser/binding-win32-x64-msvc": "0.143.0" } }, "node_modules/oxc-resolver": { @@ -10146,13 +10126,13 @@ } }, "node_modules/rolldown": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.3.tgz", - "integrity": "sha512-rn9wpmxplLf7NLNyCk9FyWh3FM43DbY8jOzCdEPzH7uflhTftRbCEpqi6Ly2osgoU8OwObtmavMbWLaWy4LX7A==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.4.tgz", + "integrity": "sha512-rSr7irW0K7QRWzjdJXqZowkcRdDtjRduh43rBltnVKd0VFq839l1lJoDvGJb6gl7+4rTTCrPWu+YfujUL8Ug7w==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.143.0", + "@oxc-project/types": "=0.144.0", "@rolldown/pluginutils": "^1.0.0" }, "bin": { @@ -10162,26 +10142,26 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.2.3", - "@rolldown/binding-darwin-arm64": "1.2.3", - "@rolldown/binding-darwin-x64": "1.2.3", - "@rolldown/binding-freebsd-x64": "1.2.3", - "@rolldown/binding-linux-arm-gnueabihf": "1.2.3", - "@rolldown/binding-linux-arm64-gnu": "1.2.3", - "@rolldown/binding-linux-arm64-musl": "1.2.3", - "@rolldown/binding-linux-ppc64-gnu": "1.2.3", - "@rolldown/binding-linux-s390x-gnu": "1.2.3", - "@rolldown/binding-linux-x64-gnu": "1.2.3", - "@rolldown/binding-linux-x64-musl": "1.2.3", - "@rolldown/binding-openharmony-arm64": "1.2.3", - "@rolldown/binding-win32-arm64-msvc": "1.2.3", - "@rolldown/binding-win32-x64-msvc": "1.2.3" + "@rolldown/binding-android-arm64": "1.2.4", + "@rolldown/binding-darwin-arm64": "1.2.4", + "@rolldown/binding-darwin-x64": "1.2.4", + "@rolldown/binding-freebsd-x64": "1.2.4", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.4", + "@rolldown/binding-linux-arm64-gnu": "1.2.4", + "@rolldown/binding-linux-arm64-musl": "1.2.4", + "@rolldown/binding-linux-ppc64-gnu": "1.2.4", + "@rolldown/binding-linux-s390x-gnu": "1.2.4", + "@rolldown/binding-linux-x64-gnu": "1.2.4", + "@rolldown/binding-linux-x64-musl": "1.2.4", + "@rolldown/binding-openharmony-arm64": "1.2.4", + "@rolldown/binding-win32-arm64-msvc": "1.2.4", + "@rolldown/binding-win32-x64-msvc": "1.2.4" } }, "node_modules/rolldown/node_modules/@oxc-project/types": { - "version": "0.143.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.143.0.tgz", - "integrity": "sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA==", + "version": "0.144.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.144.0.tgz", + "integrity": "sha512-nuhZIOLuI6TFQ32I/WnUx+SCPY7SdSKwgnFHydAuoS1+Z4BRcaP+RRJmGzl9lw+0OFF7UmaESf7KQRXaNLHypg==", "dev": true, "license": "MIT", "funding": { @@ -10492,9 +10472,9 @@ } }, "node_modules/smol-toml": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.7.1.tgz", - "integrity": "sha512-PPlsspAZ4jbMBu5DMFhfUGDQLu/vrL4SyBROVS37x8ynnVmFIs1VPBz1Co8Xks3TvpIaZXmU85y4DrQ+UyVFoQ==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.8.0.tgz", + "integrity": "sha512-kCZr2V3ch9i00x8zXRhjUNVcjG9ijES5dDudkXvUVCT5QlJNQWElSJdZqyPemffHoLNUYwOcou0Fy+ojN0uHSQ==", "dev": true, "license": "BSD-3-Clause", "engines": { diff --git a/frontend/package.json b/frontend/package.json index 6c1896c42..d9a3144ae 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -64,7 +64,7 @@ "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^7.0.1", "@testing-library/react": "^16.3.2", - "@testing-library/user-event": "^14.6.3", + "@testing-library/user-event": "^14.6.4", "@types/eslint-plugin-jsx-a11y": "6.10.0", "@types/node": "^26.2.0", "@types/react": "^19.2.18", @@ -93,7 +93,7 @@ "eslint-plugin-unicorn": "^73.0.0", "eslint-plugin-unused-imports": "^4.4.1", "jsdom": "^30.0.1", - "knip": "^6.32.1", + "knip": "^6.32.2", "postcss": "^8.5.26", "tailwindcss": "^4.3.3", "typescript": "^6.0.3", diff --git a/frontend/src/components/ui/Input.tsx b/frontend/src/components/ui/Input.tsx index aa2eabb93..650385c05 100644 --- a/frontend/src/components/ui/Input.tsx +++ b/frontend/src/components/ui/Input.tsx @@ -24,18 +24,26 @@ const Input = React.forwardRef( className, type, disabled, + id, ...props }, ref ) => { const [showPassword, setShowPassword] = React.useState(false) const isPassword = type === 'password' + // Auto-generate a stable id when the caller doesn't supply one, so the + // label is always programmatically associated with its control (WCAG + // 1.3.1 / 3.3.2). `id` is destructured out explicitly above โ€” not left + // inside `...props` โ€” so this fallback can't be silently overridden by + // a stray `id: undefined` reaching the spread below in prop order. + const generatedId = React.useId() + const inputId = id ?? generatedId return (
{label && (