diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..fd1614d --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @tetherto/miningos-bk-merge diff --git a/.github/actions/git-secrets/action.yml b/.github/actions/git-secrets/action.yml new file mode 100644 index 0000000..b91ef76 --- /dev/null +++ b/.github/actions/git-secrets/action.yml @@ -0,0 +1,20 @@ +name: 'Git Secrets Scan' +description: 'Scan repository for secrets using git-secrets' + +runs: + using: "composite" + steps: + - name: Install and configure git-secrets + shell: bash + run: | + git clone https://github.com/awslabs/git-secrets.git + cd git-secrets + git checkout 1.3.0 + sudo make install + cd .. + git secrets --install + git secrets --register-aws + + - name: Run git-secrets scan + shell: bash + run: git secrets --scan-history diff --git a/.github/actions/node-restore-cache/action.yml b/.github/actions/node-restore-cache/action.yml new file mode 100644 index 0000000..0784abf --- /dev/null +++ b/.github/actions/node-restore-cache/action.yml @@ -0,0 +1,36 @@ +name: 'Node Restore Cache' +description: 'Restore Node.js dependencies from cache' + +inputs: + cache-key: + description: 'Cache key from setup-node step' + required: true + +runs: + using: composite + steps: + # Restore-only: avoids "another job may be creating this cache" when setup and downstream jobs race on save + - name: Restore node modules cache + id: cache-node-modules + uses: actions/cache/restore@v4 + with: + path: node_modules + key: ${{ inputs.cache-key }} + restore-keys: | + node-modules-${{ runner.os }}-20.x- + + - name: Setup Node.js + if: steps.cache-node-modules.outputs.cache-hit != 'true' + uses: actions/setup-node@v6 + with: + node-version: '20.x' + + - name: Install Dependencies + if: steps.cache-node-modules.outputs.cache-hit != 'true' + shell: bash + run: | + if [ -f package-lock.json ]; then + npm ci + else + npm install + fi diff --git a/.github/actions/node-setup-cache/action.yml b/.github/actions/node-setup-cache/action.yml new file mode 100644 index 0000000..c260c9b --- /dev/null +++ b/.github/actions/node-setup-cache/action.yml @@ -0,0 +1,73 @@ +name: 'Node.js Setup with Cache' +description: 'Sets up Node.js with dependency caching for the initial setup job' +author: 'miningos-devops' + +inputs: + node-version: + description: 'Node.js version to use' + required: false + default: '20.x' + install-dependencies: + description: 'Whether to install dependencies on cache miss' + required: false + default: 'true' + additional-packages: + description: 'Additional system packages to install (space-separated)' + required: false + default: '' + +outputs: + cache-key: + description: 'The generated cache key for node_modules' + value: ${{ steps.cache-keys.outputs.cache-key }} + cache-hit: + description: 'Whether the cache was hit' + value: ${{ steps.cache-node-modules.outputs.cache-hit }} + +runs: + using: composite + steps: + - name: Generate cache keys + id: cache-keys + shell: bash + run: | + NODE_VERSION="${{ inputs.node-version }}" + BASE_KEY="node-modules-${{ runner.os }}-${NODE_VERSION}" + # Stable key per deps so downstream jobs and future runs get cache hits + HASH_KEY="${BASE_KEY}-${{ hashFiles('package-lock.json', 'package.json') }}" + echo "cache-key=${HASH_KEY}" >> $GITHUB_OUTPUT + echo "base-key=${BASE_KEY}" >> $GITHUB_OUTPUT + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: ${{ inputs.node-version }} + + - name: Install additional system packages + if: inputs.additional-packages != '' + shell: bash + run: | + echo "Installing additional packages: ${{ inputs.additional-packages }}" + sudo apt update + sudo apt install -y ${{ inputs.additional-packages }} + + - name: Install dependencies + if: inputs.install-dependencies == 'true' + shell: bash + run: | + if [ -f package-lock.json ]; then + echo "Found package-lock.json, using npm ci for deterministic installs" + npm ci + else + echo "No package-lock.json found, using npm install" + npm install + fi + + - name: Cache node modules + id: cache-node-modules + uses: actions/cache@v4 + with: + path: node_modules + key: ${{ steps.cache-keys.outputs.cache-key }} + restore-keys: | + ${{ steps.cache-keys.outputs.base-key }}- diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c2098f5 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,332 @@ +name: PR - Test & Validate (Local) + +on: + push: + branches: [main, staging, dev, develop] + pull_request: + branches: [main, staging, dev, develop] + types: [opened, reopened, synchronize] + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + NODE_VERSION: '20.x' + +jobs: + dependency-review: + name: ๐Ÿ“‹ Dependency Review + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + permissions: + contents: read + pull-requests: read + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + ref: ${{ github.event.pull_request.head.sha }} + + - name: Dependency Review + uses: actions/dependency-review-action@v4 + with: + fail-on-severity: high + vulnerability-check: true + license-check: false + + setup: + name: ๐Ÿ“ฆ Setup Dependencies + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + outputs: + cache-key: ${{ steps.setup-node.outputs.cache-key }} + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + persist-credentials: true + ref: ${{ github.event.pull_request.number && format('refs/pull/{0}/head', github.event.pull_request.number) || github.ref }} + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: ${{ env.NODE_VERSION }} + + - name: Check package-lock.json consistency + continue-on-error: true + run: | + if [ -f package-lock.json ]; then + npm install --package-lock-only --no-audit --no-fund + git diff --exit-code package-lock.json || ( + echo "::error::package-lock.json is out of sync with package.json. Run 'npm install' and commit the updated lockfile." + exit 1 + ) + fi + + - name: Setup Node.js with cache + id: setup-node + uses: ./.github/actions/node-setup-cache + with: + node-version: ${{ env.NODE_VERSION }} + + supply-chain: + name: ๐Ÿ” Supply Chain Security + runs-on: ubuntu-latest + timeout-minutes: 6 + needs: setup + permissions: + contents: read + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + persist-credentials: true + ref: ${{ github.event.pull_request.number && format('refs/pull/{0}/head', github.event.pull_request.number) || github.ref }} + + - name: Restore Node.js and Cache + uses: ./.github/actions/node-restore-cache + with: + cache-key: ${{ needs.setup.outputs.cache-key }} + + - name: Verify package integrity (npm audit signatures) + run: | + echo "๐Ÿ” Verifying npm package signatures..." + npm audit signatures 2>&1 || echo "::warning::Some packages may not have verified signatures (npm 10.7+ for full support)" + continue-on-error: true + + - name: Check for known vulnerabilities (npm audit) + run: | + echo "๐Ÿ›ก๏ธ Checking for known vulnerabilities..." + npm audit --audit-level=high 2>&1 || echo "::warning::High severity vulnerabilities detected. Consider 'npm audit fix' or updating dependencies." + continue-on-error: true + + lint: + name: โœจ Lint Code + runs-on: ubuntu-latest + timeout-minutes: 10 + needs: setup + permissions: + contents: read + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + persist-credentials: true + ref: ${{ github.event.pull_request.number && format('refs/pull/{0}/head', github.event.pull_request.number) || github.ref }} + + - name: Restore Node.js and Cache + uses: ./.github/actions/node-restore-cache + with: + cache-key: ${{ needs.setup.outputs.cache-key }} + + - name: Run linting + continue-on-error: true + run: | + echo "Running linting..." + npm run lint --if-present || echo "::warning::Linting failed but continuing workflow" + + - name: Run typecheck (optional) + continue-on-error: true + run: | + npm run typecheck --if-present || true + + test-and-sonarqube: + name: ๐Ÿงช Test Coverage & SonarQube Analysis + runs-on: ubuntu-latest + timeout-minutes: 20 + needs: setup + permissions: + contents: read + outputs: + coverage_result: ${{ steps.coverage-summary.outputs.coverage_result }} + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + persist-credentials: true + fetch-depth: 0 + ref: ${{ github.event.pull_request.number && format('refs/pull/{0}/head', github.event.pull_request.number) || github.ref }} + + - name: Restore Node.js and Cache + uses: ./.github/actions/node-restore-cache + with: + cache-key: ${{ needs.setup.outputs.cache-key }} + + - name: Setup project configuration + run: | + echo "Setting up project configuration..." + if [ -f ./setup-config.sh ]; then + chmod +x ./setup-config.sh + ./setup-config.sh + fi + + - name: Run tests with coverage + id: run-tests + run: | + echo "Running tests with coverage..." + npm run test:coverage --if-present || npm test --if-present || echo "::warning::No test:coverage or test script; skipping" + + - name: Coverage (threshold + summary) + id: coverage-summary + if: always() + run: | + MIN=80 + # Vitest: detect by config (handles its own thresholds); brittle uses Istanbul + nyc check. + if [ -f vitest.config.js ] || [ -f vitest.config.ts ]; then + if [ -f coverage/coverage-final.json ]; then + if [ "${{ steps.run-tests.outcome }}" = "success" ]; then + STATUS_CELL="โœ… PASS" + else + STATUS_CELL="โŒ FAIL (coverage below threshold in vitest.config.js)" + fi + echo "coverage_result=$STATUS_CELL Vitest (thresholds in vitest.config.js)" >> "$GITHUB_OUTPUT" + else + echo "coverage_result=โš ๏ธ No coverage report" >> "$GITHUB_OUTPUT" + fi + elif [ -f coverage/coverage-final.json ]; then + mkdir -p .nyc_output + cp coverage/coverage-final.json .nyc_output/out.json + set +e + echo "Running nyc check-coverage (threshold ${MIN}%)..." + npx --yes nyc check-coverage --lines=$MIN --statements=$MIN --functions=$MIN --branches=$MIN + EXIT=$? + set -e + echo "Running nyc report (text-summary)..." + NYC_REPORT=$(npx --yes nyc report --reporter=text-summary 2>&1) + echo "$NYC_REPORT" + STMT=$(echo "$NYC_REPORT" | sed -n 's/^Statements[^:]*: *\([0-9.]*\)%.*/\1/p') + BRANCH=$(echo "$NYC_REPORT" | sed -n 's/^Branches[^:]*: *\([0-9.]*\)%.*/\1/p') + FN=$(echo "$NYC_REPORT" | sed -n 's/^Functions[^:]*: *\([0-9.]*\)%.*/\1/p') + LINES=$(echo "$NYC_REPORT" | sed -n 's/^Lines[^:]*: *\([0-9.]*\)%.*/\1/p') + [ -z "$STMT" ] && STMT="โ€”"; [ -z "$BRANCH" ] && BRANCH="โ€”"; [ -z "$FN" ] && FN="โ€”"; [ -z "$LINES" ] && LINES="โ€”" + [ $EXIT -eq 0 ] && ICON="โœ…" || ICON="โŒ" + echo "coverage_result=$ICON Stmt ${STMT}% | Branch ${BRANCH}% | Fn ${FN}% | Lines ${LINES}% (min ${MIN}%)" >> "$GITHUB_OUTPUT" + exit $EXIT + else + echo "coverage_result=โš ๏ธ No coverage report" >> "$GITHUB_OUTPUT" + fi + + - name: Run build (optional) + continue-on-error: true + run: npm run build --if-present || true + + - name: Tailscale + id: tailscale + if: (github.event_name == 'push' && github.ref == 'refs/heads/main') || github.event_name == 'workflow_dispatch' + uses: tailscale/github-action@v4 + with: + oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }} + oauth-secret: ${{ secrets.TS_OAUTH_CLIENT_SECRET }} + tags: tag:ci-sast + version: latest + ping: dev-sonarcube-0.tail8a2a3f.ts.net + + - name: Generate ESLint JSON report + if: steps.tailscale.outcome == 'success' + continue-on-error: true + run: | + if [ -f eslint.config.cjs ]; then + npx eslint . --format json --output-file eslint-report.json + fi + + - name: Prepare SonarQube Configuration + if: steps.tailscale.outcome == 'success' + run: | + echo "Creating sonar-project.properties..." + + # Base configuration + cat > sonar-project.properties << 'SONAR_EOF' + sonar.projectKey=${{ github.event.repository.name }} + sonar.projectName=${{ github.event.repository.name }} + sonar.projectVersion=${{ github.sha }} + sonar.sources=. + sonar.language=js + sonar.sourceEncoding=UTF-8 + sonar.javascript.lcov.reportPaths=coverage/lcov.info + sonar.javascript.node.maxspace=4096 + sonar.javascript.environments=node + sonar.qualitygate.wait=true + sonar.test.inclusions=**/*.test.js,**/*.spec.js + SONAR_EOF + + # Add ESLint report if it exists + if [ -f eslint-report.json ]; then + echo "sonar.eslint.reportPaths=eslint-report.json" >> sonar-project.properties + fi + + # Base exclusions + BASE_EXCLUSIONS="**/node_modules/**,**/coverage/**,**/dist/**,**/build/**,.github/**,**/*.test.js,**/*.spec.js,**/*.json,**/*.md,**/*.yml,**/*.yaml" + + # Add tests configuration if folder exists + if [ -d "tests" ]; then + echo "Tests folder found - including test configuration" + echo "sonar.tests=tests" >> sonar-project.properties + echo "sonar.exclusions=${BASE_EXCLUSIONS},**/tests/**" >> sonar-project.properties + echo "sonar.cpd.exclusions=**/tests/**,**/node_modules/**" >> sonar-project.properties + else + echo "No tests folder found - skipping test configuration" + echo "sonar.exclusions=${BASE_EXCLUSIONS}" >> sonar-project.properties + fi + + echo "Configuration file created" + + - name: SonarQube Scan + if: steps.tailscale.outcome == 'success' + continue-on-error: true + uses: SonarSource/sonarqube-scan-action@v7.0.0 + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} + + summary: + name: ๐Ÿ“‹ CI Summary + runs-on: ubuntu-latest + needs: [setup, supply-chain, lint, test-and-sonarqube] + if: always() && !cancelled() + permissions: + contents: read + + steps: + - name: ๐Ÿ“ฅ Checkout for summary + uses: actions/checkout@v6 + with: + fetch-depth: 1 + + - name: ๐Ÿ“Š Print summary + env: + R_SETUP: ${{ needs.setup.result || 'skipped' }} + R_SUPPLY: ${{ needs.supply-chain.result || 'skipped' }} + R_LINT: ${{ needs.lint.result || 'skipped' }} + R_TEST: ${{ needs.test-and-sonarqube.result || 'skipped' }} + COVERAGE: ${{ needs.test-and-sonarqube.outputs.coverage_result || 'โ€”' }} + run: | + { + echo "## ๐Ÿ“Š CI Pipeline Summary" + echo "" + echo "### ๐Ÿ“‹ Commit information" + echo "- **Commit:** $(git rev-parse HEAD)" + echo "- **Message:** $(git log -1 --pretty=format:'%s')" + echo "- **Author:** $(git log -1 --pretty=format:'%an <%ae>')" + echo "- **Date:** $(git log -1 --pretty=format:'%ad' --date=short)" + echo "" + echo "### ๐ŸŽฏ Job results" + [ "${R_SETUP}" != "skipped" ] && echo "- ๐Ÿ“ฆ Setup: ${R_SETUP:-?}" + [ "${R_SUPPLY}" != "skipped" ] && echo "- ๐Ÿ” Supply Chain: ${R_SUPPLY:-?}" + [ "${R_LINT}" != "skipped" ] && echo "- โœจ Lint: ${R_LINT:-?}" + [ "${R_TEST}" != "skipped" ] && echo "- ๐Ÿงช Test & SonarQube: ${R_TEST:-?}" + echo "- ๐Ÿ“Š Coverage: ${COVERAGE}" + echo "" + echo "### ๐Ÿ”ง Pipeline" + echo "- โœ… Dependency review (PR only)" + echo "- โœ… Lockfile check, npm audit, lint, tests" + echo "- โœ… SonarQube (push to main or manual run)" + } | tee -a "$GITHUB_STEP_SUMMARY"