From 80aedaaf4903d17f6c479946406b0ec2679d5b16 Mon Sep 17 00:00:00 2001 From: ssvoss Date: Sat, 17 Jan 2026 13:10:54 -0500 Subject: [PATCH 01/11] initial commit of new reusable gha stub --- .github/workflows/internal_on_push_ci.yml | 7 ++++ .github/workflows/run_npm_ci_scripts.yml | 47 +++++++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 .github/workflows/run_npm_ci_scripts.yml diff --git a/.github/workflows/internal_on_push_ci.yml b/.github/workflows/internal_on_push_ci.yml index 3870b67..557d238 100644 --- a/.github/workflows/internal_on_push_ci.yml +++ b/.github/workflows/internal_on_push_ci.yml @@ -11,6 +11,13 @@ permissions: checks: write # needed if reporter is github-pr-check or github-check jobs: + internal-ci2: + uses: ./.github/workflows/run_npm_ci_scripts.yml + secrets: inherit + with: + working-directory: '.' + commit-identifier: ${{ github.sha }} + internal-ci: name: Internal CI runs-on: ubuntu-latest diff --git a/.github/workflows/run_npm_ci_scripts.yml b/.github/workflows/run_npm_ci_scripts.yml new file mode 100644 index 0000000..99f4f7d --- /dev/null +++ b/.github/workflows/run_npm_ci_scripts.yml @@ -0,0 +1,47 @@ +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 }} + steps: + - name: Change to working directory + run: | + echo "WORKING_DIRECTORY=${{ inputs.working-directory }}" + cd ${{ inputs.working-directory }} + + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ inputs.commit-identifier }} + + - name: Set up Node.js + 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' + always-auth: true + + - name: Install dependencies + run: npm ci From ff7ccd5b0cbbecc30e07c5667ed68ef2c6d28fc9 Mon Sep 17 00:00:00 2001 From: ssvoss Date: Sat, 17 Jan 2026 14:19:47 -0500 Subject: [PATCH 02/11] add composite action for npm audit --- .github/actions/audit-npm/CHANGELOG.md | 21 ++++++ .github/actions/audit-npm/README.md | 85 +++++++++++++++++++++++ .github/actions/audit-npm/action.yml | 67 ++++++++++++++++++ .github/workflows/internal_on_push_ci.yml | 3 +- .github/workflows/run_npm_ci_scripts.yml | 9 ++- 5 files changed, 182 insertions(+), 3 deletions(-) create mode 100644 .github/actions/audit-npm/CHANGELOG.md create mode 100644 .github/actions/audit-npm/README.md create mode 100644 .github/actions/audit-npm/action.yml diff --git a/.github/actions/audit-npm/CHANGELOG.md b/.github/actions/audit-npm/CHANGELOG.md new file mode 100644 index 0000000..4d6b0fe --- /dev/null +++ b/.github/actions/audit-npm/CHANGELOG.md @@ -0,0 +1,21 @@ +# {action-name} action Changelog + +All notable changes to the **{action-name}** action are documented in this file. + +## v{semver} + +### Added + +- ... + +### Changed + +- ... + +### Fixed + +- ... + +### Removed + +- ... diff --git a/.github/actions/audit-npm/README.md b/.github/actions/audit-npm/README.md new file mode 100644 index 0000000..06f62a9 --- /dev/null +++ b/.github/actions/audit-npm/README.md @@ -0,0 +1,85 @@ +# {action-name} Action + +## 🧭 Summary + + + +## Scope/Limitations + + + +## 🔒 Permissions + + + +The following GHA permissions are required to use this step: + +```yaml +permissions: + contents: read +``` + +## Dependencies + + + + + +## ⚙️ Inputs + +| Name | Required | Description | +| ------------ | -------- | ----------- | +| `input-name` | ✅/❌ | | +| `input-name` | ✅/❌ | | + +## 📤 Outputs + +| Name | Description | +| ------------- | ----------- | +| `output-name` | | +| `output-name` | | + +## 🚀 Usage + +Basic usage example: + +```yaml +- name: Name for step + id: + uses: ./.github/actions/ + with: + : +``` + +Example outputs: + +```yaml +steps..outputs. +``` + +Example usage of outputs in later steps: + +```yaml +if: steps..outputs. == '' +run: echo "Condition met" +``` + +## 🧠 Notes + + + +## Versioning + +This action uses namespaced tags for versioning and is tracked in the CHANGELOG. + +```text +action//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..417f739 --- /dev/null +++ b/.github/actions/audit-npm/action.yml @@ -0,0 +1,67 @@ +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) + + - 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/workflows/internal_on_push_ci.yml b/.github/workflows/internal_on_push_ci.yml index 557d238..6d9ad06 100644 --- a/.github/workflows/internal_on_push_ci.yml +++ b/.github/workflows/internal_on_push_ci.yml @@ -12,6 +12,7 @@ permissions: jobs: internal-ci2: + name: Internal CI uses: ./.github/workflows/run_npm_ci_scripts.yml secrets: inherit with: @@ -19,7 +20,7 @@ jobs: commit-identifier: ${{ github.sha }} internal-ci: - name: Internal CI + name: Internal CI (old) runs-on: ubuntu-latest steps: diff --git a/.github/workflows/run_npm_ci_scripts.yml b/.github/workflows/run_npm_ci_scripts.yml index 99f4f7d..0883f2c 100644 --- a/.github/workflows/run_npm_ci_scripts.yml +++ b/.github/workflows/run_npm_ci_scripts.yml @@ -29,12 +29,12 @@ jobs: echo "WORKING_DIRECTORY=${{ inputs.working-directory }}" cd ${{ inputs.working-directory }} - - name: Checkout code + - name: Checkout uses: actions/checkout@v4 with: ref: ${{ inputs.commit-identifier }} - - name: Set up Node.js + - name: Set up Node uses: actions/setup-node@v3 with: node-version-file: .nvmrc @@ -45,3 +45,8 @@ jobs: - name: Install dependencies run: npm ci + + - name: Audit dependencies + uses: ./.github/actions/audit-npm + id: audit-npm + continue-on-error: true From 89a2638065373c0fd2e40202eee32c921a262194 Mon Sep 17 00:00:00 2001 From: ssvoss Date: Sat, 17 Jan 2026 14:55:45 -0500 Subject: [PATCH 03/11] write summary with audit result --- .github/workflows/run_npm_ci_scripts.yml | 39 ++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/.github/workflows/run_npm_ci_scripts.yml b/.github/workflows/run_npm_ci_scripts.yml index 0883f2c..8b75c01 100644 --- a/.github/workflows/run_npm_ci_scripts.yml +++ b/.github/workflows/run_npm_ci_scripts.yml @@ -50,3 +50,42 @@ jobs: uses: ./.github/actions/audit-npm id: audit-npm continue-on-error: true + + - 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 }} + run: | + status_emoji () { + case "$1" in + success) echo "✅";; + failure) echo "❌";; + cancelled) echo "🛑";; + skipped|"") 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 |" + echo "|:-----:|" + echo "| $(status_emoji "$AUDIT_STATUS") "$AUDIT_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' }} + env: + AUDIT_STATUS: ${{ steps.audit-npm.outputs.gate_passed == 'true' && '✅ passed' || '❌ failed' }} + run: | + echo "❌ One or more gate steps failed:" + echo " Audit Gate: $AUDIT_STATUS" + exit 1 From e277b70104948818be3f27215c593c630383674b Mon Sep 17 00:00:00 2001 From: ssvoss Date: Sat, 17 Jan 2026 15:09:45 -0500 Subject: [PATCH 04/11] fix variable access --- .github/actions/audit-npm/action.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/actions/audit-npm/action.yml b/.github/actions/audit-npm/action.yml index 417f739..f04d907 100644 --- a/.github/actions/audit-npm/action.yml +++ b/.github/actions/audit-npm/action.yml @@ -37,6 +37,14 @@ runs: ' 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 From ecba662c0e71987046053cbc8a9ad7768992f7d2 Mon Sep 17 00:00:00 2001 From: ssvoss Date: Sat, 17 Jan 2026 15:32:10 -0500 Subject: [PATCH 05/11] add readme and change log for composite audit action --- .github/actions/audit-npm/CHANGELOG.md | 24 ++++------ .github/actions/audit-npm/README.md | 64 ++++++++++++-------------- 2 files changed, 37 insertions(+), 51 deletions(-) diff --git a/.github/actions/audit-npm/CHANGELOG.md b/.github/actions/audit-npm/CHANGELOG.md index 4d6b0fe..5e2c7c4 100644 --- a/.github/actions/audit-npm/CHANGELOG.md +++ b/.github/actions/audit-npm/CHANGELOG.md @@ -1,21 +1,13 @@ -# {action-name} action Changelog +# audit-npm action Changelog -All notable changes to the **{action-name}** action are documented in this file. +All notable changes to the **audit-npm** action are documented in this file. -## v{semver} +## v1.0.0 ### Added -- ... - -### Changed - -- ... - -### Fixed - -- ... - -### Removed - -- ... +- 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 index 06f62a9..4df8388 100644 --- a/.github/actions/audit-npm/README.md +++ b/.github/actions/audit-npm/README.md @@ -1,17 +1,17 @@ -# {action-name} Action +# 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 @@ -21,65 +21,59 @@ permissions: ## Dependencies - - - - -## ⚙️ Inputs - -| Name | Required | Description | -| ------------ | -------- | ----------- | -| `input-name` | ✅/❌ | | -| `input-name` | ✅/❌ | | +- `jq` — JSON processor (preinstalled on GitHub-hosted Ubuntu runners) +- `npm` — Node.js package manager ## 📤 Outputs -| Name | Description | -| ------------- | ----------- | -| `output-name` | | -| `output-name` | | +| 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: Name for step - id: - uses: ./.github/actions/ - with: - : +- name: Audit NPM dependencies + id: audit + uses: ./.github/actions/audit-npm + continue-on-error: true ``` Example outputs: ```yaml -steps..outputs. +steps.audit.outputs.gate_passed +steps.audit.outputs.gate_summary ``` Example usage of outputs in later steps: ```yaml -if: steps..outputs. == '' -run: echo "Condition met" +- 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//vX.Y.Z +action/audit-npm/vX.Y.Z ``` See the repository's versioning documentation for details on how tags are validated and created. From d10f07122833019ec0561db7a44a318f2c499a63 Mon Sep 17 00:00:00 2001 From: ssvoss Date: Sat, 17 Jan 2026 16:33:59 -0500 Subject: [PATCH 06/11] add build step to npm ci --- .github/workflows/run_npm_ci_scripts.yml | 32 ++++++++++++++++++------ 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/.github/workflows/run_npm_ci_scripts.yml b/.github/workflows/run_npm_ci_scripts.yml index 8b75c01..4a6aa1b 100644 --- a/.github/workflows/run_npm_ci_scripts.yml +++ b/.github/workflows/run_npm_ci_scripts.yml @@ -51,18 +51,34 @@ jobs: id: audit-npm continue-on-error: true + - name: Build + id: build + continue-on-error: true + run: | + if ! [ -f package.json ] || ! jq -e '.scripts.build' package.json > /dev/null; then + echo "No build script found in package.json." + echo "build_status=notpresent" >> $GITHUB_OUTPUT + exit 0 + fi + if npm run build; then + echo "build_status=passed" >> $GITHUB_OUTPUT + else + echo "build_status=failed" >> $GITHUB_OUTPUT + fi + - 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.build_status || steps.build.outcome }} run: | status_emoji () { case "$1" in success) echo "✅";; failure) echo "❌";; - cancelled) echo "🛑";; - skipped|"") echo "⏭️";; + cancelled) echo "➖";; + skipped|"") echo "⏩";; *) echo "❓";; esac } @@ -73,19 +89,21 @@ jobs: echo "Run: [#${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})" echo "" echo "" - echo "| Audit |" - echo "|:-----:|" - echo "| $(status_emoji "$AUDIT_STATUS") "$AUDIT_STATUS" |" + echo "| Audit | Build |" + echo "|:-----:|:-----:|" + echo "| $(status_emoji "$AUDIT_STATUS") "$AUDIT_STATUS" | $(status_emoji "$BUILD_STATUS") "$BUILD_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' }} + if: ${{ steps.audit-npm.outputs.gate_passed == 'false' || steps.build.outputs.build_status == 'failure' }} env: - AUDIT_STATUS: ${{ steps.audit-npm.outputs.gate_passed == 'true' && '✅ passed' || '❌ failed' }} + AUDIT_STATUS: ${{ steps.audit-npm.outputs.gate_passed == 'true' && 'passed' || 'failed' }} + BUILD_STATUS: ${{ steps.build.outputs.build_status }} run: | echo "❌ One or more gate steps failed:" echo " Audit Gate: $AUDIT_STATUS" + echo " Build: $BUILD_STATUS" exit 1 From 23c75487343c39574025c8a67ff9c67528a0e57f Mon Sep 17 00:00:00 2001 From: ssvoss Date: Sat, 17 Jan 2026 16:46:16 -0500 Subject: [PATCH 07/11] add lint and format checks --- .github/workflows/run_npm_ci_scripts.yml | 52 ++++++++++++++++++++---- 1 file changed, 44 insertions(+), 8 deletions(-) diff --git a/.github/workflows/run_npm_ci_scripts.yml b/.github/workflows/run_npm_ci_scripts.yml index 4a6aa1b..512eb66 100644 --- a/.github/workflows/run_npm_ci_scripts.yml +++ b/.github/workflows/run_npm_ci_scripts.yml @@ -61,9 +61,39 @@ jobs: exit 0 fi if npm run build; then - echo "build_status=passed" >> $GITHUB_OUTPUT + echo "build_status=success" >> $GITHUB_OUTPUT else - echo "build_status=failed" >> $GITHUB_OUTPUT + echo "build_status=failure" >> $GITHUB_OUTPUT + fi + + - name: Lint + id: lint + continue-on-error: true + run: | + if ! [ -f package.json ] || ! jq -e '.scripts.["lint:check"]' package.json > /dev/null; then + echo "No lint script found in package.json." + echo "lint_status=notpresent" >> $GITHUB_OUTPUT + exit 0 + fi + if npm run lint:check; then + echo "lint_status=success" >> $GITHUB_OUTPUT + else + echo "lint_status=failure" >> $GITHUB_OUTPUT + fi + + - name: Format + id: format + continue-on-error: true + run: | + if ! [ -f package.json ] || ! jq -e '.scripts.["format:check"]' package.json > /dev/null; then + echo "No format script found in package.json." + echo "format_status=notpresent" >> $GITHUB_OUTPUT + exit 0 + fi + if npm run format:check; then + echo "format_status=success" >> $GITHUB_OUTPUT + else + echo "format_status=failure" >> $GITHUB_OUTPUT fi - name: Job Summary @@ -72,13 +102,15 @@ jobs: AUDIT_STATUS: ${{ steps.audit-npm.outputs.gate_passed == 'true' && 'success' || 'failure' }} AUDIT_SUMMARY: ${{ steps.audit-npm.outputs.gate_summary }} BUILD_STATUS: ${{ steps.build.outputs.build_status || steps.build.outcome }} + LINT_STATUS: ${{ steps.lint.outputs.lint_status || steps.lint.outcome }} + FORMAT_STATUS: ${{ steps.format.outputs.format_status || steps.format.outcome }} run: | status_emoji () { case "$1" in success) echo "✅";; failure) echo "❌";; cancelled) echo "➖";; - skipped|"") echo "⏩";; + skipped|""|notpresent) echo "⏩";; *) echo "❓";; esac } @@ -89,21 +121,25 @@ jobs: echo "Run: [#${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})" echo "" echo "" - echo "| Audit | Build |" - echo "|:-----:|:-----:|" - echo "| $(status_emoji "$AUDIT_STATUS") "$AUDIT_STATUS" | $(status_emoji "$BUILD_STATUS") "$BUILD_STATUS" |" + echo "| Audit | Build | Lint | Format |" + 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" |" 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.build_status == 'failure' }} + if: ${{ steps.audit-npm.outputs.gate_passed == 'false' || steps.build.outputs.build_status == 'failure' || steps.lint.outputs.lint_status == 'failure' || steps.format.outputs.format_status == 'failure' }} env: AUDIT_STATUS: ${{ steps.audit-npm.outputs.gate_passed == 'true' && 'passed' || 'failed' }} - BUILD_STATUS: ${{ steps.build.outputs.build_status }} + BUILD_STATUS: ${{ steps.build.outputs.build_status == 'success' && 'passed' || 'failed' }} + LINT_STATUS: ${{ steps.lint.outputs.lint_status == 'success' && 'passed' || 'failed' }} + FORMAT_STATUS: ${{ steps.format.outputs.format_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" exit 1 From 521babec2c92f5355fe2f53311da1f39cb8a6ed4 Mon Sep 17 00:00:00 2001 From: ssvoss Date: Sat, 17 Jan 2026 17:12:44 -0500 Subject: [PATCH 08/11] add test to ci run --- .github/workflows/run_npm_ci_scripts.yml | 26 ++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/.github/workflows/run_npm_ci_scripts.yml b/.github/workflows/run_npm_ci_scripts.yml index 512eb66..a91ea1f 100644 --- a/.github/workflows/run_npm_ci_scripts.yml +++ b/.github/workflows/run_npm_ci_scripts.yml @@ -96,6 +96,21 @@ jobs: echo "format_status=failure" >> $GITHUB_OUTPUT fi + - name: Test + id: test + continue-on-error: true + run: | + if ! [ -f package.json ] || ! jq -e '.scripts.test' package.json > /dev/null; then + echo "No test script found in package.json." + echo "test_status=notpresent" >> $GITHUB_OUTPUT + exit 0 + fi + if npm test; then + echo "test_status=success" >> $GITHUB_OUTPUT + else + echo "test_status=failure" >> $GITHUB_OUTPUT + fi + - name: Job Summary if: always() env: @@ -104,6 +119,7 @@ jobs: BUILD_STATUS: ${{ steps.build.outputs.build_status || steps.build.outcome }} LINT_STATUS: ${{ steps.lint.outputs.lint_status || steps.lint.outcome }} FORMAT_STATUS: ${{ steps.format.outputs.format_status || steps.format.outcome }} + TEST_STATUS: ${{ steps.test.outputs.test_status || steps.test.outcome }} run: | status_emoji () { case "$1" in @@ -121,25 +137,27 @@ jobs: echo "Run: [#${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})" echo "" echo "" - echo "| Audit | Build | Lint | Format |" - 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" |" + 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.build_status == 'failure' || steps.lint.outputs.lint_status == 'failure' || steps.format.outputs.format_status == 'failure' }} + if: ${{ steps.audit-npm.outputs.gate_passed == 'false' || steps.build.outputs.build_status == 'failure' || steps.lint.outputs.lint_status == 'failure' || steps.format.outputs.format_status == 'failure' || steps.test.outputs.test_status == 'failure' }} env: AUDIT_STATUS: ${{ steps.audit-npm.outputs.gate_passed == 'true' && 'passed' || 'failed' }} BUILD_STATUS: ${{ steps.build.outputs.build_status == 'success' && 'passed' || 'failed' }} LINT_STATUS: ${{ steps.lint.outputs.lint_status == 'success' && 'passed' || 'failed' }} FORMAT_STATUS: ${{ steps.format.outputs.format_status == 'success' && 'passed' || 'failed' }} + TEST_STATUS: ${{ steps.test.outputs.test_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 From eaebb9ea9376d7ff9fce5f346658aec357d28d48 Mon Sep 17 00:00:00 2001 From: ssvoss Date: Sat, 17 Jan 2026 17:33:19 -0500 Subject: [PATCH 09/11] extract run npm script ci logic --- .github/actions/run-npm-script/action.yml | 44 ++++++++++++ .github/workflows/run_npm_ci_scripts.yml | 83 +++++++---------------- 2 files changed, 69 insertions(+), 58 deletions(-) create mode 100644 .github/actions/run-npm-script/action.yml diff --git a/.github/actions/run-npm-script/action.yml b/.github/actions/run-npm-script/action.yml new file mode 100644 index 0000000..876ac38 --- /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/run_npm_ci_scripts.yml b/.github/workflows/run_npm_ci_scripts.yml index a91ea1f..a8960a0 100644 --- a/.github/workflows/run_npm_ci_scripts.yml +++ b/.github/workflows/run_npm_ci_scripts.yml @@ -41,7 +41,6 @@ jobs: cache: 'npm' registry-url: 'https://npm.pkg.github.com/' cache-dependency-path: '**/package-lock.json' - always-auth: true - name: Install dependencies run: npm ci @@ -52,74 +51,42 @@ jobs: continue-on-error: true - name: Build + uses: ./.github/actions/run-npm-script id: build - continue-on-error: true - run: | - if ! [ -f package.json ] || ! jq -e '.scripts.build' package.json > /dev/null; then - echo "No build script found in package.json." - echo "build_status=notpresent" >> $GITHUB_OUTPUT - exit 0 - fi - if npm run build; then - echo "build_status=success" >> $GITHUB_OUTPUT - else - echo "build_status=failure" >> $GITHUB_OUTPUT - fi + with: + working-directory: '.' + script: build - name: Lint + uses: ./.github/actions/run-npm-script id: lint - continue-on-error: true - run: | - if ! [ -f package.json ] || ! jq -e '.scripts.["lint:check"]' package.json > /dev/null; then - echo "No lint script found in package.json." - echo "lint_status=notpresent" >> $GITHUB_OUTPUT - exit 0 - fi - if npm run lint:check; then - echo "lint_status=success" >> $GITHUB_OUTPUT - else - echo "lint_status=failure" >> $GITHUB_OUTPUT - fi + with: + working-directory: '.' + script: lint:check - name: Format + uses: ./.github/actions/run-npm-script id: format - continue-on-error: true - run: | - if ! [ -f package.json ] || ! jq -e '.scripts.["format:check"]' package.json > /dev/null; then - echo "No format script found in package.json." - echo "format_status=notpresent" >> $GITHUB_OUTPUT - exit 0 - fi - if npm run format:check; then - echo "format_status=success" >> $GITHUB_OUTPUT - else - echo "format_status=failure" >> $GITHUB_OUTPUT - fi + with: + working-directory: '.' + script: format:check - name: Test + uses: ./.github/actions/run-npm-script id: test - continue-on-error: true - run: | - if ! [ -f package.json ] || ! jq -e '.scripts.test' package.json > /dev/null; then - echo "No test script found in package.json." - echo "test_status=notpresent" >> $GITHUB_OUTPUT - exit 0 - fi - if npm test; then - echo "test_status=success" >> $GITHUB_OUTPUT - else - echo "test_status=failure" >> $GITHUB_OUTPUT - fi + 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.build_status || steps.build.outcome }} - LINT_STATUS: ${{ steps.lint.outputs.lint_status || steps.lint.outcome }} - FORMAT_STATUS: ${{ steps.format.outputs.format_status || steps.format.outcome }} - TEST_STATUS: ${{ steps.test.outputs.test_status || steps.test.outcome }} + 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 @@ -146,13 +113,13 @@ jobs: cat job-summary.md >> "$GITHUB_STEP_SUMMARY" - name: Set Run Outcome - if: ${{ steps.audit-npm.outputs.gate_passed == 'false' || steps.build.outputs.build_status == 'failure' || steps.lint.outputs.lint_status == 'failure' || steps.format.outputs.format_status == 'failure' || steps.test.outputs.test_status == 'failure' }} + 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.build_status == 'success' && 'passed' || 'failed' }} - LINT_STATUS: ${{ steps.lint.outputs.lint_status == 'success' && 'passed' || 'failed' }} - FORMAT_STATUS: ${{ steps.format.outputs.format_status == 'success' && 'passed' || 'failed' }} - TEST_STATUS: ${{ steps.test.outputs.test_status == 'success' && '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" From 1b527fe03488cb78a2c91c29beda5324079b2d17 Mon Sep 17 00:00:00 2001 From: ssvoss Date: Sat, 17 Jan 2026 18:32:02 -0500 Subject: [PATCH 10/11] add changelog and readme for run-npm-script composite action --- .github/actions/run-npm-script/CHANGELOG.md | 12 ++++ .github/actions/run-npm-script/README.md | 80 +++++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 .github/actions/run-npm-script/CHANGELOG.md create mode 100644 .github/actions/run-npm-script/README.md 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..268c5bc --- /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. From 5aba02568efe812bf73b8da3a4807276b7e8e13d Mon Sep 17 00:00:00 2001 From: ssvoss Date: Sat, 17 Jan 2026 18:42:36 -0500 Subject: [PATCH 11/11] normalize inputs and outputs to use _ --- .github/actions/run-npm-script/README.md | 2 +- .github/actions/run-npm-script/action.yml | 4 +-- .github/workflows/internal_on_push_ci.yml | 34 ++--------------------- .github/workflows/run_npm_ci_scripts.yml | 19 +++++++------ 4 files changed, 16 insertions(+), 43 deletions(-) diff --git a/.github/actions/run-npm-script/README.md b/.github/actions/run-npm-script/README.md index 268c5bc..a8af3bd 100644 --- a/.github/actions/run-npm-script/README.md +++ b/.github/actions/run-npm-script/README.md @@ -29,7 +29,7 @@ permissions: | 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 | +| `working_directory` | ❌ | Directory containing package.json, pass in '.' if you want the current directory | ## 📤 Outputs diff --git a/.github/actions/run-npm-script/action.yml b/.github/actions/run-npm-script/action.yml index 876ac38..2512c8b 100644 --- a/.github/actions/run-npm-script/action.yml +++ b/.github/actions/run-npm-script/action.yml @@ -6,7 +6,7 @@ inputs: description: 'The npm script to run (e.g., build, lint:check, test)' required: true default: '' - working-directory: + working_directory: description: 'Directory containing package.json (optional)' required: false default: '.' @@ -23,7 +23,7 @@ runs: id: run-script shell: bash env: - WORKING_DIRECTORY: ${{ inputs.working-directory }} + WORKING_DIRECTORY: ${{ inputs.working_directory }} SCRIPT: ${{ inputs.script }} run: | cd "$WORKING_DIRECTORY" diff --git a/.github/workflows/internal_on_push_ci.yml b/.github/workflows/internal_on_push_ci.yml index 6d9ad06..af76816 100644 --- a/.github/workflows/internal_on_push_ci.yml +++ b/.github/workflows/internal_on_push_ci.yml @@ -11,41 +11,13 @@ permissions: checks: write # needed if reporter is github-pr-check or github-check jobs: - internal-ci2: + internal-ci: name: Internal CI uses: ./.github/workflows/run_npm_ci_scripts.yml secrets: inherit with: - working-directory: '.' - commit-identifier: ${{ github.sha }} - - internal-ci: - name: Internal CI (old) - 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 + 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 index a8960a0..d595a97 100644 --- a/.github/workflows/run_npm_ci_scripts.yml +++ b/.github/workflows/run_npm_ci_scripts.yml @@ -3,11 +3,11 @@ name: NPM CI on: workflow_call: inputs: - working-directory: + working_directory: description: 'The working directory containing the package.json file with the npm ci scripts to run' required: true type: string - commit-identifier: + commit_identifier: description: 'SHA or tag to run ci against' required: true type: string @@ -23,16 +23,17 @@ jobs: 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 "WORKING_DIRECTORY=${{ inputs.working-directory }}" - cd ${{ inputs.working-directory }} + echo "Changing to working directory: $WORKING_DIRECTORY" + cd "$WORKING_DIRECTORY" - name: Checkout uses: actions/checkout@v4 with: - ref: ${{ inputs.commit-identifier }} + ref: ${{ inputs.commit_identifier }} - name: Set up Node uses: actions/setup-node@v3 @@ -54,28 +55,28 @@ jobs: uses: ./.github/actions/run-npm-script id: build with: - working-directory: '.' + working_directory: '.' script: build - name: Lint uses: ./.github/actions/run-npm-script id: lint with: - working-directory: '.' + working_directory: '.' script: lint:check - name: Format uses: ./.github/actions/run-npm-script id: format with: - working-directory: '.' + working_directory: '.' script: format:check - name: Test uses: ./.github/actions/run-npm-script id: test with: - working-directory: '.' + working_directory: '.' script: test - name: Job Summary