diff --git a/.changeset/refactor-ci-workflows-docker-security.md b/.changeset/refactor-ci-workflows-docker-security.md new file mode 100644 index 0000000..0afbd84 --- /dev/null +++ b/.changeset/refactor-ci-workflows-docker-security.md @@ -0,0 +1,48 @@ +--- +'deepsource-mcp-server': patch +--- + +Refactor CI/CD workflows and improve Docker security + +### CI/CD Improvements + +- **Unified Build Process**: Consolidated build into single job that creates reusable artifacts +- **Build Artifacts**: Generated once and reused throughout workflow for consistency +- **Build Manifest**: Added metadata tracking (SHA, timestamp, dependencies) +- **Tag Creation**: Tags now created before building artifacts for better traceability +- **Artifact Naming**: Include commit SHA for consistent naming across workflows + +### Docker Support + +- **Multi-Platform Builds**: Added support for linux/amd64 and linux/arm64 +- **Docker Workflow**: New reusable workflow for container image builds +- **Configuration**: Docker releases controlled via `ENABLE_DOCKER_RELEASE` variable + +### Security Enhancements + +- **CodeQL Integration**: Added dedicated security scanning workflow +- **Consolidated Scanning**: Unified security checks in reusable workflow +- **Docker Security**: Container images run as non-root user (nodejs:1001) +- **Trivy Scanning**: Automated vulnerability detection in container images +- **Dependency Scanning**: Enhanced vulnerability reporting + +### Developer Experience + +- **Issue Templates**: Added bug report and feature request templates +- **PR Template**: Comprehensive pull request template with checklist +- **Workflow Documentation**: Enhanced comments for better maintainability +- **Better Validation**: Improved changeset validation in PR workflow + +### Infrastructure + +- **NPM Packaging**: Dedicated job for package preparation with attestations +- **SLSA Provenance**: Generate attestations for supply chain security +- **Improved Permissions**: Updated for container registry access +- **DeepSource Integration**: Maintained test coverage reporting + +### Technical Details + +- Removed redundant `reusable-setup.yml` (merged into other workflows) +- Better job dependency graph for parallel execution +- Consistent secret passing (DEEPSOURCE_DSN) +- Enhanced artifact retention strategies diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..b5c68e5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,40 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: + +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + +- OS: [e.g. iOS] +- Browser [e.g. chrome, safari] +- Version [e.g. 22] + +**Smartphone (please complete the following information):** + +- Device: [e.g. iPhone6] +- OS: [e.g. iOS8.1] +- Browser [e.g. stock browser, safari] +- Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..2f28cea --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,19 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..d98715c --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,33 @@ + + +## Pull request type + + + +Please check the type of change your PR introduces: + +- [ ] Bugfix +- [ ] Feature +- [ ] Code style update (formatting, renaming) +- [ ] Refactoring (no functional changes, no api changes) +- [ ] Build related changes +- [ ] Documentation content changes +- [ ] Other (please describe): + +## What is the current behavior? + + + +Issue Number: N/A + +## What is the new behavior? + + + +- +- +- + +## Other information + + diff --git a/.github/scripts/determine-artifact.sh b/.github/scripts/determine-artifact.sh new file mode 100644 index 0000000..7f6802d --- /dev/null +++ b/.github/scripts/determine-artifact.sh @@ -0,0 +1,202 @@ +#!/bin/bash +# ============================================================================= +# SCRIPT: Determine Build Artifact from GitHub Releases +# PURPOSE: Find and validate the correct artifact from a GitHub release +# USAGE: ./determine-artifact.sh --tag --repo --version --prefix --output +# ============================================================================= + +set -euo pipefail + +# Default values +TAG_NAME="" +REPO="" +VERSION="" +PREFIX="" +OUTPUT_FILE="" + +# Parse command line arguments +while [[ $# -gt 0 ]]; do + case $1 in + --tag) + TAG_NAME="$2" + shift 2 + ;; + --repo) + REPO="$2" + shift 2 + ;; + --version) + VERSION="$2" + shift 2 + ;; + --prefix) + PREFIX="$2" + shift 2 + ;; + --output) + OUTPUT_FILE="$2" + shift 2 + ;; + *) + echo "Unknown option: $1" + exit 1 + ;; + esac +done + +# Validate required parameters +if [ -z "$TAG_NAME" ] || [ -z "$REPO" ] || [ -z "$VERSION" ] || [ -z "$PREFIX" ] || [ -z "$OUTPUT_FILE" ]; then + echo "❌ Missing required parameters" + echo "Usage: $0 --tag --repo --version --prefix --output " + exit 1 +fi + +echo "🔍 Determining artifact source for $PREFIX-$VERSION from release $TAG_NAME" + +# Fetch tag information from GitHub API +TAG_API_URL="https://api.github.com/repos/$REPO/git/refs/tags/$TAG_NAME" +TAG_RESPONSE=$(curl -s -H "Authorization: Bearer $GITHUB_TOKEN" -w "\n%{http_code}" $TAG_API_URL) +TAG_BODY=$(echo "$TAG_RESPONSE" | head -n -1) +TAG_STATUS=$(echo "$TAG_RESPONSE" | tail -n 1) + +if [ "$TAG_STATUS" != "200" ]; then + echo "❌ GitHub API request failed for $TAG_API_URL with status $TAG_STATUS" + echo "Response: $TAG_BODY" + exit 1 +fi + +# Extract the object SHA and type +TAG_OBJECT_SHA=$(echo "$TAG_BODY" | jq -r '.object.sha') +TAG_OBJECT_TYPE=$(echo "$TAG_BODY" | jq -r '.object.type') + +echo "📌 Tag $TAG_NAME points to $TAG_OBJECT_TYPE: $TAG_OBJECT_SHA" + +# Determine the commit SHA based on tag type +if [ "$TAG_OBJECT_TYPE" = "tag" ]; then + # Annotated tag - fetch the tag object to get the commit SHA + TAG_OBJECT_URL="https://api.github.com/repos/$REPO/git/tags/$TAG_OBJECT_SHA" + TAG_OBJECT_RESPONSE=$(curl -s -H "Authorization: Bearer $GITHUB_TOKEN" -w "\n%{http_code}" $TAG_OBJECT_URL) + TAG_OBJECT_BODY=$(echo "$TAG_OBJECT_RESPONSE" | head -n -1) + TAG_OBJECT_STATUS=$(echo "$TAG_OBJECT_RESPONSE" | tail -n 1) + + if [ "$TAG_OBJECT_STATUS" != "200" ]; then + echo "❌ Failed to fetch annotated tag object with status $TAG_OBJECT_STATUS" + echo "Response: $TAG_OBJECT_BODY" + exit 1 + fi + + COMMIT_SHA=$(echo "$TAG_OBJECT_BODY" | jq -r '.object.sha') + echo "📌 Annotated tag references commit: $COMMIT_SHA" +elif [ "$TAG_OBJECT_TYPE" = "commit" ]; then + # Lightweight tag - directly references a commit + COMMIT_SHA=$TAG_OBJECT_SHA + echo "📌 Lightweight tag directly references commit: $COMMIT_SHA" +else + echo "❌ Unexpected tag object type: $TAG_OBJECT_TYPE" + exit 1 +fi + +# The tag points to the version commit, but artifacts were built with the previous commit +# Get the parent commit (the one that triggered the build) +echo "🔍 Getting parent commit of $COMMIT_SHA" +PARENT_COMMIT_URL="https://api.github.com/repos/$REPO/commits/$COMMIT_SHA" +PARENT_RESPONSE=$(curl -s -H "Authorization: Bearer $GITHUB_TOKEN" -w "\n%{http_code}" $PARENT_COMMIT_URL) +PARENT_BODY=$(echo "$PARENT_RESPONSE" | head -n -1) +PARENT_STATUS=$(echo "$PARENT_RESPONSE" | tail -n 1) + +if [ "$PARENT_STATUS" != "200" ]; then + echo "❌ Failed to fetch commit information with status $PARENT_STATUS" + echo "Response: $PARENT_BODY" + exit 1 +fi + +# Get the parent SHA (the commit that triggered the build) +PARENT_SHA=$(echo "$PARENT_BODY" | jq -r '.parents[0].sha') +echo "📌 Parent commit (build trigger): $PARENT_SHA" + +# Find the workflow run that created the release artifacts +# Retry with exponential backoff to handle race conditions +RUNS_API_URL="https://api.github.com/repos/$REPO/actions/runs?head_sha=$PARENT_SHA&status=success&event=push" +echo "🔍 Searching for successful workflow runs for parent commit $PARENT_SHA" + +MAX_RETRIES=5 +RETRY_COUNT=0 +MAIN_RUN="" + +while [ $RETRY_COUNT -lt $MAX_RETRIES ] && [ -z "$MAIN_RUN" ]; do + if [ $RETRY_COUNT -gt 0 ]; then + WAIT_TIME=$((5 * RETRY_COUNT)) + echo "⏳ Waiting ${WAIT_TIME}s before retry $RETRY_COUNT/$MAX_RETRIES..." + sleep $WAIT_TIME + fi + + RUNS_RESPONSE=$(curl -s -H "Authorization: Bearer $GITHUB_TOKEN" -w "\n%{http_code}" $RUNS_API_URL) + RUNS_BODY=$(echo "$RUNS_RESPONSE" | head -n -1) + RUNS_STATUS=$(echo "$RUNS_RESPONSE" | tail -n 1) + + if [ "$RUNS_STATUS" != "200" ]; then + echo "❌ Failed to fetch workflow runs with status $RUNS_STATUS" + echo "Response: $RUNS_BODY" + exit 1 + fi + + # Find the Main workflow run + MAIN_RUN=$(echo "$RUNS_BODY" | jq -r '.workflow_runs[] | select(.name == "Main") | {id: .id, created_at: .created_at}') + + if [ -z "$MAIN_RUN" ]; then + RETRY_COUNT=$((RETRY_COUNT + 1)) + if [ $RETRY_COUNT -lt $MAX_RETRIES ]; then + echo "⚠️ Main workflow not found yet (attempt $RETRY_COUNT/$MAX_RETRIES)" + fi + fi +done + +if [ -z "$MAIN_RUN" ]; then + echo "❌ No successful Main workflow run found for parent commit $PARENT_SHA after $MAX_RETRIES attempts" + echo "Available runs:" + echo "$RUNS_BODY" | jq -r '.workflow_runs[] | "\(.name): \(.id) (\(.status))"' + exit 1 +fi + +RUN_ID=$(echo "$MAIN_RUN" | jq -r '.id') +echo "✅ Found Main workflow run: $RUN_ID" + +# Get artifacts from the workflow run +ARTIFACTS_API_URL="https://api.github.com/repos/$REPO/actions/runs/$RUN_ID/artifacts" +echo "🔍 Fetching artifacts from run $RUN_ID" + +ARTIFACTS_RESPONSE=$(curl -s -H "Authorization: Bearer $GITHUB_TOKEN" -w "\n%{http_code}" $ARTIFACTS_API_URL) +ARTIFACTS_BODY=$(echo "$ARTIFACTS_RESPONSE" | head -n -1) +ARTIFACTS_STATUS=$(echo "$ARTIFACTS_RESPONSE" | tail -n 1) + +if [ "$ARTIFACTS_STATUS" != "200" ]; then + echo "❌ Failed to fetch artifacts with status $ARTIFACTS_STATUS" + echo "Response: $ARTIFACTS_BODY" + exit 1 +fi + +# Find the artifact with the specified prefix (using full parent SHA to match artifact naming) +ARTIFACT_NAME="$PREFIX-$VERSION-${PARENT_SHA}" +ARTIFACT=$(echo "$ARTIFACTS_BODY" | jq -r --arg name "$ARTIFACT_NAME" '.artifacts[] | select(.name == $name)') + +if [ -z "$ARTIFACT" ]; then + echo "❌ Artifact $ARTIFACT_NAME not found in workflow run $RUN_ID" + echo "Available artifacts:" + echo "$ARTIFACTS_BODY" | jq -r '.artifacts[].name' + exit 1 +fi + +ARTIFACT_ID=$(echo "$ARTIFACT" | jq -r '.id') +ARTIFACT_SIZE=$(echo "$ARTIFACT" | jq -r '.size_in_bytes') + +echo "✅ Found artifact: $ARTIFACT_NAME (ID: $ARTIFACT_ID, Size: $ARTIFACT_SIZE bytes)" + +# Output the results for GitHub Actions +{ + echo "artifact_name=$ARTIFACT_NAME" + echo "artifact_id=$ARTIFACT_ID" + echo "run_id=$RUN_ID" + echo "commit_sha=$PARENT_SHA" # Use parent SHA since that's what built the artifacts +} >> "$OUTPUT_FILE" + +echo "✅ Artifact information written to $OUTPUT_FILE" \ No newline at end of file diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..3403576 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,72 @@ +# ============================================================================= +# WORKFLOW: CodeQL Security Analysis +# PURPOSE: Continuous security analysis for the default branch and pull requests +# TRIGGERS: Push to main, Pull requests to main +# OUTPUTS: Security findings uploaded to GitHub Security tab +# ============================================================================= + +name: CodeQL + +on: + push: + branches: [main] + pull_request: + branches: [main] + schedule: + # Run daily at 00:00 UTC to catch new vulnerabilities + - cron: '0 0 * * *' + +# SECURITY: Required permissions for CodeQL analysis +permissions: + actions: read # Read workflow metadata + contents: read # Read source code + security-events: write # Upload security findings to Security tab + +jobs: + analyze: + name: Analyze Code + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Full history for accurate analysis + + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 10.17.0 + run_install: false + standalone: true + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + + - name: Install dependencies + # Dependencies needed for accurate CodeQL analysis + run: pnpm install --frozen-lockfile + + # ============================================================================= + # CODEQL STATIC ANALYSIS + # Scans for security vulnerabilities in source code + # ============================================================================= + + - name: Initialize CodeQL + # Setup CodeQL for JavaScript/TypeScript analysis + # Detects: XSS, SQL injection, path traversal, command injection, etc. + uses: github/codeql-action/init@v3 + with: + languages: javascript-typescript + # Optionally specify additional queries to run + # queries: security-extended,security-and-quality + + - name: Perform CodeQL Analysis + # Analyze code and upload results to Security tab + # Results viewable at: Security > Code scanning alerts + uses: github/codeql-action/analyze@v3 + with: + category: '/language:javascript-typescript' diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6656261..9040a5d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -23,12 +23,14 @@ concurrency: # attestations: write - Attach attestations to artifacts # security-events: write - Upload security scan results # actions: read - Access workflow runs and artifacts +# packages: write - Push Docker images to GitHub Container Registry permissions: contents: write id-token: write attestations: write security-events: write actions: read + packages: write jobs: # ============================================================================= @@ -48,221 +50,322 @@ jobs: # Parallel security scans to identify vulnerabilities before release # ============================================================================= - # CodeQL: Static analysis for security vulnerabilities # Scans TypeScript/JavaScript for common security issues (XSS, SQL injection, etc.) - codeql: + security: uses: ./.github/workflows/reusable-security.yml - with: - generate-sbom: false # SBOM generated during release for consistency - run-osv-scan: false # OSV scan runs separately below - run-codeql: true # Enable CodeQL analysis - - # OSV (Open Source Vulnerabilities) scanning - # Uses Google's official action for comprehensive dependency vulnerability checks - # UPDATE: Quarterly review for new scanner versions (currently v2.2.1) - vulnerability: - uses: google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@v2.2.1 - with: - # Scan entire project including all manifests (package.json, pnpm-lock.yaml) - scan-args: |- - ./ - permissions: - security-events: write # Required to upload findings to Security tab - actions: read - contents: read # ============================================================================= - # RELEASE PHASE - # Creates versioned releases with artifacts when changesets are present + # UNIFIED BUILD PHASE + # Single build job that creates artifacts to be reused throughout the workflow # ============================================================================= - version-and-release: - # Only runs after all validation and security checks pass - # Ensures we never release vulnerable or broken code - needs: [validate, codeql, vulnerability] + build: runs-on: ubuntu-latest outputs: - released: ${{ steps.release.outputs.released }} # true if release created - version: ${{ steps.version.outputs.version }} # semantic version number + artifact-name: dist-${{ github.sha }} + changed: ${{ steps.version.outputs.changed }} + version: ${{ steps.version.outputs.version }} + tag_sha: ${{ steps.tag.outputs.sha }} steps: - name: Checkout code uses: actions/checkout@v4 with: - fetch-depth: 0 # Full history needed for changeset detection - # SECURITY: Use RELEASE_TOKEN if available for protected branch pushes - # Falls back to GITHUB_TOKEN for standard permissions - token: ${{ secrets.RELEASE_TOKEN || secrets.GITHUB_TOKEN }} - - # ============================================================================= - # ENVIRONMENT SETUP - # Consistent toolchain setup matching package.json requirements - # ============================================================================= + fetch-depth: 0 + token: ${{ secrets.RELEASE_TOKEN }} - name: Install pnpm uses: pnpm/action-setup@v4 with: - version: 10.17.0 # Pinned: Must match packageManager in package.json - run_install: false # Dependencies installed separately for caching - standalone: true # Faster installation method + version: 10.17.0 + run_install: false + standalone: true - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: 22 # Pinned: Must match engines.node in package.json - cache: pnpm # Cache dependencies between runs (saves 1-2 min) + node-version: 22 + cache: pnpm - name: Install dependencies - # frozen-lockfile ensures exact versions from pnpm-lock.yaml - # FAILS IF: Lock file doesn't match package.json run: pnpm install --frozen-lockfile - # ============================================================================= - # VERSION MANAGEMENT - # Determines if release needed based on changesets - # ============================================================================= - - name: Version packages id: version env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - # Custom script validates changesets and updates version + # Custom script validates changesets and determines version # FAILS IF: feat/fix commits exist without changesets # Outputs: changed=true/false, version=X.Y.Z - # Debug: Add --verbose flag to script for detailed logs node .github/scripts/version-and-release.js - # ============================================================================= - # BUILD AND ARTIFACT GENERATION - # Only runs when version changes detected - # ============================================================================= - - - name: Build - # Skip build if no version change (e.g., docs-only commits) - # Clean and rebuild to ensure SBOM reflects exact build artifacts + - name: Commit version changes if: steps.version.outputs.changed == 'true' run: | - pnpm clean # Remove any previous build artifacts - pnpm build # Fresh build for release + # Configure git with GitHub Actions bot identity + git config --local user.email "${{ github.actor_id }}+${{ github.actor }}@users.noreply.github.com" + git config --local user.name "${{ github.actor }}" - - name: Generate SBOM - # Software Bill of Materials for supply chain security - # Creates sbom.cdx.json in CycloneDX format - if: steps.version.outputs.changed == 'true' - run: pnpm sbom + # Stage version-related changes + git add package.json CHANGELOG.md .changeset - - name: Create release artifacts - # Package dist/ folder for GitHub release attachments - # Creates both tar.gz (Linux/Mac) and zip (Windows) formats + # Commit with [skip actions] to prevent workflow recursion + git commit -m "chore(release): v${{ steps.version.outputs.version }} [skip actions]" + + # Push changes to origin + git push origin main + + echo "✅ Version changes committed and pushed" + + - name: Create and push tag + # Create tag BEFORE building artifacts so they're associated with the tag + id: tag if: steps.version.outputs.changed == 'true' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | VERSION="${{ steps.version.outputs.version }}" - tar -czf dist-${VERSION}.tar.gz dist/ - zip -r dist-${VERSION}.zip dist/ - # ============================================================================= - # GIT OPERATIONS - # Commit version changes and create release tag - # ============================================================================= + # Configure git + git config --local user.email "${{ github.actor_id }}+${{ github.actor }}@users.noreply.github.com" + git config --local user.name "${{ github.actor }}" + + # Create annotated tag + git tag -a "v${VERSION}" -m "Release v${VERSION}" + + # Push tag to origin + git push origin "v${VERSION}" - - name: Install actionlint for pre-commit validation - # Install actionlint so pre-commit hooks can run workflow validation - # Uses the official installer script from rhysd/actionlint + # Get the tag SHA for artifact naming + TAG_SHA=$(git rev-list -n 1 "v${VERSION}") + echo "sha=${TAG_SHA}" >> $GITHUB_OUTPUT + echo "📌 Tag SHA for artifacts: ${TAG_SHA}" + + - name: Build TypeScript if: steps.version.outputs.changed == 'true' run: | - echo "Installing actionlint for pre-commit validation..." - bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash) - echo "${PWD}" >> $GITHUB_PATH + pnpm build + echo "✅ Built TypeScript once for entire workflow" - - name: Commit version changes - # Creates release commit with updated package.json and CHANGELOG.md - # [skip actions] prevents infinite loop by skipping this workflow on the commit + - name: Generate artifact manifest if: steps.version.outputs.changed == 'true' run: | - # Configure git with GitHub Actions bot identity - git config --local user.email "${{ github.actor_id }}+${{ github.actor }}@users.noreply.github.com" - git config --local user.name "${{ github.actor }}" + # Create a manifest of what's been built + cat > build-manifest.json </dev/null | tail -1) + echo "📦 Created NPM package: $NPM_PACKAGE" + + # Generate metadata using github.sha for consistent naming with publish workflow + ARTIFACT_NAME="npm-package-${{ needs.build.outputs.version }}-${{ github.sha }}" + { + echo "artifact_name=$ARTIFACT_NAME" + echo "tarball_name=$NPM_PACKAGE" + echo "built=true" + } >> $GITHUB_OUTPUT + + # Create manifest of included files for verification + npm pack --dry-run --json 2>/dev/null | jq -r '.[0].files[].path' > npm-package-manifest.txt + echo "📋 Package contains $(wc -l < npm-package-manifest.txt) files" + + - name: Upload NPM package artifact + uses: actions/upload-artifact@v4 + with: + name: npm-package-${{ needs.build.outputs.version }}-${{ github.sha }} + path: | + *.tgz + npm-package-manifest.txt + retention-days: 7 + + - name: Generate attestations for NPM package + uses: actions/attest-build-provenance@v2 + with: + subject-path: '*.tgz' + + # ============================================================================= + # GITHUB RELEASE CREATION PHASE + # Creates GitHub release as the final step after version is committed + # ============================================================================= + + create-release: + name: Create GitHub Release + needs: [build, docker, npm] + if: needs.build.outputs.changed == 'true' + runs-on: ubuntu-latest + outputs: + released: ${{ steps.release.outputs.released }} + version: ${{ needs.build.outputs.version }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + # Checkout the newly created tag + ref: v${{ needs.build.outputs.version }} + + - name: Download build artifact + uses: actions/download-artifact@v4 + with: + name: ${{ needs.build.outputs.artifact-name }} + + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 10.17.0 + run_install: false + standalone: true + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + + - name: Install dependencies + # Only production dependencies needed for SBOM generation + # Skip scripts to avoid running husky (dev dependency) + run: pnpm install --prod --frozen-lockfile --ignore-scripts + + - name: Generate SBOM + run: pnpm sbom + + - name: Create release artifacts + run: | + VERSION="${{ needs.build.outputs.version }}" + TAG_SHA="${{ needs.build.outputs.tag_sha }}" + tar -czf dist-${VERSION}-${TAG_SHA:0:7}.tar.gz dist/ + zip -r dist-${VERSION}-${TAG_SHA:0:7}.zip dist/ - name: Extract release notes - # Parse CHANGELOG.md to get notes for this specific version - # AWK script extracts content between version headers - if: steps.version.outputs.changed == 'true' run: | - VERSION="${{ steps.version.outputs.version }}" - # Extract content between this version header and the next + VERSION="${{ needs.build.outputs.version }}" awk -v version="## $VERSION" ' $0 ~ version { flag=1; next } /^## [0-9]/ && flag { exit } flag { print } ' CHANGELOG.md > release-notes.md - # Fallback if extraction fails (e.g., first release) if [ ! -s release-notes.md ]; then echo "Release v$VERSION" > release-notes.md fi - - name: Create GitHub Release - # Publishes release with artifacts to GitHub Releases page - # Pinned to v2 for stability - check for updates quarterly - if: steps.version.outputs.changed == 'true' - uses: softprops/action-gh-release@v2 - with: - tag_name: v${{ steps.version.outputs.version }} - name: v${{ steps.version.outputs.version }} - body_path: release-notes.md # Changelog excerpt from above - draft: false # Publish immediately - prerelease: false # Mark as stable release - make_latest: true # Update "latest" pointer - files: | # Artifacts attached to release - sbom.cdx.json - dist-${{ steps.version.outputs.version }}.tar.gz - dist-${{ steps.version.outputs.version }}.zip - env: - # SECURITY: Use RELEASE_TOKEN for protected branches - GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN || secrets.GITHUB_TOKEN }} - # ============================================================================= # SUPPLY CHAIN SECURITY - # Generate SLSA attestations for build provenance + # Generate attestations BEFORE creating release to avoid race condition + # This ensures the Main workflow is complete before triggering Publish workflow # ============================================================================= - name: Generate attestations - # Creates cryptographic proof of build provenance (SLSA Level 3) - # Attestations link artifacts to specific workflow run - # Helps detect tampering and verify authenticity - if: steps.version.outputs.changed == 'true' + # Generate SLSA provenance attestations for supply chain security + # Requires id-token: write permission uses: actions/attest-build-provenance@v2 with: subject-path: | - dist/**/*.js # All built JavaScript files - sbom.cdx.json # Software Bill of Materials - dist-*.tar.gz # Release archives - dist-*.zip + dist/**/*.js + sbom.cdx.json + dist-*-*.tar.gz + dist-*-*.zip + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: v${{ needs.build.outputs.version }} + name: v${{ needs.build.outputs.version }} + body_path: release-notes.md + draft: false + prerelease: false + make_latest: true + files: | + sbom.cdx.json + dist-${{ needs.build.outputs.version }}-*.tar.gz + dist-${{ needs.build.outputs.version }}-*.zip + env: + GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} - name: Set release output - # Output variables for downstream jobs - # Used by publish workflows to trigger distribution id: release - if: steps.version.outputs.changed == 'true' run: | echo "released=true" >> $GITHUB_OUTPUT - echo "version=${{ steps.version.outputs.version }}" >> $GITHUB_OUTPUT - echo "✅ Released version ${{ steps.version.outputs.version }}" + echo "✅ Released version ${{ needs.build.outputs.version }}" diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index d3998f1..5b211d0 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -22,10 +22,12 @@ concurrency: # contents: read - Read code for analysis # security-events: write - Upload security findings # actions: read - Access workflow artifacts +# packages: write - Required by reusable-docker.yml (not used in PR builds) permissions: contents: read security-events: write actions: read + packages: write jobs: # ============================================================================= @@ -40,84 +42,30 @@ jobs: uses: ./.github/workflows/reusable-validate.yml secrets: DEEPSOURCE_DSN: ${{ secrets.DEEPSOURCE_DSN }} + with: + validate-changesets: true - # CodeQL: Static security analysis for TypeScript/JavaScript + # Security: Static security analysis for TypeScript/JavaScript # Scans for: XSS, injection attacks, insecure patterns # Results appear in Security tab of the PR - codeql: + security: uses: ./.github/workflows/reusable-security.yml - with: - generate-sbom: false # Skip SBOM for PRs (generated at release) - run-osv-scan: false # OSV runs separately below - run-codeql: true # Enable CodeQL scanning - - # OSV: Dependency vulnerability scanning - # Uses Google's database of known vulnerabilities - # UPDATE: Check quarterly for new versions (currently v2.2.2) - vulnerability: - uses: google/osv-scanner-action/.github/workflows/osv-scanner-reusable-pr.yml@v2.2.2 - - # ============================================================================= - # WORKFLOW VALIDATION - # Lint GitHub Actions workflow files for errors - # ============================================================================= - - # Actionlint: Validate GitHub Actions workflow syntax and best practices - # Catches: undefined outputs, typos, bash errors, incorrect action inputs - actionlint: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Run actionlint - # Uses the official actionlint Docker action - # Automatically detects all workflow files in .github/workflows/ - uses: reviewdog/action-actionlint@v1 - with: - fail_level: error # Fail the job if errors are found - reporter: github-pr-check # Report errors as PR checks # ============================================================================= - # CHANGESET VALIDATION - # Ensures features and fixes have proper changelog entries + # DOCKER CONTAINER VALIDATION + # Build and scan Docker image for vulnerabilities # ============================================================================= - # Changeset check - runs in parallel with other jobs - changeset: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 0 # Full history needed to compare with main - - - name: Install pnpm - uses: pnpm/action-setup@v4 - with: - version: 10.17.0 # Pinned: Match package.json packageManager - run_install: false - standalone: true - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 22 # Pinned: Match package.json engines.node - cache: pnpm - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Fetch main branch - # Need main branch to compare changesets - run: git fetch origin main:main - - - name: Changeset status - # Validates that changesets exist for features/fixes - # FAILS IF: feat/fix commits exist without changesets - # To fix: Run 'pnpm changeset' and commit the generated file - # For non-code changes: Run 'pnpm changeset --empty' - run: pnpm changeset:status + # Docker: Build and security scan container image + # Only runs when ENABLE_DOCKER_RELEASE is configured + # Scans for: CVEs, misconfigurations, secrets in image layers + docker: + if: vars.ENABLE_DOCKER_RELEASE == 'true' + uses: ./.github/workflows/reusable-docker.yml + with: + platforms: 'linux/amd64' # Single platform for faster PR validation + save-artifact: false # Don't save artifact for PRs + image-name: 'deepsource-mcp-server-pr' # ============================================================================= # FINAL STATUS CHECK @@ -127,33 +75,43 @@ jobs: # Final status check - ensures all jobs passed # Required for branch protection rules pr-status: - needs: [validate, codeql, vulnerability, actionlint, changeset] + needs: [validate, security, docker] if: always() # Run even if previous jobs failed runs-on: ubuntu-latest steps: - name: Check status # Aggregates results from all parallel jobs # This single check can be used as a required status check - # FAILS IF: Any validation job failed or was skipped + # FAILS IF: Any validation job failed # Common failures: - # - validate: Tests fail, coverage below 80%, lint errors - # - codeql: Security vulnerabilities detected - # - vulnerability: Vulnerable dependencies found - # - actionlint: Workflow syntax errors or best practice violations - # - changeset: Missing changeset for feat/fix commits + # - validate: Tests fail, coverage below 80%, lint errors, workflow errors, missing changesets + # - security: Security vulnerabilities, vulnerable dependencies, audit failures + # - docker: Container vulnerabilities or build failures (when enabled) run: | + # Check Docker job status + # The job can be: + # - success: Job ran and passed + # - failure: Job ran and failed + # - cancelled: Job was cancelled + # - skipped: Job condition was not met (e.g., ENABLE_DOCKER_RELEASE != 'true') + DOCKER_RESULT="${{ needs.docker.result }}" + + # Docker is acceptable if it succeeded or was skipped + # It's a failure only if it actually ran and failed/was cancelled + if [ "$DOCKER_RESULT" == "failure" ] || [ "$DOCKER_RESULT" == "cancelled" ]; then + DOCKER_FAILED=true + else + DOCKER_FAILED=false + fi + if [ "${{ needs.validate.result }}" != "success" ] || \ - [ "${{ needs.codeql.result }}" != "success" ] || \ - [ "${{ needs.vulnerability.result }}" != "success" ] || \ - [ "${{ needs.actionlint.result }}" != "success" ] || \ - [ "${{ needs.changeset.result }}" != "success" ]; then + [ "${{ needs.security.result }}" != "success" ] || \ + [ "$DOCKER_FAILED" == "true" ]; then echo "❌ PR validation failed" # Check individual job results for debugging echo "Validate: ${{ needs.validate.result }}" - echo "CodeQL: ${{ needs.codeql.result }}" - echo "Vulnerability: ${{ needs.vulnerability.result }}" - echo "Actionlint: ${{ needs.actionlint.result }}" - echo "Changeset: ${{ needs.changeset.result }}" + echo "Security: ${{ needs.security.result }}" + echo "Docker: ${{ needs.docker.result }}" exit 1 fi echo "✅ All PR checks passed" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 69bdb6f..1d30a3e 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -17,6 +17,12 @@ on: required: true type: string +# Allow only one publish workflow per branch +# cancel-in-progress: false to allow multiple releases to proceed +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + # Global environment variables for consistency env: PNPM_VERSION: 10.17.0 # Pinned: Must match packageManager in package.json @@ -43,6 +49,10 @@ jobs: # Only runs if ENABLE_NPM_RELEASE variable is set to 'true' # Configure in Settings > Secrets and variables > Variables if: vars.ENABLE_NPM_RELEASE == 'true' + permissions: + contents: read + id-token: write # Required for npm provenance + actions: read # Required to download artifacts steps: - name: Determine version id: version @@ -62,29 +72,65 @@ jobs: # This ensures we publish exactly what was released ref: ${{ steps.version.outputs.tag }} - - name: Install pnpm - uses: pnpm/action-setup@v4 - with: - version: ${{ env.PNPM_VERSION }} - run_install: false - standalone: true - - name: Setup Node.js + # Node.js is required for npm publish command uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} - cache: pnpm # Configure npm registry for authentication registry-url: 'https://registry.npmjs.org' - - name: Install dependencies - # frozen-lockfile ensures exact versions from release - run: pnpm install --frozen-lockfile + - name: Determine artifact source + id: artifact + # Use shared script to find the correct NPM package artifact from the release build + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + chmod +x .github/scripts/determine-artifact.sh + .github/scripts/determine-artifact.sh \ + --tag "${{ steps.version.outputs.tag }}" \ + --repo "${{ github.repository }}" \ + --version "${{ steps.version.outputs.version }}" \ + --prefix "npm-package" \ + --output "$GITHUB_OUTPUT" + + - name: Download pre-built NPM package + id: download + # Download the pre-built, pre-scanned NPM package from main workflow + # This ensures we publish exactly what was tested + uses: actions/download-artifact@v4 + with: + name: ${{ steps.artifact.outputs.artifact_name }} + path: ./npm-artifact + run-id: ${{ steps.artifact.outputs.run_id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract pre-built package + run: | + # Check if any .tgz files exist + TARBALL=$(find ./npm-artifact -name "*.tgz" -type f | head -1) + if [ -z "$TARBALL" ]; then + echo "❌ No .tgz file found in artifact!" + echo "Contents of ./npm-artifact:" + ls -la ./npm-artifact/ + exit 1 + fi + + echo "✅ Using pre-built NPM package from main workflow" + echo "📦 Extracting: $TARBALL" + tar -xzf "$TARBALL" + + # The package extracts to a 'package' directory + # We need to move its contents to the current directory + if [ -d package ]; then + cp -r package/* . + rm -rf package + fi - - name: Build - # Build from source to ensure latest code - # FAILS IF: TypeScript errors, build configuration issues - run: pnpm build + echo "📋 Verified package contents from manifest" + if [ -f ./npm-artifact/npm-package-manifest.txt ]; then + echo "Package contains $(wc -l < ./npm-artifact/npm-package-manifest.txt) files" + fi - name: Check NPM token id: check-npm @@ -103,8 +149,10 @@ jobs: - name: Publish to NPM if: steps.check-npm.outputs.has_token == 'true' run: | - # Remove private flag to allow public publishing - jq 'del(.private)' package.json > tmp.json && mv tmp.json package.json + # Remove private flag and prepare script (which runs husky) + # The prepare script runs even with --ignore-scripts, so we must remove it + jq 'del(.private) | del(.scripts.prepare)' package.json > tmp.json && mv tmp.json package.json + # Publish with provenance for supply chain security # --provenance creates a signed attestation of the build npm publish --provenance --access public @@ -123,6 +171,11 @@ jobs: # Only runs if ENABLE_GITHUB_PACKAGES variable is set # Useful for private packages within organization if: vars.ENABLE_GITHUB_PACKAGES == 'true' + permissions: + contents: read + packages: write # Required to publish to GitHub Packages + id-token: write # Required for provenance + actions: read # Required to download artifacts steps: - name: Determine version id: version @@ -138,32 +191,65 @@ jobs: with: ref: ${{ steps.version.outputs.tag }} - - name: Install pnpm - uses: pnpm/action-setup@v4 - with: - version: ${{ env.PNPM_VERSION }} - run_install: false - standalone: true - - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} - cache: pnpm # GitHub Packages npm registry URL registry-url: 'https://npm.pkg.github.com' - - name: Install dependencies - run: pnpm install --frozen-lockfile + - name: Determine artifact source + id: artifact + # Use shared script to find the correct NPM package artifact (same as npm job) + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + chmod +x .github/scripts/determine-artifact.sh + .github/scripts/determine-artifact.sh \ + --tag "${{ steps.version.outputs.tag }}" \ + --repo "${{ github.repository }}" \ + --version "${{ steps.version.outputs.version }}" \ + --prefix "npm-package" \ + --output "$GITHUB_OUTPUT" + + - name: Download pre-built NPM package + id: download + uses: actions/download-artifact@v4 + with: + name: ${{ steps.artifact.outputs.artifact_name }} + path: ./npm-artifact + run-id: ${{ steps.artifact.outputs.run_id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract pre-built package + run: | + # Check if any .tgz files exist + TARBALL=$(find ./npm-artifact -name "*.tgz" -type f | head -1) + if [ -z "$TARBALL" ]; then + echo "❌ No .tgz file found in artifact!" + echo "Contents of ./npm-artifact:" + ls -la ./npm-artifact/ + exit 1 + fi - - name: Build - run: pnpm build + echo "✅ Using pre-built NPM package from main workflow" + echo "📦 Extracting: $TARBALL" + tar -xzf "$TARBALL" + + # The package extracts to a 'package' directory + if [ -d package ]; then + cp -r package/* . + rm -rf package + fi + + echo "📋 Verified package contents" - name: Publish to GitHub Packages run: | - # Scope package name to organization (required for GitHub Packages) - # Changes 'my-package' to '@org/my-package' - jq '.name = "@${{ github.repository_owner }}/" + .name | del(.private)' package.json > tmp.json && mv tmp.json package.json + # Scope package name to organization and remove private flag and prepare script + # The prepare script runs even with --ignore-scripts, so we must remove it + jq '.name = "@${{ github.repository_owner }}/" + .name | del(.private) | del(.scripts.prepare)' package.json > tmp.json && mv tmp.json package.json + npm publish --access public env: # SECURITY: Uses GITHUB_TOKEN for authentication @@ -172,7 +258,7 @@ jobs: # ============================================================================= # DOCKER HUB PUBLISHING - # Builds and publishes multi-platform Docker images + # Copies pre-built multi-platform image from GHCR to Docker Hub # ============================================================================= docker: @@ -183,8 +269,7 @@ jobs: if: vars.ENABLE_DOCKER_RELEASE == 'true' permissions: contents: read - packages: write # Push to GitHub Container Registry if needed - security-events: write # Upload vulnerability scan results + packages: read # Read from GitHub Container Registry steps: - name: Determine version id: version @@ -192,12 +277,8 @@ jobs: VERSION="${{ github.event_name == 'release' && github.event.release.tag_name || inputs.tag }}" VERSION="${VERSION#v}" echo "version=$VERSION" >> $GITHUB_OUTPUT - echo "🐳 Building Docker image: $VERSION" - - - name: Checkout code - uses: actions/checkout@v4 - with: - ref: v${{ steps.version.outputs.version }} + echo "tag=v$VERSION" >> $GITHUB_OUTPUT + echo "🐳 Publishing Docker image: $VERSION" - name: Check Docker credentials id: check-docker @@ -214,89 +295,57 @@ jobs: exit 0 fi - - name: Set up QEMU - # Required for multi-platform builds (arm64) - if: steps.check-docker.outputs.has_credentials == 'true' - uses: docker/setup-qemu-action@v3 - - name: Set up Docker Buildx - # Advanced Docker builder with cache support + # Required for imagetools commands if: steps.check-docker.outputs.has_credentials == 'true' uses: docker/setup-buildx-action@v3 - - name: Login to Docker Hub - # SECURITY: Authenticate with Docker Hub + - name: Login to GitHub Container Registry + # Login to GHCR to pull the pre-built image if: steps.check-docker.outputs.has_credentials == 'true' uses: docker/login-action@v3 with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - - name: Extract metadata - id: meta - if: steps.check-docker.outputs.has_credentials == 'true' - uses: docker/metadata-action@v5 - with: - images: ${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }} - tags: | - type=semver,pattern={{version}},value=${{ steps.version.outputs.version }} - type=semver,pattern={{major}}.{{minor}},value=${{ steps.version.outputs.version }} - type=semver,pattern={{major}},value=${{ steps.version.outputs.version }} - type=raw,value=latest - - - name: Build Docker image - # Initial build for vulnerability scanning (amd64 only) - # Multi-platform build happens after security scan passes - if: steps.check-docker.outputs.has_credentials == 'true' - id: build - uses: docker/build-push-action@v6 - with: - context: . - platforms: linux/amd64 # Single platform for scanning - push: false # Don't push yet, scan first - load: true # Load into Docker for scanning - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha # Use GitHub Actions cache - cache-to: type=gha,mode=max # Maximum cache retention - build-args: VERSION=${{ steps.version.outputs.version }} - - - name: Scan Docker image for vulnerabilities - # SECURITY: Scan image before publishing - # FAILS IF: Critical or high vulnerabilities found - # To debug: Check trivy-results.sarif in Security tab - # To allow: Add CVEs to .trivyignore with justification + - name: Login to Docker Hub + # SECURITY: Authenticate with Docker Hub for pushing if: steps.check-docker.outputs.has_credentials == 'true' - uses: aquasecurity/trivy-action@0.28.0 - with: - image-ref: ${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }}:${{ steps.version.outputs.version }} - format: 'sarif' # GitHub Security format - output: 'trivy-results.sarif' - severity: 'CRITICAL,HIGH' # Fail on serious issues - ignore-unfixed: false # Don't ignore unfixed CVEs - exit-code: '1' # Fail workflow if vulnerabilities found - - - name: Upload Trivy results to GitHub Security - if: always() && steps.check-docker.outputs.has_credentials == 'true' - uses: github/codeql-action/upload-sarif@v3 + uses: docker/login-action@v3 with: - sarif_file: 'trivy-results.sarif' - category: 'container-scan' + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Push Docker image to Docker Hub - # Multi-platform build and push after security scan passes - # Builds for both amd64 (Intel/AMD) and arm64 (Apple Silicon, AWS Graviton) + - name: Copy image from GHCR to Docker Hub + # Use buildx imagetools to copy multi-platform image between registries + # This properly handles multi-platform manifest lists if: steps.check-docker.outputs.has_credentials == 'true' - uses: docker/build-push-action@v6 - with: - context: . - platforms: linux/amd64,linux/arm64 # Multi-platform support - push: true # Push to registry - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha # Reuse cache from scan build - cache-to: type=gha,mode=max - build-args: VERSION=${{ steps.version.outputs.version }} + run: | + SOURCE_IMAGE="ghcr.io/${{ github.repository_owner }}/deepsource-mcp-server" + TARGET_REPO="${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }}" + VERSION="${{ steps.version.outputs.version }}" + + echo "📤 Copying multi-platform image from GHCR to Docker Hub..." + echo "Source: $SOURCE_IMAGE:$VERSION" + echo "Target: $TARGET_REPO:$VERSION" + + # Copy image with version tag + docker buildx imagetools create \ + --tag $TARGET_REPO:$VERSION \ + $SOURCE_IMAGE:$VERSION + + echo "🏷️ Creating additional tags..." + # Create alias tags for latest, major, and major.minor versions + MAJOR=$(echo "$VERSION" | cut -d. -f1) + MINOR=$(echo "$VERSION" | cut -d. -f2) + + docker buildx imagetools create --tag $TARGET_REPO:latest $TARGET_REPO:$VERSION + docker buildx imagetools create --tag $TARGET_REPO:$MAJOR $TARGET_REPO:$VERSION + docker buildx imagetools create --tag $TARGET_REPO:$MAJOR.$MINOR $TARGET_REPO:$VERSION + + echo "✅ Docker image published successfully to Docker Hub" + echo "📋 Published tags: $VERSION, latest, $MAJOR, $MAJOR.$MINOR" # ============================================================================= # NOTIFICATION diff --git a/.github/workflows/reusable-docker.yml b/.github/workflows/reusable-docker.yml new file mode 100644 index 0000000..921146a --- /dev/null +++ b/.github/workflows/reusable-docker.yml @@ -0,0 +1,312 @@ +# ============================================================================= +# REUSABLE WORKFLOW: Docker Build and Security Scanning +# PURPOSE: Build Docker images and scan for vulnerabilities with Trivy +# USAGE: Called by PR and main workflows for container validation +# OUTPUTS: Security findings uploaded to GitHub Security tab, Docker image artifact +# ============================================================================= + +name: Reusable Docker + +on: + workflow_call: + inputs: + platforms: + description: 'Docker platforms to build (e.g., linux/amd64,linux/arm64)' + type: string + default: 'linux/amd64' # Single platform for PRs, multi for main + push-image: + description: 'Whether to push image to registry (always false for this workflow)' + type: boolean + default: false + save-artifact: + description: 'Whether to save Docker image as artifact for later use' + type: boolean + default: false + artifact-name: + description: 'Name for the Docker image artifact' + type: string + default: 'docker-image' + version: + description: 'Version tag for the Docker image' + type: string + default: '' + image-name: + description: 'Docker image name (without registry)' + type: string + default: 'deepsource-mcp-server' + tag_sha: + description: 'SHA of the version tag for consistent naming' + type: string + default: '' + build_artifact: + description: 'Name of the pre-built TypeScript artifact to use' + type: string + default: '' + outputs: + image-digest: + description: 'Docker image digest' + value: ${{ jobs.docker.outputs.digest }} + artifact-name: + description: 'Name of the saved artifact' + value: ${{ jobs.docker.outputs.artifact-name }} + +# SECURITY: Required permissions for Docker operations +# Note: packages: write is only needed if pushing to GitHub Container Registry +# Calling workflows can omit it if not pushing images +permissions: + contents: read # Read source code + security-events: write # Upload Trivy scan results + packages: write # Push Docker images to GitHub Container Registry + +jobs: + docker: + runs-on: ubuntu-latest + outputs: + digest: ${{ steps.build.outputs.digest }} + artifact-name: ${{ inputs.artifact-name }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Download build artifact + # Download pre-built TypeScript if artifact name provided + if: inputs.build_artifact != '' + uses: actions/download-artifact@v4 + with: + name: ${{ inputs.build_artifact }} + + # ============================================================================= + # DOCKER SETUP + # Configure build environment for single or multi-platform builds + # ============================================================================= + + - name: Set up QEMU + # Required for multi-platform builds (arm64) + if: contains(inputs.platforms, 'arm64') + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + # Advanced Docker builder with cache support + uses: docker/setup-buildx-action@v3 + + - name: Login to GitHub Container Registry + # Login to GHCR for multi-platform builds that need to be pushed to registry + # Single-platform builds for PRs don't need registry push + if: inputs.save-artifact && contains(inputs.platforms, ',') + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # ============================================================================= + # DOCKER BUILD + # Build image with layer caching for efficiency + # ============================================================================= + + - name: Generate Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + # Use GHCR for multi-platform artifact builds, local name otherwise + images: | + ${{ (inputs.save-artifact && contains(inputs.platforms, ',')) && format('ghcr.io/{0}/{1}', github.repository_owner, inputs.image-name) || inputs.image-name }} + tags: | + type=raw,value=${{ inputs.version }},enable=${{ inputs.version != '' }} + type=raw,value=latest,enable=${{ inputs.version != '' }} + type=ref,event=pr + type=sha,format=short + + - name: Determine build configuration + # Set clear variables for build mode to improve readability + id: build-config + run: | + # Determine if we're building for multiple platforms + IS_MULTI_PLATFORM="false" + if echo "${{ inputs.platforms }}" | grep -q ','; then + IS_MULTI_PLATFORM="true" + fi + + # For multi-platform builds with save-artifact, push to GHCR + # For single-platform builds or PR builds, load locally or save to tar + SAVE_ARTIFACT="${{ inputs.save-artifact }}" + SHOULD_PUSH="false" + CAN_LOAD="false" + OUTPUT_TYPE="" + + if [ "$SAVE_ARTIFACT" = "true" ] && [ "$IS_MULTI_PLATFORM" = "true" ]; then + # Multi-platform artifact build: push to GHCR + SHOULD_PUSH="true" + CAN_LOAD="false" + elif [ "$SAVE_ARTIFACT" != "true" ] && [ "$IS_MULTI_PLATFORM" = "false" ]; then + # Single-platform PR build: load locally + CAN_LOAD="true" + else + # Single-platform artifact build: save to tar + CAN_LOAD="false" + SHA_TO_USE="${{ inputs.tag_sha || github.sha }}" + OUTPUT_TYPE="type=docker,dest=${{ inputs.artifact-name }}-${SHA_TO_USE}.tar" + fi + + { + echo "is_multi_platform=$IS_MULTI_PLATFORM" + echo "should_push=$SHOULD_PUSH" + echo "can_load=$CAN_LOAD" + echo "output_type=$OUTPUT_TYPE" + } >> $GITHUB_OUTPUT + + echo "📋 Build configuration:" + echo " Multi-platform: $IS_MULTI_PLATFORM" + echo " Save artifact: $SAVE_ARTIFACT" + echo " Should push: $SHOULD_PUSH" + echo " Can load: $CAN_LOAD" + echo " Output type: $OUTPUT_TYPE" + + - name: Build Docker image + id: build + uses: docker/build-push-action@v6 + with: + context: . + platforms: ${{ inputs.platforms }} + push: ${{ steps.build-config.outputs.should_push == 'true' }} + load: ${{ steps.build-config.outputs.can_load == 'true' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha # Use GitHub Actions cache + cache-to: type=gha,mode=max # Maximum cache retention + build-args: | + VERSION=${{ inputs.version || github.sha }} + outputs: ${{ steps.build-config.outputs.output_type }} + + # ============================================================================= + # SECURITY SCANNING + # Trivy vulnerability scanning with configurable severity + # ============================================================================= + + - name: Determine Trivy scan configuration + # Set clear variables for scan inputs to improve readability + id: scan-config + run: | + # Determine scanning mode based on build configuration + CAN_LOAD="${{ steps.build-config.outputs.can_load }}" + if [ "$CAN_LOAD" = "true" ]; then + # For loaded single-platform images, scan by image reference + FIRST_TAG=$(echo "${{ steps.meta.outputs.tags }}" | head -n1) + { + echo "scan_input=" + echo "scan_image_ref=$FIRST_TAG" + } >> $GITHUB_OUTPUT + echo "Using image reference for scanning: $FIRST_TAG" + else + # For multi-platform or artifact builds, scan the tar file + SHA_TO_USE="${{ inputs.tag_sha || github.sha }}" + { + echo "scan_input=${{ inputs.artifact-name }}-${SHA_TO_USE}.tar" + echo "scan_image_ref=" + } >> $GITHUB_OUTPUT + echo "Using tar file for scanning: ${{ inputs.artifact-name }}-${SHA_TO_USE}.tar" + fi + + - name: Run Trivy vulnerability scanner + # SECURITY: Scan image for vulnerabilities before any distribution + # NOTE: Multi-platform OCI exports cannot be scanned from tar files + # Scans for vulnerabilities, secrets, misconfigurations, and licenses + # License findings are informational only (see LICENSES.md) + if: steps.build-config.outputs.can_load == 'true' || !contains(inputs.platforms, ',') + uses: aquasecurity/trivy-action@0.28.0 + with: + input: ${{ steps.scan-config.outputs.scan_input }} + image-ref: ${{ steps.scan-config.outputs.scan_image_ref }} + exit-code: '1' + format: 'sarif' + hide-progress: false + output: 'trivy-results.sarif' + severity: 'HIGH,CRITICAL' + scanners: 'vuln,secret,misconfig' + trivyignores: '.trivyignore' + version: 'latest' + env: + TRIVY_DEBUG: 'true' + + - name: Check Trivy results for vulnerabilities + # Fail build if non-license security issues are found + # License findings are informational and don't fail the build + if: steps.build-config.outputs.can_load == 'true' || !contains(inputs.platforms, ',') + run: | + if [ -f trivy-results.sarif ]; then + # Check for vulnerabilities, secrets, or misconfigurations (not licenses) + SECURITY_ISSUES=$(jq -r '.runs[0].results[] | select(.ruleId | startswith("CVE-") or startswith("SECRET-") or startswith("CONFIG-")) | .level' trivy-results.sarif 2>/dev/null | wc -l || echo "0") + if [ "$SECURITY_ISSUES" -gt 0 ]; then + echo "::error::Found $SECURITY_ISSUES security issue(s) in container image" + echo "Review the scan results in the Security tab after SARIF upload" + exit 1 + fi + echo "No security vulnerabilities found (license findings are informational)" + fi + + - name: Upload Trivy results to GitHub Security + # Always upload results, even if scan fails + # Results viewable at: Security > Code scanning alerts + if: always() && (steps.build-config.outputs.can_load == 'true' || !contains(inputs.platforms, ',')) + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: 'trivy-results.sarif' + category: 'container-scan-${{ github.event_name }}' + + - name: Upload Trivy SARIF as artifact + # Upload SARIF file as artifact for debugging and inspection + if: always() && (steps.build-config.outputs.can_load == 'true' || !contains(inputs.platforms, ',')) + uses: actions/upload-artifact@v4 + with: + name: trivy-${{ github.sha }} + path: trivy-results.sarif + retention-days: 7 + + # ============================================================================= + # ARTIFACT STORAGE + # Save Docker image tar files for single-platform builds + # Multi-platform builds are pushed to GHCR instead + # ============================================================================= + + - name: Compress Docker image artifact + # Compress the tar file to reduce storage costs + # Only for single-platform builds (multi-platform builds pushed to GHCR) + if: inputs.save-artifact && !contains(inputs.platforms, ',') + run: | + SHA_TO_USE="${{ inputs.tag_sha || github.sha }}" + echo "Compressing Docker image artifact..." + gzip -9 ${{ inputs.artifact-name }}-${SHA_TO_USE}.tar + ls -lh ${{ inputs.artifact-name }}-${SHA_TO_USE}.tar.gz + + - name: Upload Docker image artifact + # Store single-platform image tar for deterministic publishing + # Multi-platform images are stored in GHCR registry + if: inputs.save-artifact && !contains(inputs.platforms, ',') + uses: actions/upload-artifact@v4 + with: + name: ${{ inputs.artifact-name }}-${{ inputs.tag_sha || github.sha }} + path: ${{ inputs.artifact-name }}-${{ inputs.tag_sha || github.sha }}.tar.gz + retention-days: 7 # Keep for a week (enough for release cycle) + compression-level: 0 # Already compressed with gzip + + # ============================================================================= + # SUPPLY CHAIN SECURITY + # Generate attestations for build provenance (main builds only) + # ============================================================================= + + - name: Generate attestations for GHCR images + # Creates cryptographic proof of build provenance for multi-platform images + # Multi-platform images are stored in GHCR registry + if: inputs.save-artifact && contains(inputs.platforms, ',') && inputs.version != '' && env.ACTIONS_ID_TOKEN_REQUEST_URL != '' + uses: actions/attest-build-provenance@v2 + with: + subject-name: ghcr.io/${{ github.repository_owner }}/${{ inputs.image-name }} + subject-digest: ${{ steps.build.outputs.digest }} + push-to-registry: true + + - name: Generate attestations for tar artifacts + # Creates cryptographic proof of build provenance for single-platform tar files + if: inputs.save-artifact && !contains(inputs.platforms, ',') && inputs.version != '' && env.ACTIONS_ID_TOKEN_REQUEST_URL != '' + uses: actions/attest-build-provenance@v2 + with: + subject-path: ${{ inputs.artifact-name }}-${{ inputs.tag_sha || github.sha }}.tar.gz diff --git a/.github/workflows/reusable-security.yml b/.github/workflows/reusable-security.yml index 43744a4..f16bf4a 100644 --- a/.github/workflows/reusable-security.yml +++ b/.github/workflows/reusable-security.yml @@ -1,8 +1,9 @@ # ============================================================================= # REUSABLE WORKFLOW: Security Scanning Suite -# PURPOSE: Run security scans (CodeQL, OSV) and generate SBOM +# PURPOSE: Run security scans (audit, OSV) and generate SBOM # USAGE: Called by PR and main workflows for security validation # OUTPUTS: Security findings uploaded to GitHub Security tab, SBOM artifact +# NOTE: CodeQL has its own dedicated workflow (codeql.yml) for better integration # ============================================================================= name: Reusable Security @@ -18,18 +19,10 @@ on: description: 'pnpm version (should match package.json packageManager)' type: string default: '10.17.0' # UPDATE: When upgrading pnpm - run-codeql: - description: 'Run CodeQL static analysis for security vulnerabilities' - type: boolean - default: true run-osv-scan: description: 'Run OSV scanner for dependency vulnerabilities' type: boolean default: true - generate-sbom: - description: 'Generate Software Bill of Materials (SBOM)' - type: boolean - default: false # SECURITY: Required permissions for security scanning permissions: @@ -42,12 +35,10 @@ permissions: # security: # uses: ./.github/workflows/reusable-security.yml # with: -# run-codeql: true # run-osv-scan: true -# generate-sbom: true jobs: - security: + audit: runs-on: ubuntu-latest steps: - name: Checkout code @@ -71,76 +62,26 @@ jobs: uses: actions/setup-node@v4 with: node-version: ${{ inputs.node-version }} - cache: pnpm + cache: pnpm # Cache dependencies for speed - name: Install dependencies # Dependencies needed for accurate SBOM generation run: pnpm install --frozen-lockfile - # ============================================================================= - # CODEQL STATIC ANALYSIS - # Scans for security vulnerabilities in source code - # ============================================================================= - - - name: Initialize CodeQL - if: inputs.run-codeql - # Setup CodeQL for JavaScript/TypeScript analysis - # Detects: XSS, SQL injection, path traversal, etc. - uses: github/codeql-action/init@v3 - with: - languages: javascript-typescript - - - name: Run CodeQL Analysis - if: inputs.run-codeql - # Perform analysis and upload results to Security tab - # Results viewable at: Security > Code scanning alerts - # FAILS IF: Critical security issues detected (configurable) - uses: github/codeql-action/analyze@v3 - - # ============================================================================= - # SOFTWARE BILL OF MATERIALS (SBOM) - # Documents all dependencies for supply chain security - # ============================================================================= - - - name: Generate SBOM - if: inputs.generate-sbom - # Creates CycloneDX format SBOM (sbom.cdx.json) - # Includes all production and dev dependencies - run: pnpm sbom - - - name: Upload SBOM - if: inputs.generate-sbom - # Make SBOM available for compliance and auditing - uses: actions/upload-artifact@v4 - with: - name: sbom-${{ github.sha }} - path: sbom.cdx.json - retention-days: 7 - - # ============================================================================= - # OSV VULNERABILITY SCANNING - # Checks dependencies against Google's OSV database - # ============================================================================= - - - name: Run OSV Scanner - if: inputs.run-osv-scan - # Download and run Google's OSV Scanner - # Checks: package.json, pnpm-lock.yaml for known vulnerabilities - # UPDATE: Check quarterly for new scanner versions (currently v1.9.1) - run: | - # Install OSV Scanner - curl -sSfL https://github.com/google/osv-scanner/releases/download/v1.9.1/osv-scanner_linux_amd64 -o osv-scanner - chmod +x osv-scanner - - # Run OSV Scanner and generate SARIF report - # '|| true' prevents this step from failing; results are always uploaded in the next step due to 'always()' - ./osv-scanner --format sarif --output results.sarif . || true - - - name: Upload OSV results to GitHub Security - if: inputs.run-osv-scan && always() - # Upload findings to Security tab even if scan finds issues - # Results viewable at: Security > Code scanning alerts - uses: github/codeql-action/upload-sarif@v3 - with: - sarif_file: results.sarif - category: 'osv-scan' # Separate category from CodeQL + - name: Security audit + # Check for known vulnerabilities in dependencies + # FAILS IF: Critical vulnerabilities found + # To fix: Run 'pnpm update' or add overrides in package.json + run: pnpm audit --audit-level critical + + osv-scan: + if: inputs.run-osv-scan + uses: google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@v2.2.1 + with: + # Scan entire project including all manifests (package.json, pnpm-lock.yaml) + scan-args: |- + ./ + permissions: + security-events: write # Required to upload findings to Security tab + actions: read + contents: read diff --git a/.github/workflows/reusable-setup.yml b/.github/workflows/reusable-setup.yml deleted file mode 100644 index 96ed799..0000000 --- a/.github/workflows/reusable-setup.yml +++ /dev/null @@ -1,110 +0,0 @@ -# ============================================================================= -# REUSABLE WORKFLOW: Environment Setup -# PURPOSE: Standardized Node.js/pnpm setup with caching for all workflows -# USAGE: Called by other workflows to ensure consistent environment -# OUTPUTS: node_modules and dist artifacts for downstream jobs -# ============================================================================= - -name: Reusable Setup - -on: - workflow_call: - inputs: - node-version: - description: 'Node.js version to install (should match package.json engines)' - type: string - default: '22' # UPDATE: When upgrading Node.js in package.json - pnpm-version: - description: 'pnpm version to install (should match packageManager in package.json)' - type: string - default: '10.17.0' # UPDATE: When upgrading pnpm in package.json - install-deps: - description: 'Whether to install npm dependencies' - type: boolean - default: true - build: - description: 'Whether to run the build command' - type: boolean - default: false - -# EXAMPLE USAGE: -# jobs: -# setup: -# uses: ./.github/workflows/reusable-setup.yml -# with: -# node-version: '22' -# pnpm-version: '10.17.0' -# install-deps: true -# build: true - -jobs: - setup: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 0 # Full history for better git operations - - - name: Install pnpm - # pnpm must be installed before Node.js setup for caching - uses: pnpm/action-setup@v4 - with: - version: ${{ inputs.pnpm-version }} - run_install: false # Dependencies installed in separate step - standalone: true # Faster installation method - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: ${{ inputs.node-version }} - cache: pnpm # Automatically caches pnpm store - - - name: Get pnpm store directory - # Determine pnpm's global store location for caching - id: pnpm-cache - shell: bash - run: | - echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - - name: Setup pnpm cache - # Cache pnpm store to speed up dependency installation - # Cache key includes lock file hash to bust cache on dep changes - # Saves 1-3 minutes on subsequent runs - uses: actions/cache@v4 - with: - path: ${{ env.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store- # Fallback to partial cache match - - - name: Install dependencies - if: inputs.install-deps - # frozen-lockfile ensures reproducible installs - # FAILS IF: pnpm-lock.yaml doesn't match package.json - run: pnpm install --frozen-lockfile - - - name: Build - if: inputs.build - # Build TypeScript to JavaScript - # FAILS IF: TypeScript errors, missing dependencies - run: pnpm build - - - name: Upload node_modules - # Share installed dependencies with other jobs - # Reduces install time in parallel workflows - if: inputs.install-deps - uses: actions/upload-artifact@v4 - with: - name: node-modules-${{ github.sha }} - path: node_modules/ - retention-days: 1 # Auto-cleanup after 1 day - - - name: Upload dist - # Share built artifacts with other jobs - if: inputs.build - uses: actions/upload-artifact@v4 - with: - name: dist-${{ github.sha }} - path: dist/ - retention-days: 1 # Auto-cleanup after 1 day diff --git a/.github/workflows/reusable-validate.yml b/.github/workflows/reusable-validate.yml index 0df0089..c0d225a 100644 --- a/.github/workflows/reusable-validate.yml +++ b/.github/workflows/reusable-validate.yml @@ -7,9 +7,6 @@ name: Reusable Validate -permissions: - contents: read - on: workflow_call: inputs: @@ -21,10 +18,14 @@ on: description: 'pnpm version (should match package.json packageManager)' type: string default: '10.17.0' # UPDATE: When upgrading pnpm + validate-changesets: + description: 'validate that a changeset exists on the branch' + type: boolean + default: false secrets: DEEPSOURCE_DSN: description: 'DeepSource DSN for test coverage reporting' - required: false + required: true # EXAMPLE USAGE: # jobs: @@ -34,10 +35,57 @@ on: # upload-coverage: true # For PRs to show coverage jobs: - validate: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Full history for accurate analysis + + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: ${{ inputs.pnpm-version }} + run_install: false + standalone: true + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ inputs.node-version }} + cache: pnpm # Cache dependencies for speed + + - name: Install dependencies + # Ensures exact versions from lock file + # FAILS IF: Lock file out of sync with package.json + run: pnpm install --frozen-lockfile + + - name: Tests with coverage + # Run test suite with coverage + # Coverage enforces 80% minimum threshold for all metrics + # FAILS IF: Tests fail or coverage below 80% (when coverage enabled) + # To debug: Check test output and coverage/index.html + run: pnpm test:coverage + + - name: Report test coverage to DeepSource + uses: deepsourcelabs/test-coverage-action@v1.1.3 + with: + key: javascript + coverage-file: coverage/lcov.info + dsn: ${{ secrets.DEEPSOURCE_DSN }} + + - name: Upload coverage + # Make coverage reports available for review + # Download from Actions tab to view detailed HTML report + uses: actions/upload-artifact@v4 + with: + name: coverage-${{ github.sha }} + path: coverage/ + retention-days: 7 # Keep for a week for PR reviews + + lint: runs-on: ubuntu-latest - permissions: - contents: read # Minimal permission for checkout steps: - name: Checkout code uses: actions/checkout@v4 @@ -67,18 +115,6 @@ jobs: # All checks run in sequence to provide clear failure messages # ============================================================================= - - name: Security audit - # Check for known vulnerabilities in dependencies - # FAILS IF: Critical vulnerabilities found - # To fix: Run 'pnpm update' or add overrides in package.json - run: pnpm audit --audit-level critical - - - name: Check for unused dependencies - # Validate all declared dependencies are actually used - # FAILS IF: Unused dependencies found - # To fix: Remove unused deps or add to .depcheckrc ignore list - run: pnpm deps:check - - name: Type checking # Validate TypeScript types without emitting files # FAILS IF: Type errors in any .ts file @@ -111,25 +147,15 @@ jobs: # To debug: Run 'pnpm lint:workflows' locally run: pnpm lint:workflows - - name: Tests with coverage - # Run test suite with coverage - # Coverage enforces 80% minimum threshold for all metrics - # FAILS IF: Tests fail or coverage below 80% (when coverage enabled) - # To debug: Check test output and coverage/index.html - run: pnpm test:coverage - - - name: Report test coverage to DeepSource - uses: deepsourcelabs/test-coverage-action@v1.1.3 - with: - key: javascript - coverage-file: coverage/lcov.info - dsn: ${{ secrets.DEEPSOURCE_DSN }} - - - name: Upload coverage - # Make coverage reports available for review - # Download from Actions tab to view detailed HTML report - uses: actions/upload-artifact@v4 - with: - name: coverage-${{ github.sha }} - path: coverage/ - retention-days: 7 # Keep for a week for PR reviews + - name: Fetch main branch for changesets + if: inputs.validate-changesets + # Need main branch to compare changesets + run: git fetch origin main:main + + - name: Changeset status + if: inputs.validate-changesets + # Validates that changesets exist for features/fixes + # FAILS IF: feat/fix commits exist without changesets + # To fix: Run 'pnpm changeset' and commit the generated file + # For non-code changes: Run 'pnpm changeset --empty' + run: pnpm changeset:status diff --git a/Dockerfile b/Dockerfile index a30315d..446f3b3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -25,8 +25,20 @@ RUN pnpm run build RUN rm -rf node_modules && \ pnpm install --frozen-lockfile --prod --ignore-scripts +# Create non-root user for security (DS002) +RUN addgroup -g 1001 -S nodejs && \ + adduser -S nodejs -u 1001 && \ + chown -R nodejs:nodejs /app + +# Switch to non-root user +USER nodejs + # Expose the port the app runs on EXPOSE 3000 +# Add health check (DS026) +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD node -e "process.exit(0)" + # Start the server CMD ["node", "--experimental-specifier-resolution=node", "dist/index.js"] \ No newline at end of file