diff --git a/.github/actions/audit-npm/CHANGELOG.md b/.github/actions/audit-npm/CHANGELOG.md new file mode 100644 index 0000000..5e2c7c4 --- /dev/null +++ b/.github/actions/audit-npm/CHANGELOG.md @@ -0,0 +1,13 @@ +# audit-npm action Changelog + +All notable changes to the **audit-npm** action are documented in this file. + +## v1.0.0 + +### Added + +- Initial release of audit-npm composite action +- Runs `npm audit` +- Parses and summarizes vulnerabilities by severity +- Outputs a markdown summary and a boolean audit gate +- Audit gate fails if a critical or high production vulnerability exists diff --git a/.github/actions/audit-npm/README.md b/.github/actions/audit-npm/README.md new file mode 100644 index 0000000..4df8388 --- /dev/null +++ b/.github/actions/audit-npm/README.md @@ -0,0 +1,79 @@ +# NPM Audit Action + +## 🧭 Summary + +Runs `npm audit`, parses the results, and outputs a markdown summary and a pass/fail gate for use in CI workflows. Designed for Node.js projects to automate dependency vulnerability checks. + +## Scope/Limitations + +- Only supports projects with a `package.json` in the working directory. +- Requires `jq` (preinstalled on GitHub-hosted runners). +- Only checks for vulnerabilities reported by `npm audit`. + +## 🔒 Permissions + +The following GHA permissions are required to use this step: + +```yaml +permissions: + contents: read +``` + +## Dependencies + +- `jq` — JSON processor (preinstalled on GitHub-hosted Ubuntu runners) +- `npm` — Node.js package manager + +## 📤 Outputs + +| Name | Description | +| -------------- | ------------------------------------------------------------------------------------------------ | +| `gate_passed` | true/false if audit gate passed (no critical or high vulnerabilities in production dependencies) | +| `gate_summary` | Markdown summary of audit results | + +## 🚀 Usage + +Basic usage example: + +```yaml +- name: Audit NPM dependencies + id: audit + uses: ./.github/actions/audit-npm + continue-on-error: true +``` + +Example outputs: + +```yaml +steps.audit.outputs.gate_passed +steps.audit.outputs.gate_summary +``` + +Example usage of outputs in later steps: + +```yaml +- name: Show audit summary + run: echo "${{ steps.audit.outputs.gate_summary }}" + +- name: Check audit gate + if: steps.audit.outputs.gate_passed == 'false' + run: | + echo "Audit gate failed" + exit 1 +``` + +## 🧠 Notes + +- The audit gate only checks production dependencies for critical or high vulnerabilities. +- The summary table includes both production and all dependencies. +- This action does not auto-fix vulnerabilities; it only reports them. + +## Versioning + +This action uses namespaced tags for versioning and is tracked in the CHANGELOG. + +```text +action/audit-npm/vX.Y.Z +``` + +See the repository's versioning documentation for details on how tags are validated and created. diff --git a/.github/actions/audit-npm/action.yml b/.github/actions/audit-npm/action.yml new file mode 100644 index 0000000..f04d907 --- /dev/null +++ b/.github/actions/audit-npm/action.yml @@ -0,0 +1,75 @@ +name: Audit NPM Dependencies +description: Run npm audit, parse results, and output a summary and audit gate + +outputs: + gate_passed: + description: 'true/false if audit gate passed (no critical or high vulnerabilities in production dependencies)' + value: ${{ steps.evaluate-gate.outputs.gate_passed }} + gate_summary: + description: 'Markdown summary of audit results' + value: ${{ steps.generate-summary.outputs.gate_summary }} + +runs: + using: 'composite' + steps: + - name: Audit All + id: audit-all + shell: bash + run: npm audit --json > audit-all.json || true + + - name: Audit Production + id: audit-prod + shell: bash + run: npm audit --json --omit=dev > audit-prod.json || true + + - name: Parse audit reports + id: parse-audit + shell: bash + run: | + jq_counts=' + (.metadata.vulnerabilities // {}) as $v | + [ + ($v.critical // 0), + ($v.high // 0), + ($v.moderate // 0), + ($v.low // 0) + ] | @tsv + ' + read -r ALL_CRIT ALL_HIGH ALL_MOD ALL_LOW < <(jq -r "$jq_counts" audit-all.json) + read -r PRD_CRIT PRD_HIGH PRD_MOD PRD_LOW < <(jq -r "$jq_counts" audit-prod.json) + echo "ALL_CRIT=$ALL_CRIT" >> $GITHUB_ENV + echo "ALL_HIGH=$ALL_HIGH" >> $GITHUB_ENV + echo "ALL_MOD=$ALL_MOD" >> $GITHUB_ENV + echo "ALL_LOW=$ALL_LOW" >> $GITHUB_ENV + echo "PRD_CRIT=$PRD_CRIT" >> $GITHUB_ENV + echo "PRD_HIGH=$PRD_HIGH" >> $GITHUB_ENV + echo "PRD_MOD=$PRD_MOD" >> $GITHUB_ENV + echo "PRD_LOW=$PRD_LOW" >> $GITHUB_ENV + + - name: Evaluate audit gate + id: evaluate-gate + shell: bash + run: | + if [ "$PRD_CRIT" -gt 0 ] || [ "$PRD_HIGH" -gt 0 ]; then + gate_passed="false" + else + gate_passed="true" + fi + echo "gate_passed=${gate_passed}" >> "$GITHUB_OUTPUT" + + - name: Generate audit summary + id: generate-summary + shell: bash + run: | + { + echo "gate_summary<Gate checks production dependencies only (fails on any Critical or High)" + echo "EOF" + } >> "$GITHUB_OUTPUT" diff --git a/.github/actions/run-npm-script/CHANGELOG.md b/.github/actions/run-npm-script/CHANGELOG.md new file mode 100644 index 0000000..502d584 --- /dev/null +++ b/.github/actions/run-npm-script/CHANGELOG.md @@ -0,0 +1,12 @@ +# run-npm-script action Changelog + +All notable changes to the **run-npm-script** action are documented in this file. + +## v1.0.0 + +### Added + +- Initial release of run-npm-script composite action +- Runs the specified npm script from the input, if present in package.json +- Outputs status: success, failure, or notpresent +- Supports custom working directory diff --git a/.github/actions/run-npm-script/README.md b/.github/actions/run-npm-script/README.md new file mode 100644 index 0000000..a8af3bd --- /dev/null +++ b/.github/actions/run-npm-script/README.md @@ -0,0 +1,80 @@ +# Run NPM Script Action + +## 🧭 Summary + +Runs a specified npm script (e.g., build, lint:check, test) if present in package.json, and outputs the result as success, failure, or notpresent. Useful for DRY, reusable npm script checks in CI workflows. + +## Scope/Limitations + +- Only works with projects that have a package.json in the specified working directory. +- Requires jq (preinstalled on GitHub-hosted runners). +- Only checks for the existence of the script key, not its content. + +## 🔒 Permissions + +The following GHA permissions are required to use this step: + +```yaml +permissions: + contents: read +``` + +## Dependencies + +- `jq` — JSON processor (preinstalled on GitHub-hosted Ubuntu runners) +- `npm` — Node.js package manager + +## ⚙️ Inputs + +| Name | Required | Description | +| ------------------- | -------- | -------------------------------------------------------------------------------- | +| `script` | ✅ | The npm script to run (e.g., build, lint:check, test) | +| `working_directory` | ❌ | Directory containing package.json, pass in '.' if you want the current directory | + +## 📤 Outputs + +| Name | Description | +| -------- | ------------------------------- | +| `status` | success, failure, or notpresent | + +## 🚀 Usage + +Basic usage example: + +```yaml +- name: Run build script + id: build + uses: ./.github/actions/run-npm-script + with: + working-directory: '.' + script: build +``` + +Example outputs: + +```yaml +steps.build.outputs.status +``` + +Example usage of outputs in later steps: + +```yaml +if: steps.build.outputs.status == 'success' +run: echo "Build passed!" +``` + +## 🧠 Notes + +- The action will output notpresent if the script is not found in package.json or if package.json is missing. +- The action will output failure if the script exists but fails. +- The action will output success if the script runs and exits with code 0. + +## Versioning + +This action uses namespaced tags for versioning and is tracked in the CHANGELOG. + +```text +action/run-npm-script/vX.Y.Z +``` + +See the repository's versioning documentation for details on how tags are validated and created. diff --git a/.github/actions/run-npm-script/action.yml b/.github/actions/run-npm-script/action.yml new file mode 100644 index 0000000..2512c8b --- /dev/null +++ b/.github/actions/run-npm-script/action.yml @@ -0,0 +1,44 @@ +name: 'Run NPM Script' +description: 'Run an npm script if present, and output status' + +inputs: + script: + description: 'The npm script to run (e.g., build, lint:check, test)' + required: true + default: '' + working_directory: + description: 'Directory containing package.json (optional)' + required: false + default: '.' + +outputs: + status: + description: 'success, failure, or notpresent' + value: ${{ steps.run-script.outputs.status }} + +runs: + using: 'composite' + steps: + - name: Run npm script + id: run-script + shell: bash + env: + WORKING_DIRECTORY: ${{ inputs.working_directory }} + SCRIPT: ${{ inputs.script }} + run: | + cd "$WORKING_DIRECTORY" + if ! [ -f package.json ]; then + echo "No package.json found. Skipping $SCRIPT." + echo "status=notpresent" >> "$GITHUB_OUTPUT" + exit 0 + fi + if ! jq -e --arg s "$SCRIPT" '.scripts | has($s)' package.json > /dev/null; then + echo "script $SCRIPT not found in package.json. Skipping." + echo "status=notpresent" >> "$GITHUB_OUTPUT" + exit 0 + fi + if npm run "$SCRIPT"; then + echo "status=success" >> "$GITHUB_OUTPUT" + else + echo "status=failure" >> "$GITHUB_OUTPUT" + fi diff --git a/.github/workflows/internal_on_push_ci.yml b/.github/workflows/internal_on_push_ci.yml index 3870b67..af76816 100644 --- a/.github/workflows/internal_on_push_ci.yml +++ b/.github/workflows/internal_on_push_ci.yml @@ -13,31 +13,11 @@ permissions: jobs: internal-ci: name: Internal CI - runs-on: ubuntu-latest - - steps: - - name: Checkout repo - uses: actions/checkout@v4 - - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version-file: .nvmrc - - - name: Install dependencies - run: npm ci - - - name: Dependency Audit - run: npm audit - - - name: Test - run: npm test - - - name: Lint Check - run: npm run lint:check - - - name: Format Check - run: npm run format:check + uses: ./.github/workflows/run_npm_ci_scripts.yml + secrets: inherit + with: + working_directory: '.' + commit_identifier: ${{ github.sha }} semgrep: uses: ./.github/workflows/run_semgrep_scan.yml diff --git a/.github/workflows/run_npm_ci_scripts.yml b/.github/workflows/run_npm_ci_scripts.yml new file mode 100644 index 0000000..d595a97 --- /dev/null +++ b/.github/workflows/run_npm_ci_scripts.yml @@ -0,0 +1,131 @@ +name: NPM CI + +on: + workflow_call: + inputs: + working_directory: + description: 'The working directory containing the package.json file with the npm ci scripts to run' + required: true + type: string + commit_identifier: + description: 'SHA or tag to run ci against' + required: true + type: string + +permissions: + contents: read + checks: write + pull-requests: write + +jobs: + run-npm-ci-scripts: + name: NPM CI + runs-on: ubuntu-latest + env: + NODE_AUTH_TOKEN: ${{ secrets.ORG_GITHUB_PACKAGES_READ_ONLY_TOKEN }} + WORKING_DIRECTORY: ${{ inputs.working_directory }} + steps: + - name: Change to working directory + run: | + echo "Changing to working directory: $WORKING_DIRECTORY" + cd "$WORKING_DIRECTORY" + + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ inputs.commit_identifier }} + + - name: Set up Node + uses: actions/setup-node@v3 + with: + node-version-file: .nvmrc + cache: 'npm' + registry-url: 'https://npm.pkg.github.com/' + cache-dependency-path: '**/package-lock.json' + + - name: Install dependencies + run: npm ci + + - name: Audit dependencies + uses: ./.github/actions/audit-npm + id: audit-npm + continue-on-error: true + + - name: Build + uses: ./.github/actions/run-npm-script + id: build + with: + working_directory: '.' + script: build + + - name: Lint + uses: ./.github/actions/run-npm-script + id: lint + with: + working_directory: '.' + script: lint:check + + - name: Format + uses: ./.github/actions/run-npm-script + id: format + with: + working_directory: '.' + script: format:check + + - name: Test + uses: ./.github/actions/run-npm-script + id: test + with: + working_directory: '.' + script: test + + - name: Job Summary + if: always() + env: + AUDIT_STATUS: ${{ steps.audit-npm.outputs.gate_passed == 'true' && 'success' || 'failure' }} + AUDIT_SUMMARY: ${{ steps.audit-npm.outputs.gate_summary }} + BUILD_STATUS: ${{ steps.build.outputs.status || steps.build.outcome }} + LINT_STATUS: ${{ steps.lint.outputs.status || steps.lint.outcome }} + FORMAT_STATUS: ${{ steps.format.outputs.status || steps.format.outcome }} + TEST_STATUS: ${{ steps.test.outputs.status || steps.test.outcome }} + run: | + status_emoji () { + case "$1" in + success) echo "✅";; + failure) echo "❌";; + cancelled) echo "➖";; + skipped|""|notpresent) echo "⏩";; + *) echo "❓";; + esac + } + + { + echo "## 🚪 Gate Summary" + echo "triggered by: \`${{ github.event_name }}\` on \`${{ github.ref_name }}\`" + echo "Run: [#${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})" + echo "" + echo "" + echo "| Audit | Build | Lint | Format | Test |" + echo "|:-----:|:-----:|:----:|:------:|:----:|" + echo "| $(status_emoji "$AUDIT_STATUS") "$AUDIT_STATUS" | $(status_emoji "$BUILD_STATUS") "$BUILD_STATUS" | $(status_emoji "$LINT_STATUS") "$LINT_STATUS" | $(status_emoji "$FORMAT_STATUS") "$FORMAT_STATUS" | $(status_emoji "$TEST_STATUS") "$TEST_STATUS" |" + echo "" + echo "$AUDIT_SUMMARY" + } > job-summary.md + cat job-summary.md >> "$GITHUB_STEP_SUMMARY" + + - name: Set Run Outcome + if: ${{ steps.audit-npm.outputs.gate_passed == 'false' || steps.build.outputs.status == 'failure' || steps.lint.outputs.status == 'failure' || steps.format.outputs.status == 'failure' || steps.test.outputs.status == 'failure' }} + env: + AUDIT_STATUS: ${{ steps.audit-npm.outputs.gate_passed == 'true' && 'passed' || 'failed' }} + BUILD_STATUS: ${{ steps.build.outputs.status == 'success' && 'passed' || 'failed' }} + LINT_STATUS: ${{ steps.lint.outputs.status == 'success' && 'passed' || 'failed' }} + FORMAT_STATUS: ${{ steps.format.outputs.status == 'success' && 'passed' || 'failed' }} + TEST_STATUS: ${{ steps.test.outputs.status == 'success' && 'passed' || 'failed' }} + run: | + echo "❌ One or more gate steps failed:" + echo " Audit Gate: $AUDIT_STATUS" + echo " Build: $BUILD_STATUS" + echo " Lint: $LINT_STATUS" + echo " Format: $FORMAT_STATUS" + echo " Test: $TEST_STATUS" + exit 1