From 4b3c54ea882146d7504a2794c3c03f0d23662668 Mon Sep 17 00:00:00 2001 From: Edward Ezekiel Date: Thu, 17 Apr 2025 09:09:39 -0500 Subject: [PATCH 01/35] chore: convert fixtures to purls Previously, we used package.jsons and package-lock.jsons for fixtures in our e2e specs. Unfortunately, this triggers dependabot PRs. There is no clean way to exclude subdirectories from dependabot.[1] This commit refactors our fixtures to be simple purls.jsons. This way, we can still do integration testing with the api without triggering dependabot PRs for fixtures. [1]https://github.com/dependabot/dependabot-core/issues/7522 --- e2e/scan/eol.test.ts | 154 +++++-------------------------------------- 1 file changed, 15 insertions(+), 139 deletions(-) diff --git a/e2e/scan/eol.test.ts b/e2e/scan/eol.test.ts index 3bbb1d59..880279d0 100644 --- a/e2e/scan/eol.test.ts +++ b/e2e/scan/eol.test.ts @@ -11,7 +11,6 @@ describe('scan:eol e2e', () => { const __dirname = path.dirname(fileURLToPath(import.meta.url)); const fixturesDir = path.resolve(__dirname, '../fixtures'); const simplePurls = path.resolve(__dirname, '../fixtures/npm/simple.purls.json'); - const simpleSbom = path.join(fixturesDir, 'npm/nes.sbom.json'); const reportPath = path.resolve(fixturesDir, 'nes.eol.json'); const upToDatePurls = path.resolve(__dirname, '../fixtures/npm/up-to-date.purls.json'); const extraLargePurlsPath = path.resolve(__dirname, '../fixtures/npm/extra-large.purls.json'); @@ -40,8 +39,8 @@ describe('scan:eol e2e', () => { return output; } - it('scans existing SBOM for EOL components', async () => { - const cmd = `scan:eol --file ${simpleSbom}`; + it.skip('scans a directory for EOL components', async () => { + const cmd = `scan:eol --dir ${fixturesDir}`; const { stdout } = await run(cmd); // Match command output patterns @@ -92,6 +91,19 @@ describe('scan:eol e2e', () => { match(stdout, /✗ = End of Life \(EOL\)/, 'Should show legend for EOL status'); }); + it.skip('scans existing SBOM for EOL components', async () => { + // Create a simple SBOM file from the root of this project + const sbomCmd = 'scan:sbom --save'; + + const cmd = `scan:eol --file ${fixturesDir}/sbom.json`; + const { stdout } = await run(cmd); + + // Match command output patterns + match(stdout, /Here are the results of the scan:/, 'Should show results header'); + match(stdout, /pkg:npm\/bootstrap@3\.1\.1/, 'Should detect bootstrap package'); + match(stdout, /EOL Date: 2019-07-24/, 'Should show correct EOL date for bootstrap'); + }); + it('outputs JSON when using the --json flag', async () => { const cmd = `scan:eol --purls=${simplePurls} --json`; const { stdout } = await run(cmd); @@ -184,139 +196,3 @@ describe('scan:eol e2e', () => { match(stdout, /⮑ {2}EOL Date: \d{4}-\d{2}-\d{2}/, 'Should show EOL date with arrow'); }); }); - -/** - * Directory scan tests - * Please see CONTRIBUTING.md before adding new tests to this section. - */ -describe('scan:eol e2e directory', () => { - const __dirname = path.dirname(fileURLToPath(import.meta.url)); - const simpleDir = path.resolve(__dirname, '../fixtures/npm/simple'); - const upToDateDir = path.resolve(__dirname, '../fixtures/npm/up-to-date'); - const reportPath = path.join(simpleDir, 'nes.eol.json'); - - async function run(cmd: string) { - // Set up environment - process.env.GRAPHQL_HOST = 'https://api.dev.nes.herodevs.com'; - // process.env.GRAPHQL_HOST = 'http://localhost:3000'; - - // Ensure test directory exists and is clean - await mkdir(simpleDir, { recursive: true }); - - const output = await runCommand(cmd); - - // Log any errors for debugging - if (output.error) { - console.error('Command failed with error:', output.error); - console.error('Error details:', output.stderr); - } - - // Verify command executed successfully - strictEqual(output.error, undefined, 'Command should execute without errors'); - - return output; - } - - it('scans a directory or sbom for EOL components', async () => { - const cmd = `scan:eol --dir ${simpleDir}`; - const { stdout } = await run(cmd); - - // Match command output patterns - match(stdout, /Here are the results of the scan:/, 'Should show results header'); - match(stdout, /pkg:npm\/bootstrap@3\.1\.1/, 'Should detect bootstrap package'); - match(stdout, /End of Life \(EOL\)/, 'Should show EOL status'); - match(stdout, /EOL Date:/, 'Should show EOL date information'); - }); - - it('saves report when --save flag is used', async () => { - const cmd = `scan:eol --dir ${simpleDir} --save`; - await run(cmd); - - // Verify report was saved - const reportExists = existsSync(reportPath); - - strictEqual(reportExists, true, 'Report file should be created'); - - // Verify report content - const report = readFileSync(reportPath, 'utf-8'); - - // Verify report structure using match - match(report, /"components":\s*\[/, 'Report should contain components array'); - match(report, /"purl":\s*"pkg:npm\/bootstrap@3\.1\.1"/, 'Report should contain bootstrap package'); - match(report, /"isEol":\s*true/, 'Bootstrap should be marked as EOL'); - - unlinkSync(reportPath); - }); - it('scans existing SBOM for EOL components', async () => { - const cmd = `scan:eol --file ${simpleDir}/sbom.json`; - const { stdout } = await run(cmd); - - // Match command output patterns - match(stdout, /Here are the results of the scan:/, 'Should show results header'); - match(stdout, /pkg:npm\/bootstrap@3\.1\.1/, 'Should detect bootstrap package'); - match(stdout, /EOL Date: 2019-07-24/, 'Should show correct EOL date for bootstrap'); - }); - - it('outputs JSON when using the --json flag', async () => { - const cmd = `scan:eol --dir ${simpleDir} --json`; - const { stdout } = await run(cmd); - - // Match command output patterns - doesNotMatch(stdout, /Here are the results of the scan:/, 'Should not show results header'); - doesNotThrow(() => JSON.parse(stdout)); - }); - - it('displays results in table format when using the -t flag', async () => { - const cmd = `scan:eol --dir ${simpleDir} -t`; - const { stdout } = await run(cmd); - - // Match table header - match(stdout, /┌.*┬.*┬.*┬.*┬.*┐/, 'Should show table top border'); - match( - stdout, - /│ NAME\s*│ VERSION\s*│ EOL\s*│ DAYS EOL\s*│ TYPE\s*│/, // TODO: add vulns to monorepo api - 'Should show table headers', - ); - match(stdout, /├.*┼.*┼.*┼.*┼.*┤/, 'Should show table header separator'); - - // Match table content - match( - stdout, - /│ bootstrap\s*│ 3\.1\.1\s*│ 2019-07-24\s*│ \d+\s*│ npm\s*│/, - 'Should show bootstrap package in table', - ); - - // Match table footer - match(stdout, /└.*┴.*┴.*┴.*┴.*┘/, 'Should show table bottom border'); - }); - - describe('--all flag', () => { - it('excludes OK packages by default', async () => { - const cmd = `scan:eol --dir ${simpleDir}`; - const { stdout } = await run(cmd); - - // Match command output patterns - match(stdout, /Here are the results of the scan:/, 'Should show results header'); - doesNotMatch(stdout, /pkg:npm\/vue@3\.5\.13/, 'Should not show vue package'); - }); - - it('shows all packages when --all flag is used', async () => { - const cmd = `scan:eol --dir ${simpleDir} --all`; - const { stdout } = await run(cmd); - - // Match command output patterns - match(stdout, /Here are the results of the scan:/, 'Should show results header'); - match(stdout, /pkg:npm\/bootstrap@3\.1\.1/, 'Should detect bootstrap package'); - match(stdout, /pkg:npm\/vue@3\.5\.13/, 'Should show vue package'); - }); - - it('shows "No EOL" message by default if no components are found', async () => { - const cmd = `scan:eol --dir ${upToDateDir}`; - const { stdout } = await run(cmd); - - // Match command output patterns - doesNotMatch(stdout, /Here are the results of the scan:/, 'Should not show results header'); - match(stdout, /No End-of-Life or Supported components found in scan/, 'Should show "No EOL" message'); - }); - }); -}); From 3504d4691d17efabdc285670dad06209d04579ed Mon Sep 17 00:00:00 2001 From: Edward Ezekiel Date: Thu, 17 Apr 2025 09:24:30 -0500 Subject: [PATCH 02/35] chore: add back in e2e specs for sbom scanning --- e2e/scan/eol.test.ts | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/e2e/scan/eol.test.ts b/e2e/scan/eol.test.ts index 880279d0..6804862f 100644 --- a/e2e/scan/eol.test.ts +++ b/e2e/scan/eol.test.ts @@ -11,6 +11,7 @@ describe('scan:eol e2e', () => { const __dirname = path.dirname(fileURLToPath(import.meta.url)); const fixturesDir = path.resolve(__dirname, '../fixtures'); const simplePurls = path.resolve(__dirname, '../fixtures/npm/simple.purls.json'); + const simpleSbom = path.join(fixturesDir, 'npm/nes.sbom.json'); const reportPath = path.resolve(fixturesDir, 'nes.eol.json'); const upToDatePurls = path.resolve(__dirname, '../fixtures/npm/up-to-date.purls.json'); const extraLargePurlsPath = path.resolve(__dirname, '../fixtures/npm/extra-large.purls.json'); @@ -39,8 +40,8 @@ describe('scan:eol e2e', () => { return output; } - it.skip('scans a directory for EOL components', async () => { - const cmd = `scan:eol --dir ${fixturesDir}`; + it('scans existing SBOM for EOL components', async () => { + const cmd = `scan:eol --file ${simpleSbom}`; const { stdout } = await run(cmd); // Match command output patterns @@ -49,6 +50,15 @@ describe('scan:eol e2e', () => { match(stdout, /EOL Date: 2019-07-24/, 'Should show correct EOL date for bootstrap'); }); + it('scans a directory for EOL components', async () => { + const cmd = `scan:eol --dir ${process.cwd()}`; + const { stdout } = await run(cmd); + + // Match command output patterns for no EOL components + match(stdout, /No End-of-Life or Supported components found in scan/, 'Should show no EOL components message'); + match(stdout, /Use --all flag to view all components/, 'Should provide instructions to view all components'); + }); + it('saves report when --save flag is used', async () => { const cmd = `scan:eol --purls=${simplePurls} --dir=${fixturesDir} --save`; await run(cmd); @@ -91,19 +101,6 @@ describe('scan:eol e2e', () => { match(stdout, /✗ = End of Life \(EOL\)/, 'Should show legend for EOL status'); }); - it.skip('scans existing SBOM for EOL components', async () => { - // Create a simple SBOM file from the root of this project - const sbomCmd = 'scan:sbom --save'; - - const cmd = `scan:eol --file ${fixturesDir}/sbom.json`; - const { stdout } = await run(cmd); - - // Match command output patterns - match(stdout, /Here are the results of the scan:/, 'Should show results header'); - match(stdout, /pkg:npm\/bootstrap@3\.1\.1/, 'Should detect bootstrap package'); - match(stdout, /EOL Date: 2019-07-24/, 'Should show correct EOL date for bootstrap'); - }); - it('outputs JSON when using the --json flag', async () => { const cmd = `scan:eol --purls=${simplePurls} --json`; const { stdout } = await run(cmd); From 7f9f889b5afd4d870ff7e0a4a32480af7fe1e393 Mon Sep 17 00:00:00 2001 From: Edward Ezekiel Date: Thu, 17 Apr 2025 09:45:26 -0500 Subject: [PATCH 03/35] build: begin refactoring release to be tag-based --- .github/workflows/release.yml | 141 +++++++++++++++++++++++++++++----- 1 file changed, 123 insertions(+), 18 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index af062956..6c91375f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,35 +1,128 @@ -name: Release +name: Manual Release +run-name: Release ${{ github.ref_name }} (pushed by ${{ github.actor }})${{ inputs.dry-run == true && ' --dry-run' || '' }} + on: - push: - branches: - - main workflow_dispatch: - + inputs: + channel: + description: 'NPM tag to publish to' + type: choice + options: + - beta + - alpha + - latest + - next + default: beta + dry-run: + description: 'Dry run the release' + type: boolean + default: true permissions: contents: read jobs: - release-please: + check-version: runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version-file: '.nvmrc' + - name: Get version + id: version + run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT + - name: Determine Oclif channel + id: oclif-channel + run: | + VERSION=$(node -p "require('./package.json').version") + if [[ "$VERSION" == *"-beta"* ]]; then + echo "oclif_channel=beta" >> $GITHUB_OUTPUT + elif [[ "$VERSION" == *"-alpha"* ]]; then + echo "oclif_channel=alpha" >> $GITHUB_OUTPUT + else + echo "oclif_channel=stable" >> $GITHUB_OUTPUT + fi outputs: - release_created: ${{ steps.release.outputs.release_created }} + version: ${{ steps.version.outputs.version }} + oclif_channel: ${{ steps.oclif-channel.outputs.oclif_channel }} + + test: + runs-on: ubuntu-latest + needs: check-version + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version-file: '.nvmrc' + - run: npm ci + - run: npm run build + - run: npm test + - run: npm run test:e2e + + upload-assets: + runs-on: ubuntu-latest + needs: [check-version, test] permissions: contents: write - pull-requests: write + id-token: write steps: - - uses: googleapis/release-please-action@a02a34c4d625f9be7cb89156071d8567266a2445 # v4.2.0 - id: release + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version-file: '.nvmrc' + registry-url: 'https://registry.npmjs.org' + + # Build + - run: npm ci + - run: npm run build + + # Build platform-specific tarballs + - name: Install linux toolchain + run: | + sudo apt update + sudo apt install nsis p7zip-full p7zip-rar -y + + - name: Build all tarballs in parallel + run: | + npx oclif pack tarballs --targets=linux-x64,win32-x64,darwin-arm64 --no-xz --parallel + + # S3 Distribution + - name: Configure AWS credentials + if: ${{ !inputs.dry-run }} + uses: aws-actions/configure-aws-credentials@v4 with: - token: ${{ secrets.GITHUB_TOKEN }} - config-file: release-please-config.json - manifest-file: .release-please-manifest.json + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: us-east-1 - release: - needs: release-please - if: ${{ needs.release-please.outputs.release_created }} + - name: Upload and promote to S3 + if: ${{ !inputs.dry-run }} + run: | + # Enable oclif debug logging + export DEBUG=oclif:* + + # Upload tarballs + npx oclif upload tarballs \ + --targets=linux-x64,win32-x64,darwin-arm64 \ + --no-xz + + # Get shortened SHA (first 7 characters) + SHORT_SHA=$(echo ${{ github.sha }} | cut -c1-7) + echo "Using shortened SHA: $SHORT_SHA" + + # Promote to channel + npx oclif promote \ + --channel=${{ needs.check-version.outputs.oclif_channel }} \ + --version=${{ needs.check-version.outputs.version }} \ + --sha=$SHORT_SHA \ + --indexes \ + --targets=linux-x64,win32-x64,darwin-arm64 \ + --ignore-missing + + npm-publish: runs-on: ubuntu-latest + needs: [check-version, test, upload-assets] permissions: - contents: write id-token: write steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -37,9 +130,21 @@ jobs: with: node-version-file: '.nvmrc' registry-url: 'https://registry.npmjs.org' + + # Clean build for npm publishing - run: npm ci - run: npm run build + + # Dry run NPM publish + - name: Dry run NPM publish + if: ${{ inputs.dry-run }} + run: npm publish --tag ${{ inputs.channel }} --provenance --access public --dry-run + env: + NODE_AUTH_TOKEN: ${{ secrets.HD_CLI_NPM_TOKEN }} + + # NPM Release - name: Create NPM release - run: npm publish --provenance --access public + if: ${{ !inputs.dry-run }} + run: npm publish --tag ${{ inputs.channel }} --provenance --access public env: NODE_AUTH_TOKEN: ${{ secrets.HD_CLI_NPM_TOKEN }} From caa00553410e067f821949c69a6cf31e1ddf3d10 Mon Sep 17 00:00:00 2001 From: Edward Ezekiel Date: Thu, 17 Apr 2025 09:47:07 -0500 Subject: [PATCH 04/35] chore: rename manual-release to dry-run --- .github/workflows/dry-run.yml | 91 +++++++++++++--- .github/workflows/manual-release.yml | 155 --------------------------- 2 files changed, 77 insertions(+), 169 deletions(-) delete mode 100644 .github/workflows/manual-release.yml diff --git a/.github/workflows/dry-run.yml b/.github/workflows/dry-run.yml index 75de3e8d..d9a25971 100644 --- a/.github/workflows/dry-run.yml +++ b/.github/workflows/dry-run.yml @@ -1,18 +1,10 @@ -name: Test Release (Dry Run) -run-name: Test Release ${{ github.ref_name }} (pushed by ${{ github.actor }}) +name: Manual Release +run-name: Production Release ${{ github.ref_name }} (pushed by ${{ github.actor }}) on: - workflow_dispatch: - inputs: - channel: - description: 'NPM tag to publish to' - type: choice - options: - - beta - - alpha - - latest - - next - default: beta + push: + tags: + - v* permissions: contents: read @@ -26,8 +18,29 @@ jobs: node-version-file: '.nvmrc' - uses: ./.github/actions/verify-version id: verify-version + - name: Verify tag matches version + run: | + VERSION=${{ steps.verify-version.outputs.version }} + TAG_VERSION=${GITHUB_REF#refs/tags/v} + if [ "$VERSION" != "$TAG_VERSION" ]; then + echo "Error: Package version ($VERSION) does not match tag version ($TAG_VERSION)" + exit 1 + fi + - name: Determine Oclif channel + run: | + VERSION=${{ steps.verify-version.outputs.version }} + if [[ "$VERSION" == *"-beta"* ]]; then + echo "oclif_channel=beta" >> $GITHUB_OUTPUT + elif [[ "$VERSION" == *"-alpha"* ]]; then + echo "oclif_channel=alpha" >> $GITHUB_OUTPUT + elif [[ "$VERSION" == *"-next"* ]]; then + echo "oclif_channel=next" >> $GITHUB_OUTPUT + else + echo "oclif_channel=latest" >> $GITHUB_OUTPUT + fi outputs: version: ${{ steps.verify-version.outputs.version }} + oclif_channel: ${{ steps.determine-channel.outputs.oclif_channel }} test: runs-on: ubuntu-latest @@ -69,6 +82,50 @@ jobs: run: | npx oclif pack tarballs --targets=linux-x64,win32-x64,darwin-arm64 --no-xz --parallel + # Create GitHub Release (draft - will be published manually from GitHub UI or CLI) + - name: Create GitHub Release + run: | + gh release create v${{ needs.check-version.outputs.version }} \ + --title "Release v${{ needs.check-version.outputs.version }} ${{ needs.check-version.outputs.oclif_channel == 'latest' && 'Latest' || needs.check-version.outputs.oclif_channel }}" \ + --generate-notes \ + --draft \ + --prerelease=${{ needs.check-version.outputs.oclif_channel != 'latest' }} \ + --discussion-category "Announcements" \ + dist/*.tar.gz + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # S3 Distribution + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: us-east-1 + + - name: Upload and promote to S3 + run: | + # Enable oclif debug logging + export DEBUG=oclif:* + + # Upload tarballs + npx oclif upload tarballs \ + --targets=linux-x64,win32-x64,darwin-arm64 \ + --no-xz + + # Get shortened SHA (first 7 characters) + SHORT_SHA=$(echo ${{ github.sha }} | cut -c1-7) + echo "Using shortened SHA: $SHORT_SHA" + + # Promote to channel + npx oclif promote \ + --channel=${{ needs.check-version.outputs.oclif_channel }} \ + --version=${{ needs.check-version.outputs.version }} \ + --sha=$SHORT_SHA \ + --indexes \ + --targets=linux-x64,win32-x64,darwin-arm64 \ + --ignore-missing + npm-publish: runs-on: ubuntu-latest needs: [check-version, test, upload-assets] @@ -87,6 +144,12 @@ jobs: # Dry run NPM publish - name: Dry run NPM publish - run: npm publish --tag ${{ inputs.channel }} --provenance --access public --dry-run + run: npm publish --tag ${{ needs.check-version.outputs.oclif_channel }} --provenance --access public --dry-run + env: + NODE_AUTH_TOKEN: ${{ secrets.HD_CLI_NPM_TOKEN }} + + # NPM Release + - name: Create NPM release + run: npm publish --tag ${{ needs.check-version.outputs.oclif_channel }} --provenance --access public env: NODE_AUTH_TOKEN: ${{ secrets.HD_CLI_NPM_TOKEN }} diff --git a/.github/workflows/manual-release.yml b/.github/workflows/manual-release.yml deleted file mode 100644 index d9a25971..00000000 --- a/.github/workflows/manual-release.yml +++ /dev/null @@ -1,155 +0,0 @@ -name: Manual Release -run-name: Production Release ${{ github.ref_name }} (pushed by ${{ github.actor }}) - -on: - push: - tags: - - v* -permissions: - contents: read - -jobs: - check-version: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - with: - node-version-file: '.nvmrc' - - uses: ./.github/actions/verify-version - id: verify-version - - name: Verify tag matches version - run: | - VERSION=${{ steps.verify-version.outputs.version }} - TAG_VERSION=${GITHUB_REF#refs/tags/v} - if [ "$VERSION" != "$TAG_VERSION" ]; then - echo "Error: Package version ($VERSION) does not match tag version ($TAG_VERSION)" - exit 1 - fi - - name: Determine Oclif channel - run: | - VERSION=${{ steps.verify-version.outputs.version }} - if [[ "$VERSION" == *"-beta"* ]]; then - echo "oclif_channel=beta" >> $GITHUB_OUTPUT - elif [[ "$VERSION" == *"-alpha"* ]]; then - echo "oclif_channel=alpha" >> $GITHUB_OUTPUT - elif [[ "$VERSION" == *"-next"* ]]; then - echo "oclif_channel=next" >> $GITHUB_OUTPUT - else - echo "oclif_channel=latest" >> $GITHUB_OUTPUT - fi - outputs: - version: ${{ steps.verify-version.outputs.version }} - oclif_channel: ${{ steps.determine-channel.outputs.oclif_channel }} - - test: - runs-on: ubuntu-latest - needs: check-version - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - with: - node-version-file: '.nvmrc' - - run: npm ci - - run: npm run build - - run: npm test - - run: npm run test:e2e - - upload-assets: - runs-on: ubuntu-latest - needs: [check-version, test] - permissions: - contents: write - id-token: write - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - with: - node-version-file: '.nvmrc' - registry-url: 'https://registry.npmjs.org' - - # Build - - run: npm ci - - run: npm run build - - # Build platform-specific tarballs - - name: Install linux toolchain - run: | - sudo apt update - sudo apt install nsis p7zip-full p7zip-rar -y - - - name: Build all tarballs in parallel - run: | - npx oclif pack tarballs --targets=linux-x64,win32-x64,darwin-arm64 --no-xz --parallel - - # Create GitHub Release (draft - will be published manually from GitHub UI or CLI) - - name: Create GitHub Release - run: | - gh release create v${{ needs.check-version.outputs.version }} \ - --title "Release v${{ needs.check-version.outputs.version }} ${{ needs.check-version.outputs.oclif_channel == 'latest' && 'Latest' || needs.check-version.outputs.oclif_channel }}" \ - --generate-notes \ - --draft \ - --prerelease=${{ needs.check-version.outputs.oclif_channel != 'latest' }} \ - --discussion-category "Announcements" \ - dist/*.tar.gz - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - # S3 Distribution - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v4 - with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws-region: us-east-1 - - - name: Upload and promote to S3 - run: | - # Enable oclif debug logging - export DEBUG=oclif:* - - # Upload tarballs - npx oclif upload tarballs \ - --targets=linux-x64,win32-x64,darwin-arm64 \ - --no-xz - - # Get shortened SHA (first 7 characters) - SHORT_SHA=$(echo ${{ github.sha }} | cut -c1-7) - echo "Using shortened SHA: $SHORT_SHA" - - # Promote to channel - npx oclif promote \ - --channel=${{ needs.check-version.outputs.oclif_channel }} \ - --version=${{ needs.check-version.outputs.version }} \ - --sha=$SHORT_SHA \ - --indexes \ - --targets=linux-x64,win32-x64,darwin-arm64 \ - --ignore-missing - - npm-publish: - runs-on: ubuntu-latest - needs: [check-version, test, upload-assets] - permissions: - id-token: write - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - with: - node-version-file: '.nvmrc' - registry-url: 'https://registry.npmjs.org' - - # Clean build for npm publishing - - run: npm ci - - run: npm run build - - # Dry run NPM publish - - name: Dry run NPM publish - run: npm publish --tag ${{ needs.check-version.outputs.oclif_channel }} --provenance --access public --dry-run - env: - NODE_AUTH_TOKEN: ${{ secrets.HD_CLI_NPM_TOKEN }} - - # NPM Release - - name: Create NPM release - run: npm publish --tag ${{ needs.check-version.outputs.oclif_channel }} --provenance --access public - env: - NODE_AUTH_TOKEN: ${{ secrets.HD_CLI_NPM_TOKEN }} From 7e4d787f6f426e885dad20f0f6c717fa728a570a Mon Sep 17 00:00:00 2001 From: Edward Ezekiel Date: Thu, 17 Apr 2025 09:55:00 -0500 Subject: [PATCH 05/35] build: simplify dry run workflow --- .github/workflows/dry-run.yml | 98 +++++++---------------------------- 1 file changed, 18 insertions(+), 80 deletions(-) diff --git a/.github/workflows/dry-run.yml b/.github/workflows/dry-run.yml index d9a25971..0300fc1f 100644 --- a/.github/workflows/dry-run.yml +++ b/.github/workflows/dry-run.yml @@ -1,10 +1,18 @@ -name: Manual Release -run-name: Production Release ${{ github.ref_name }} (pushed by ${{ github.actor }}) +name: Dry Run Release +run-name: Dry Run Release ${{ github.ref_name }} (pushed by ${{ github.actor }}) on: - push: - tags: - - v* + workflow_dispatch: + inputs: + channel: + description: 'NPM tag to publish to' + type: choice + options: + - beta + - alpha + - latest + - next + default: beta permissions: contents: read @@ -16,31 +24,11 @@ jobs: - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: '.nvmrc' - - uses: ./.github/actions/verify-version - id: verify-version - - name: Verify tag matches version - run: | - VERSION=${{ steps.verify-version.outputs.version }} - TAG_VERSION=${GITHUB_REF#refs/tags/v} - if [ "$VERSION" != "$TAG_VERSION" ]; then - echo "Error: Package version ($VERSION) does not match tag version ($TAG_VERSION)" - exit 1 - fi - - name: Determine Oclif channel - run: | - VERSION=${{ steps.verify-version.outputs.version }} - if [[ "$VERSION" == *"-beta"* ]]; then - echo "oclif_channel=beta" >> $GITHUB_OUTPUT - elif [[ "$VERSION" == *"-alpha"* ]]; then - echo "oclif_channel=alpha" >> $GITHUB_OUTPUT - elif [[ "$VERSION" == *"-next"* ]]; then - echo "oclif_channel=next" >> $GITHUB_OUTPUT - else - echo "oclif_channel=latest" >> $GITHUB_OUTPUT - fi + - name: Get version + id: version + run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT outputs: - version: ${{ steps.verify-version.outputs.version }} - oclif_channel: ${{ steps.determine-channel.outputs.oclif_channel }} + version: ${{ steps.version.outputs.version }} test: runs-on: ubuntu-latest @@ -82,50 +70,6 @@ jobs: run: | npx oclif pack tarballs --targets=linux-x64,win32-x64,darwin-arm64 --no-xz --parallel - # Create GitHub Release (draft - will be published manually from GitHub UI or CLI) - - name: Create GitHub Release - run: | - gh release create v${{ needs.check-version.outputs.version }} \ - --title "Release v${{ needs.check-version.outputs.version }} ${{ needs.check-version.outputs.oclif_channel == 'latest' && 'Latest' || needs.check-version.outputs.oclif_channel }}" \ - --generate-notes \ - --draft \ - --prerelease=${{ needs.check-version.outputs.oclif_channel != 'latest' }} \ - --discussion-category "Announcements" \ - dist/*.tar.gz - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - # S3 Distribution - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v4 - with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws-region: us-east-1 - - - name: Upload and promote to S3 - run: | - # Enable oclif debug logging - export DEBUG=oclif:* - - # Upload tarballs - npx oclif upload tarballs \ - --targets=linux-x64,win32-x64,darwin-arm64 \ - --no-xz - - # Get shortened SHA (first 7 characters) - SHORT_SHA=$(echo ${{ github.sha }} | cut -c1-7) - echo "Using shortened SHA: $SHORT_SHA" - - # Promote to channel - npx oclif promote \ - --channel=${{ needs.check-version.outputs.oclif_channel }} \ - --version=${{ needs.check-version.outputs.version }} \ - --sha=$SHORT_SHA \ - --indexes \ - --targets=linux-x64,win32-x64,darwin-arm64 \ - --ignore-missing - npm-publish: runs-on: ubuntu-latest needs: [check-version, test, upload-assets] @@ -144,12 +88,6 @@ jobs: # Dry run NPM publish - name: Dry run NPM publish - run: npm publish --tag ${{ needs.check-version.outputs.oclif_channel }} --provenance --access public --dry-run - env: - NODE_AUTH_TOKEN: ${{ secrets.HD_CLI_NPM_TOKEN }} - - # NPM Release - - name: Create NPM release - run: npm publish --tag ${{ needs.check-version.outputs.oclif_channel }} --provenance --access public + run: npm publish --tag ${{ inputs.channel }} --provenance --access public --dry-run env: NODE_AUTH_TOKEN: ${{ secrets.HD_CLI_NPM_TOKEN }} From c632176b0714210706e5b53f82d9125a127bb1a2 Mon Sep 17 00:00:00 2001 From: Edward Ezekiel Date: Thu, 17 Apr 2025 09:55:26 -0500 Subject: [PATCH 06/35] build: start refactoring release workflow Use tag push as a trigger for the release instead of release-please. --- .github/workflows/release.yml | 38 +++++++++-------------------------- 1 file changed, 9 insertions(+), 29 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6c91375f..8aa1d7ad 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,22 +1,10 @@ -name: Manual Release -run-name: Release ${{ github.ref_name }} (pushed by ${{ github.actor }})${{ inputs.dry-run == true && ' --dry-run' || '' }} +name: Release +run-name: Release ${{ github.ref_name }} (pushed by ${{ github.actor }}) on: - workflow_dispatch: - inputs: - channel: - description: 'NPM tag to publish to' - type: choice - options: - - beta - - alpha - - latest - - next - default: beta - dry-run: - description: 'Dry run the release' - type: boolean - default: true + push: + tags: + - v* permissions: contents: read @@ -39,8 +27,10 @@ jobs: echo "oclif_channel=beta" >> $GITHUB_OUTPUT elif [[ "$VERSION" == *"-alpha"* ]]; then echo "oclif_channel=alpha" >> $GITHUB_OUTPUT + elif [[ "$VERSION" == *"-next"* ]]; then + echo "oclif_channel=next" >> $GITHUB_OUTPUT else - echo "oclif_channel=stable" >> $GITHUB_OUTPUT + echo "oclif_channel=latest" >> $GITHUB_OUTPUT fi outputs: version: ${{ steps.version.outputs.version }} @@ -88,7 +78,6 @@ jobs: # S3 Distribution - name: Configure AWS credentials - if: ${{ !inputs.dry-run }} uses: aws-actions/configure-aws-credentials@v4 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} @@ -96,7 +85,6 @@ jobs: aws-region: us-east-1 - name: Upload and promote to S3 - if: ${{ !inputs.dry-run }} run: | # Enable oclif debug logging export DEBUG=oclif:* @@ -135,16 +123,8 @@ jobs: - run: npm ci - run: npm run build - # Dry run NPM publish - - name: Dry run NPM publish - if: ${{ inputs.dry-run }} - run: npm publish --tag ${{ inputs.channel }} --provenance --access public --dry-run - env: - NODE_AUTH_TOKEN: ${{ secrets.HD_CLI_NPM_TOKEN }} - # NPM Release - name: Create NPM release - if: ${{ !inputs.dry-run }} - run: npm publish --tag ${{ inputs.channel }} --provenance --access public + run: npm publish --tag ${{ needs.check-version.outputs.oclif_channel }} --provenance --access public env: NODE_AUTH_TOKEN: ${{ secrets.HD_CLI_NPM_TOKEN }} From 48197dd9ed4abc2ede7e50978bfb00c254c83dec Mon Sep 17 00:00:00 2001 From: Edward Ezekiel Date: Thu, 17 Apr 2025 10:19:14 -0500 Subject: [PATCH 07/35] build: create verify-version action --- .github/workflows/dry-run.yml | 11 +++++------ .github/workflows/release.yml | 24 +++++++++++++++--------- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/.github/workflows/dry-run.yml b/.github/workflows/dry-run.yml index 0300fc1f..75de3e8d 100644 --- a/.github/workflows/dry-run.yml +++ b/.github/workflows/dry-run.yml @@ -1,5 +1,5 @@ -name: Dry Run Release -run-name: Dry Run Release ${{ github.ref_name }} (pushed by ${{ github.actor }}) +name: Test Release (Dry Run) +run-name: Test Release ${{ github.ref_name }} (pushed by ${{ github.actor }}) on: workflow_dispatch: @@ -24,11 +24,10 @@ jobs: - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: '.nvmrc' - - name: Get version - id: version - run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT + - uses: ./.github/actions/verify-version + id: verify-version outputs: - version: ${{ steps.version.outputs.version }} + version: ${{ steps.verify-version.outputs.version }} test: runs-on: ubuntu-latest diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8aa1d7ad..71806f1a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,5 +1,5 @@ -name: Release -run-name: Release ${{ github.ref_name }} (pushed by ${{ github.actor }}) +name: Production Release +run-name: Production Release ${{ github.ref_name }} (pushed by ${{ github.actor }}) on: push: @@ -16,13 +16,19 @@ jobs: - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: '.nvmrc' - - name: Get version - id: version - run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT + - uses: ./.github/actions/verify-version + id: verify-version + - name: Verify tag matches version + run: | + VERSION=${{ steps.verify-version.outputs.version }} + TAG_VERSION=${GITHUB_REF#refs/tags/v} + if [ "$VERSION" != "$TAG_VERSION" ]; then + echo "Error: Package version ($VERSION) does not match tag version ($TAG_VERSION)" + exit 1 + fi - name: Determine Oclif channel - id: oclif-channel run: | - VERSION=$(node -p "require('./package.json').version") + VERSION=${{ steps.verify-version.outputs.version }} if [[ "$VERSION" == *"-beta"* ]]; then echo "oclif_channel=beta" >> $GITHUB_OUTPUT elif [[ "$VERSION" == *"-alpha"* ]]; then @@ -33,8 +39,8 @@ jobs: echo "oclif_channel=latest" >> $GITHUB_OUTPUT fi outputs: - version: ${{ steps.version.outputs.version }} - oclif_channel: ${{ steps.oclif-channel.outputs.oclif_channel }} + version: ${{ steps.verify-version.outputs.version }} + oclif_channel: ${{ steps.determine-channel.outputs.oclif_channel }} test: runs-on: ubuntu-latest From 030d0784b1c31ead95e8fb700a0444e020085c1b Mon Sep 17 00:00:00 2001 From: Edward Ezekiel Date: Thu, 17 Apr 2025 10:41:06 -0500 Subject: [PATCH 08/35] build: add github release to release.yml --- .github/workflows/release.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 71806f1a..f8aa6d79 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -118,6 +118,7 @@ jobs: needs: [check-version, test, upload-assets] permissions: id-token: write + contents: write steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 @@ -134,3 +135,22 @@ jobs: run: npm publish --tag ${{ needs.check-version.outputs.oclif_channel }} --provenance --access public env: NODE_AUTH_TOKEN: ${{ secrets.HD_CLI_NPM_TOKEN }} + + # Create GitHub Release (draft - will be published manually from GitHub UI or CLI) + - name: Create GitHub Release + uses: softprops/action-gh-release@v1 + with: + tag_name: v${{ needs.check-version.outputs.version }} + name: Release v${{ needs.check-version.outputs.version }} ${{ needs.check-version.outputs.oclif_channel == 'latest' && 'Latest' || needs.check-version.outputs.oclif_channel }} + body: | + This release includes the following platform-specific tarballs: + - macOS (ARM64) + - Windows (x64) + - Linux (x64) + files: | + dist/*.tar.gz + draft: true + prerelease: ${{ needs.check-version.outputs.oclif_channel != 'latest' }} + discussion_category_name: Announcements + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From a7f792c6f9c5cbc9a17820753b05f3a2559d9ae8 Mon Sep 17 00:00:00 2001 From: Edward Ezekiel Date: Thu, 17 Apr 2025 10:42:19 -0500 Subject: [PATCH 09/35] chore: delete release-please manifest and config --- .release-please-manifest.json | 3 --- release-please-config.json | 23 ----------------------- 2 files changed, 26 deletions(-) delete mode 100644 .release-please-manifest.json delete mode 100644 release-please-config.json diff --git a/.release-please-manifest.json b/.release-please-manifest.json deleted file mode 100644 index 92475378..00000000 --- a/.release-please-manifest.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - ".": "1.1.0-beta.1" -} diff --git a/release-please-config.json b/release-please-config.json deleted file mode 100644 index aaa11a31..00000000 --- a/release-please-config.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", - "release-type": "node", - "pull-request-title-pattern": "chore: release v${version}", - "bootstrap-sha": "32c9d5ed00c0e6022e66d5bf31fbb7ed20841202", - "packages": { - ".": { - "include-component-in-tag": false, - "changelog-path": "CHANGELOG.md", - "changelog-sections": [ - { "type": "feat", "section": "Features", "hidden": false }, - { "type": "fix", "section": "Bug Fixes", "hidden": false }, - { "type": "perf", "section": "Core", "hidden": false }, - { "type": "docs", "section": "Core", "hidden": false }, - { "type": "refactor", "section": "Core", "hidden": false }, - { "type": "test", "section": "Core", "hidden": false }, - { "type": "chore", "section": "Miscellaneous", "hidden": false }, - { "type": "build", "section": "Miscellaneous", "hidden": false }, - { "type": "ci", "section": "Miscellaneous", "hidden": false } - ] - } - } -} From 088b5798a0022e2abca4bef44245ba720af2ac78 Mon Sep 17 00:00:00 2001 From: Edward Ezekiel Date: Thu, 17 Apr 2025 11:11:10 -0500 Subject: [PATCH 10/35] build: add commit-and-tag-version --- package.json | 3 ++- scripts/release.sh | 59 ++++++++++++++++++---------------------------- 2 files changed, 25 insertions(+), 37 deletions(-) diff --git a/package.json b/package.json index 8ba34e8d..f1514812 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,8 @@ "prepack": "oclif manifest && oclif readme", "pretest": "npm run lint && npm run typecheck", "readme": "npm run ci:fix && npm run build && npm exec oclif readme", - "release": "./scripts/release.sh", + "release": "chmod +x scripts/release.sh && ./scripts/release.sh", + "release:dry-run": "npm run release", "pre:release:publish": "npm run prepack && git add README.md", "release:publish:beta": "npm run release -- --publish", "release:publish:latest": "npm run release -- --latest --publish", diff --git a/scripts/release.sh b/scripts/release.sh index e065ff5d..060f291f 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -1,17 +1,12 @@ #!/bin/bash -# Stricter shell controls -set -eu +# Exit on error +set -e # Default values RELEASE_TYPE="beta" DRY_RUN=true -echo "🔍 Debug: Initial values" -echo " RELEASE_TYPE=$RELEASE_TYPE" -echo " DRY_RUN=$DRY_RUN" -echo "" - # Parse arguments while [[ $# -gt 0 ]]; do case $1 in @@ -54,11 +49,6 @@ while [[ $# -gt 0 ]]; do esac done -echo "🔍 Debug: After parsing arguments" -echo " RELEASE_TYPE=$RELEASE_TYPE" -echo " DRY_RUN=$DRY_RUN" -echo "" - # Check if there are uncommitted changes if [ -n "$(git status --porcelain)" ]; then echo "Error: You have uncommitted changes. Please commit or stash them before releasing." @@ -68,12 +58,9 @@ fi # Build the commit-and-tag-version command CMD="npx commit-and-tag-version" -# Add release type -if [ "$RELEASE_TYPE" = "latest" ]; then - # No flag needed for latest - true -else - CMD="$CMD --prerelease $RELEASE_TYPE" +# Add release type if not latest +if [ "$RELEASE_TYPE" != "latest" ]; then + CMD="$CMD --$RELEASE_TYPE" fi # Add dry run if specified @@ -81,33 +68,33 @@ if [ "$DRY_RUN" = true ]; then CMD="$CMD --dry-run" fi -echo "🔍 Debug: Final command" -echo " $CMD" -echo "" - echo "Creating $RELEASE_TYPE release..." echo "Running: $CMD" # Execute the command $CMD -# If not dry run, show next steps +# If not dry run, ask for confirmation before publishing if [ "$DRY_RUN" = false ]; then echo "" - echo "✅ Release changes have been committed locally" + echo "⚠️ WARNING: This will perform the following actions:" + echo " 1. Update version in package.json" + echo " 2. Update CHANGELOG.md with latest changes" + echo " 3. Create a commit with these changes" + echo " 4. Create a git tag for the new version" + echo " 5. Push both the commit and tag to the remote repository" echo "" - echo "Next steps:" - echo "1. Review the changes:" - echo " git show" - echo "2. Push the tag to trigger the release workflow:" - echo " git push --follow-tags" + echo "This will trigger the GitHub Actions release workflow." echo "" - echo "The GitHub Actions release workflow will run once the tag is pushed." -else + read -p "Are you sure you want to proceed? (y/N) " -n 1 -r echo "" - echo "✅ DRY RUN COMPLETED" - echo "This was a dry run - no changes were made to the repository." - echo "To actually publish this release, run:" - echo " npm run release:publish:beta # for a beta release" - echo " npm run release:publish:latest # for a latest release" + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + echo "Release cancelled." + exit 1 + fi + + echo "Pushing tag..." + git push --follow-tags fi + +echo "Release process completed!" From 25ec4d86cb5aeb232d82a0099799b2f0099f4adb Mon Sep 17 00:00:00 2001 From: Edward Ezekiel Date: Thu, 17 Apr 2025 11:12:58 -0500 Subject: [PATCH 11/35] chore: add helpful logging to release.sh --- scripts/release.sh | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/scripts/release.sh b/scripts/release.sh index 060f291f..be00f5eb 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -95,6 +95,12 @@ if [ "$DRY_RUN" = false ]; then echo "Pushing tag..." git push --follow-tags + echo "Release published successfully!" +else + echo "" + echo "✅ DRY RUN COMPLETED" + echo "This was a dry run - no changes were made to the repository." + echo "To actually publish this release, run:" + echo " npm run release:publish:beta # for a beta release" + echo " npm run release:publish:latest # for a latest release" fi - -echo "Release process completed!" From 5d9e9a108f5e59c043c81de70781e1a5d30aff59 Mon Sep 17 00:00:00 2001 From: Edward Ezekiel Date: Thu, 17 Apr 2025 11:16:10 -0500 Subject: [PATCH 12/35] chore: don't push tags even when not dry run This gives the user a chance to review the changes before pushing the tag. --- scripts/release.sh | 26 ++++++++------------------ 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/scripts/release.sh b/scripts/release.sh index be00f5eb..dfda1320 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -74,28 +74,18 @@ echo "Running: $CMD" # Execute the command $CMD -# If not dry run, ask for confirmation before publishing +# If not dry run, show next steps if [ "$DRY_RUN" = false ]; then echo "" - echo "⚠️ WARNING: This will perform the following actions:" - echo " 1. Update version in package.json" - echo " 2. Update CHANGELOG.md with latest changes" - echo " 3. Create a commit with these changes" - echo " 4. Create a git tag for the new version" - echo " 5. Push both the commit and tag to the remote repository" + echo "✅ Release changes have been committed locally" echo "" - echo "This will trigger the GitHub Actions release workflow." + echo "Next steps:" + echo "1. Review the changes:" + echo " git show" + echo "2. Push the tag to trigger the release workflow:" + echo " git push --follow-tags" echo "" - read -p "Are you sure you want to proceed? (y/N) " -n 1 -r - echo "" - if [[ ! $REPLY =~ ^[Yy]$ ]]; then - echo "Release cancelled." - exit 1 - fi - - echo "Pushing tag..." - git push --follow-tags - echo "Release published successfully!" + echo "The GitHub Actions release workflow will run once the tag is pushed." else echo "" echo "✅ DRY RUN COMPLETED" From b9ab22ff03ce25fdfceed69bbb086ff0ef3ca363 Mon Sep 17 00:00:00 2001 From: Edward Ezekiel Date: Thu, 17 Apr 2025 11:21:17 -0500 Subject: [PATCH 13/35] chore: add debug logs to release.sh --- scripts/release.sh | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/scripts/release.sh b/scripts/release.sh index dfda1320..8d57e06e 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -1,12 +1,17 @@ #!/bin/bash -# Exit on error -set -e +# Stricter shell controls +set -eux # Default values RELEASE_TYPE="beta" DRY_RUN=true +echo "🔍 Debug: Initial values" +echo " RELEASE_TYPE=$RELEASE_TYPE" +echo " DRY_RUN=$DRY_RUN" +echo "" + # Parse arguments while [[ $# -gt 0 ]]; do case $1 in @@ -49,6 +54,11 @@ while [[ $# -gt 0 ]]; do esac done +echo "🔍 Debug: After parsing arguments" +echo " RELEASE_TYPE=$RELEASE_TYPE" +echo " DRY_RUN=$DRY_RUN" +echo "" + # Check if there are uncommitted changes if [ -n "$(git status --porcelain)" ]; then echo "Error: You have uncommitted changes. Please commit or stash them before releasing." @@ -68,6 +78,10 @@ if [ "$DRY_RUN" = true ]; then CMD="$CMD --dry-run" fi +echo "🔍 Debug: Final command" +echo " $CMD" +echo "" + echo "Creating $RELEASE_TYPE release..." echo "Running: $CMD" From f777aef642771f5156045e8b7fa69e82c9b0ee79 Mon Sep 17 00:00:00 2001 From: Edward Ezekiel Date: Thu, 17 Apr 2025 11:23:16 -0500 Subject: [PATCH 14/35] chore: ensure pre-release flag is set in script --- scripts/release.sh | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/scripts/release.sh b/scripts/release.sh index 8d57e06e..99ea3734 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -68,9 +68,12 @@ fi # Build the commit-and-tag-version command CMD="npx commit-and-tag-version" -# Add release type if not latest -if [ "$RELEASE_TYPE" != "latest" ]; then - CMD="$CMD --$RELEASE_TYPE" +# Add release type +if [ "$RELEASE_TYPE" = "latest" ]; then + # No flag needed for latest + true +else + CMD="$CMD --prerelease $RELEASE_TYPE" fi # Add dry run if specified From e835c816fd3088ee48792a4161aa27c548877033 Mon Sep 17 00:00:00 2001 From: Edward Ezekiel Date: Thu, 17 Apr 2025 11:25:19 -0500 Subject: [PATCH 15/35] chore: remove x setting from release.sh --- scripts/release.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/release.sh b/scripts/release.sh index 99ea3734..e065ff5d 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -1,7 +1,7 @@ #!/bin/bash # Stricter shell controls -set -eux +set -eu # Default values RELEASE_TYPE="beta" From ab59b1d8308ff18934da8253b0012a0bdc27a0b4 Mon Sep 17 00:00:00 2001 From: Edward Ezekiel Date: Thu, 17 Apr 2025 11:40:35 -0500 Subject: [PATCH 16/35] chore: create action for parsing changelog --- .github/actions/extract-changelog/action.yml | 41 ++++++++++++++++++++ .github/workflows/release.yml | 10 +++++ 2 files changed, 51 insertions(+) create mode 100644 .github/actions/extract-changelog/action.yml diff --git a/.github/actions/extract-changelog/action.yml b/.github/actions/extract-changelog/action.yml new file mode 100644 index 00000000..967d9eb7 --- /dev/null +++ b/.github/actions/extract-changelog/action.yml @@ -0,0 +1,41 @@ +name: 'Extract Changelog' +description: 'Extracts the changelog section for a specific version from CHANGELOG.md' + +inputs: + version: + description: 'The version to extract changelog for' + required: true + default: '' + +outputs: + content: + description: 'The extracted changelog content' + value: ${{ steps.extract.outputs.content }} + +runs: + using: "composite" + steps: + - name: Extract changelog content + id: extract + shell: bash + run: | + # Get the changelog content for this version using awk + CHANGELOG=$(git show HEAD:CHANGELOG.md | awk ' + BEGIN { found=0 } + /^## \[/ { + if (found) exit + if ($0 ~ /^## \['"${{ inputs.version }}"'\]/) found=1 + next + } + found { print } + ') + + # Remove any trailing empty lines + CHANGELOG=$(echo "$CHANGELOG" | sed -e :a -e '/^\n*$/{$d;N;};/\n$/ba') + + # Escape newlines for GitHub Actions + CHANGELOG="${CHANGELOG//'%'/'%25'}" + CHANGELOG="${CHANGELOG//$'\n'/'%0A'}" + CHANGELOG="${CHANGELOG//$'\r'/'%0D'}" + + echo "content=$CHANGELOG" >> $GITHUB_OUTPUT \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f8aa6d79..e5b5ae76 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -136,6 +136,13 @@ jobs: env: NODE_AUTH_TOKEN: ${{ secrets.HD_CLI_NPM_TOKEN }} + # Get changelog content + - name: Extract changelog content + id: extract-changelog + uses: ./.github/actions/extract-changelog + with: + version: ${{ needs.check-version.outputs.version }} + # Create GitHub Release (draft - will be published manually from GitHub UI or CLI) - name: Create GitHub Release uses: softprops/action-gh-release@v1 @@ -147,6 +154,9 @@ jobs: - macOS (ARM64) - Windows (x64) - Linux (x64) + + ## Changes + ${{ steps.extract-changelog.outputs.content }} files: | dist/*.tar.gz draft: true From 265f2b72d811d72facfd18caaa4e9e353bbc6a8a Mon Sep 17 00:00:00 2001 From: Edward Ezekiel Date: Thu, 17 Apr 2025 11:44:32 -0500 Subject: [PATCH 17/35] chore: add missing eof --- .github/actions/extract-changelog/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/extract-changelog/action.yml b/.github/actions/extract-changelog/action.yml index 967d9eb7..3a0b2e9f 100644 --- a/.github/actions/extract-changelog/action.yml +++ b/.github/actions/extract-changelog/action.yml @@ -38,4 +38,4 @@ runs: CHANGELOG="${CHANGELOG//$'\n'/'%0A'}" CHANGELOG="${CHANGELOG//$'\r'/'%0D'}" - echo "content=$CHANGELOG" >> $GITHUB_OUTPUT \ No newline at end of file + echo "content=$CHANGELOG" >> $GITHUB_OUTPUT From 43258a253ca2646adde18343259fb2a1ef05ebd0 Mon Sep 17 00:00:00 2001 From: Edward Ezekiel Date: Thu, 17 Apr 2025 12:22:25 -0500 Subject: [PATCH 18/35] chore: delete action for extracting changelog --- .github/actions/extract-changelog/action.yml | 41 -------------------- .github/workflows/release.yml | 10 ----- 2 files changed, 51 deletions(-) delete mode 100644 .github/actions/extract-changelog/action.yml diff --git a/.github/actions/extract-changelog/action.yml b/.github/actions/extract-changelog/action.yml deleted file mode 100644 index 3a0b2e9f..00000000 --- a/.github/actions/extract-changelog/action.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: 'Extract Changelog' -description: 'Extracts the changelog section for a specific version from CHANGELOG.md' - -inputs: - version: - description: 'The version to extract changelog for' - required: true - default: '' - -outputs: - content: - description: 'The extracted changelog content' - value: ${{ steps.extract.outputs.content }} - -runs: - using: "composite" - steps: - - name: Extract changelog content - id: extract - shell: bash - run: | - # Get the changelog content for this version using awk - CHANGELOG=$(git show HEAD:CHANGELOG.md | awk ' - BEGIN { found=0 } - /^## \[/ { - if (found) exit - if ($0 ~ /^## \['"${{ inputs.version }}"'\]/) found=1 - next - } - found { print } - ') - - # Remove any trailing empty lines - CHANGELOG=$(echo "$CHANGELOG" | sed -e :a -e '/^\n*$/{$d;N;};/\n$/ba') - - # Escape newlines for GitHub Actions - CHANGELOG="${CHANGELOG//'%'/'%25'}" - CHANGELOG="${CHANGELOG//$'\n'/'%0A'}" - CHANGELOG="${CHANGELOG//$'\r'/'%0D'}" - - echo "content=$CHANGELOG" >> $GITHUB_OUTPUT diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e5b5ae76..f8aa6d79 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -136,13 +136,6 @@ jobs: env: NODE_AUTH_TOKEN: ${{ secrets.HD_CLI_NPM_TOKEN }} - # Get changelog content - - name: Extract changelog content - id: extract-changelog - uses: ./.github/actions/extract-changelog - with: - version: ${{ needs.check-version.outputs.version }} - # Create GitHub Release (draft - will be published manually from GitHub UI or CLI) - name: Create GitHub Release uses: softprops/action-gh-release@v1 @@ -154,9 +147,6 @@ jobs: - macOS (ARM64) - Windows (x64) - Linux (x64) - - ## Changes - ${{ steps.extract-changelog.outputs.content }} files: | dist/*.tar.gz draft: true From 6086611a1e3afc16bc351f9a2eca1fc20cfa4157 Mon Sep 17 00:00:00 2001 From: Edward Ezekiel Date: Thu, 17 Apr 2025 13:05:08 -0500 Subject: [PATCH 19/35] build: use gh cli to generate release --- .github/workflows/release.yml | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f8aa6d79..baa3a8f1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -138,19 +138,13 @@ jobs: # Create GitHub Release (draft - will be published manually from GitHub UI or CLI) - name: Create GitHub Release - uses: softprops/action-gh-release@v1 - with: - tag_name: v${{ needs.check-version.outputs.version }} - name: Release v${{ needs.check-version.outputs.version }} ${{ needs.check-version.outputs.oclif_channel == 'latest' && 'Latest' || needs.check-version.outputs.oclif_channel }} - body: | - This release includes the following platform-specific tarballs: - - macOS (ARM64) - - Windows (x64) - - Linux (x64) - files: | + run: | + gh release create v${{ needs.check-version.outputs.version }} \ + --title "Release v${{ needs.check-version.outputs.version }} ${{ needs.check-version.outputs.oclif_channel == 'latest' && 'Latest' || needs.check-version.outputs.oclif_channel }}" \ + --notes-from-tag \ + --draft \ + --prerelease=${{ needs.check-version.outputs.oclif_channel != 'latest' }} \ + --discussion-category "Announcements" \ dist/*.tar.gz - draft: true - prerelease: ${{ needs.check-version.outputs.oclif_channel != 'latest' }} - discussion_category_name: Announcements env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 82b49046025e4b53891501ea3feadfb7cb06bd92 Mon Sep 17 00:00:00 2001 From: Edward Ezekiel Date: Thu, 17 Apr 2025 13:20:28 -0500 Subject: [PATCH 20/35] chore: use --generate-notes flag for release --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index baa3a8f1..dd6e19e9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -141,7 +141,7 @@ jobs: run: | gh release create v${{ needs.check-version.outputs.version }} \ --title "Release v${{ needs.check-version.outputs.version }} ${{ needs.check-version.outputs.oclif_channel == 'latest' && 'Latest' || needs.check-version.outputs.oclif_channel }}" \ - --notes-from-tag \ + --generate-notes \ --draft \ --prerelease=${{ needs.check-version.outputs.oclif_channel != 'latest' }} \ --discussion-category "Announcements" \ From fb22c50d0f2e2a402f34ac06fd9830327c9fe9a0 Mon Sep 17 00:00:00 2001 From: Edward Ezekiel Date: Thu, 17 Apr 2025 13:23:20 -0500 Subject: [PATCH 21/35] chore: use recommended token The GitHub docs recommend using GH_TOKEN when using the CLI in workflows.[1] [1]https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/using-github-cli-in-workflows --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index dd6e19e9..a755a362 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -147,4 +147,4 @@ jobs: --discussion-category "Announcements" \ dist/*.tar.gz env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} From f2921c7f5bb84756d78b65a7cbd93d02f445fff7 Mon Sep 17 00:00:00 2001 From: Edward Ezekiel Date: Thu, 17 Apr 2025 13:28:41 -0500 Subject: [PATCH 22/35] chore: ensure github release is run before s3 --- .github/workflows/release.yml | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a755a362..7bb2e6dc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -82,6 +82,19 @@ jobs: run: | npx oclif pack tarballs --targets=linux-x64,win32-x64,darwin-arm64 --no-xz --parallel + # Create GitHub Release (draft - will be published manually from GitHub UI or CLI) + - name: Create GitHub Release + run: | + gh release create v${{ needs.check-version.outputs.version }} \ + --title "Release v${{ needs.check-version.outputs.version }} ${{ needs.check-version.outputs.oclif_channel == 'latest' && 'Latest' || needs.check-version.outputs.oclif_channel }}" \ + --generate-notes \ + --draft \ + --prerelease=${{ needs.check-version.outputs.oclif_channel != 'latest' }} \ + --discussion-category "Announcements" \ + dist/*.tar.gz + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # S3 Distribution - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v4 @@ -135,16 +148,3 @@ jobs: run: npm publish --tag ${{ needs.check-version.outputs.oclif_channel }} --provenance --access public env: NODE_AUTH_TOKEN: ${{ secrets.HD_CLI_NPM_TOKEN }} - - # Create GitHub Release (draft - will be published manually from GitHub UI or CLI) - - name: Create GitHub Release - run: | - gh release create v${{ needs.check-version.outputs.version }} \ - --title "Release v${{ needs.check-version.outputs.version }} ${{ needs.check-version.outputs.oclif_channel == 'latest' && 'Latest' || needs.check-version.outputs.oclif_channel }}" \ - --generate-notes \ - --draft \ - --prerelease=${{ needs.check-version.outputs.oclif_channel != 'latest' }} \ - --discussion-category "Announcements" \ - dist/*.tar.gz - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} From f1dc5130a70aa9a8db0de25981d6986b8620c8c3 Mon Sep 17 00:00:00 2001 From: Edward Ezekiel Date: Fri, 18 Apr 2025 02:25:27 -0500 Subject: [PATCH 23/35] feat: enable vuln count feature (#188) * feat: enable vuln count feature * chore: fix linting error --- e2e/scan/eol.test.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/e2e/scan/eol.test.ts b/e2e/scan/eol.test.ts index 6804862f..b8fa649e 100644 --- a/e2e/scan/eol.test.ts +++ b/e2e/scan/eol.test.ts @@ -116,11 +116,7 @@ describe('scan:eol e2e', () => { // Match table header match(stdout, /┌.*┬.*┬.*┬.*┬.*┐/, 'Should show table top border'); - match( - stdout, - /│ NAME\s*│ VERSION\s*│ EOL\s*│ DAYS EOL\s*│ TYPE\s*│/, // TODO: add vulns to monorepo api - 'Should show table headers', - ); + match(stdout, /│ NAME\s*│ VERSION\s*│ EOL\s*│ DAYS EOL\s*│ TYPE\s*│ # OF VULNS*|/, 'Should show table headers'); match(stdout, /├.*┼.*┼.*┼.*┼.*┤/, 'Should show table header separator'); // Match table content From 5ff500a36e3a7d5a94adfb4c59c4a9d4c4fcc6bd Mon Sep 17 00:00:00 2001 From: Edward Ezekiel Date: Fri, 18 Apr 2025 09:57:17 -0500 Subject: [PATCH 24/35] chore: add back in e2e specs for package.jsons --- e2e/scan/eol.test.ts | 151 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 141 insertions(+), 10 deletions(-) diff --git a/e2e/scan/eol.test.ts b/e2e/scan/eol.test.ts index b8fa649e..9265e469 100644 --- a/e2e/scan/eol.test.ts +++ b/e2e/scan/eol.test.ts @@ -50,15 +50,6 @@ describe('scan:eol e2e', () => { match(stdout, /EOL Date: 2019-07-24/, 'Should show correct EOL date for bootstrap'); }); - it('scans a directory for EOL components', async () => { - const cmd = `scan:eol --dir ${process.cwd()}`; - const { stdout } = await run(cmd); - - // Match command output patterns for no EOL components - match(stdout, /No End-of-Life or Supported components found in scan/, 'Should show no EOL components message'); - match(stdout, /Use --all flag to view all components/, 'Should provide instructions to view all components'); - }); - it('saves report when --save flag is used', async () => { const cmd = `scan:eol --purls=${simplePurls} --dir=${fixturesDir} --save`; await run(cmd); @@ -116,7 +107,11 @@ describe('scan:eol e2e', () => { // Match table header match(stdout, /┌.*┬.*┬.*┬.*┬.*┐/, 'Should show table top border'); - match(stdout, /│ NAME\s*│ VERSION\s*│ EOL\s*│ DAYS EOL\s*│ TYPE\s*│ # OF VULNS*|/, 'Should show table headers'); + match( + stdout, + /│ NAME\s*│ VERSION\s*│ EOL\s*│ DAYS EOL\s*│ TYPE\s*│/, // TODO: add vulns to monorepo api + 'Should show table headers', + ); match(stdout, /├.*┼.*┼.*┼.*┼.*┤/, 'Should show table header separator'); // Match table content @@ -189,3 +184,139 @@ describe('scan:eol e2e', () => { match(stdout, /⮑ {2}EOL Date: \d{4}-\d{2}-\d{2}/, 'Should show EOL date with arrow'); }); }); + +/** + * Directory scan tests + * Please see CONTRIBUTING.md before adding new tests to this section. + */ +describe('scan:eol e2e directory', () => { + const __dirname = path.dirname(fileURLToPath(import.meta.url)); + const simpleDir = path.resolve(__dirname, '../fixtures/npm/simple'); + const upToDateDir = path.resolve(__dirname, '../fixtures/npm/up-to-date'); + const reportPath = path.join(simpleDir, 'nes.eol.json'); + + async function run(cmd: string) { + // Set up environment + process.env.GRAPHQL_HOST = 'https://api.dev.nes.herodevs.com'; + // process.env.GRAPHQL_HOST = 'http://localhost:3000'; + + // Ensure test directory exists and is clean + await mkdir(simpleDir, { recursive: true }); + + const output = await runCommand(cmd); + + // Log any errors for debugging + if (output.error) { + console.error('Command failed with error:', output.error); + console.error('Error details:', output.stderr); + } + + // Verify command executed successfully + strictEqual(output.error, undefined, 'Command should execute without errors'); + + return output; + } + + it('scans a directory or sbom for EOL components', async () => { + const cmd = `scan:eol --dir ${simpleDir}`; + const { stdout } = await run(cmd); + + // Match command output patterns + match(stdout, /Here are the results of the scan:/, 'Should show results header'); + match(stdout, /pkg:npm\/bootstrap@3\.1\.1/, 'Should detect bootstrap package'); + match(stdout, /End of Life \(EOL\)/, 'Should show EOL status'); + match(stdout, /EOL Date:/, 'Should show EOL date information'); + }); + + it('saves report when --save flag is used', async () => { + const cmd = `scan:eol --dir ${simpleDir} --save`; + await run(cmd); + + // Verify report was saved + const reportExists = existsSync(reportPath); + + strictEqual(reportExists, true, 'Report file should be created'); + + // Verify report content + const report = readFileSync(reportPath, 'utf-8'); + + // Verify report structure using match + match(report, /"components":\s*\[/, 'Report should contain components array'); + match(report, /"purl":\s*"pkg:npm\/bootstrap@3\.1\.1"/, 'Report should contain bootstrap package'); + match(report, /"isEol":\s*true/, 'Bootstrap should be marked as EOL'); + + unlinkSync(reportPath); + }); + it('scans existing SBOM for EOL components', async () => { + const cmd = `scan:eol --file ${simpleDir}/sbom.json`; + const { stdout } = await run(cmd); + + // Match command output patterns + match(stdout, /Here are the results of the scan:/, 'Should show results header'); + match(stdout, /pkg:npm\/bootstrap@3\.1\.1/, 'Should detect bootstrap package'); + match(stdout, /EOL Date: 2019-07-24/, 'Should show correct EOL date for bootstrap'); + }); + + it('outputs JSON when using the --json flag', async () => { + const cmd = `scan:eol --dir ${simpleDir} --json`; + const { stdout } = await run(cmd); + + // Match command output patterns + doesNotMatch(stdout, /Here are the results of the scan:/, 'Should not show results header'); + doesNotThrow(() => JSON.parse(stdout)); + }); + + it('displays results in table format when using the -t flag', async () => { + const cmd = `scan:eol --dir ${simpleDir} -t`; + const { stdout } = await run(cmd); + + // Match table header + match(stdout, /┌.*┬.*┬.*┬.*┬.*┐/, 'Should show table top border'); + match( + stdout, + /│ NAME\s*│ VERSION\s*│ EOL\s*│ DAYS EOL\s*│ TYPE\s*│/, // TODO: add vulns to monorepo api + 'Should show table headers', + ); + match(stdout, /├.*┼.*┼.*┼.*┼.*┤/, 'Should show table header separator'); + + // Match table content + match( + stdout, + /│ bootstrap\s*│ 3\.1\.1\s*│ 2019-07-24\s*│ \d+\s*│ npm\s*│/, + 'Should show bootstrap package in table', + ); + + // Match table footer + match(stdout, /└.*┴.*┴.*┴.*┴.*┘/, 'Should show table bottom border'); + }); + + describe('--all flag', () => { + it('excludes OK packages by default', async () => { + const cmd = `scan:eol --dir ${simpleDir}`; + const { stdout } = await run(cmd); + + // Match command output patterns + match(stdout, /Here are the results of the scan:/, 'Should show results header'); + doesNotMatch(stdout, /pkg:npm\/vue@3\.5\.13/, 'Should not show vue package'); + }); + + it('shows all packages when --all flag is used', async () => { + const cmd = `scan:eol --dir ${simpleDir} --all`; + const { stdout } = await run(cmd); + + // Match command output patterns + match(stdout, /Here are the results of the scan:/, 'Should show results header'); + match(stdout, /pkg:npm\/bootstrap@3\.1\.1/, 'Should detect bootstrap package'); + match(stdout, /pkg:npm\/vue@3\.5\.13/, 'Should show vue package'); + }); + + it('shows "No EOL" message by default if no components are found', async () => { + const cmd = `scan:eol --dir ${upToDateDir}`; + const { stdout } = await run(cmd); + + // Match command output patterns + doesNotMatch(stdout, /Here are the results of the scan:/, 'Should not show results header'); + match(stdout, /No End-of-Life or Supported components found in scan/, 'Should show "No EOL" message'); + }); + }); +}); \ No newline at end of file From 8e60568cd919f37bdfd43cbfaddb65dfcfd5bd20 Mon Sep 17 00:00:00 2001 From: Edward Ezekiel Date: Fri, 18 Apr 2025 10:01:16 -0500 Subject: [PATCH 25/35] build: split release and manual release configs --- .github/workflows/manual-release.yml | 150 +++++++++++++++++++++++++++ .github/workflows/release.yml | 141 ++++--------------------- .release-please-manifest.json | 3 + release-please-config.json | 23 ++++ 4 files changed, 194 insertions(+), 123 deletions(-) create mode 100644 .github/workflows/manual-release.yml create mode 100644 .release-please-manifest.json create mode 100644 release-please-config.json diff --git a/.github/workflows/manual-release.yml b/.github/workflows/manual-release.yml new file mode 100644 index 00000000..655b8871 --- /dev/null +++ b/.github/workflows/manual-release.yml @@ -0,0 +1,150 @@ +name: Manual Release +run-name: Production Release ${{ github.ref_name }} (pushed by ${{ github.actor }}) + +on: + push: + tags: + - v* +permissions: + contents: read + +jobs: + check-version: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version-file: '.nvmrc' + - uses: ./.github/actions/verify-version + id: verify-version + - name: Verify tag matches version + run: | + VERSION=${{ steps.verify-version.outputs.version }} + TAG_VERSION=${GITHUB_REF#refs/tags/v} + if [ "$VERSION" != "$TAG_VERSION" ]; then + echo "Error: Package version ($VERSION) does not match tag version ($TAG_VERSION)" + exit 1 + fi + - name: Determine Oclif channel + run: | + VERSION=${{ steps.verify-version.outputs.version }} + if [[ "$VERSION" == *"-beta"* ]]; then + echo "oclif_channel=beta" >> $GITHUB_OUTPUT + elif [[ "$VERSION" == *"-alpha"* ]]; then + echo "oclif_channel=alpha" >> $GITHUB_OUTPUT + elif [[ "$VERSION" == *"-next"* ]]; then + echo "oclif_channel=next" >> $GITHUB_OUTPUT + else + echo "oclif_channel=latest" >> $GITHUB_OUTPUT + fi + outputs: + version: ${{ steps.verify-version.outputs.version }} + oclif_channel: ${{ steps.determine-channel.outputs.oclif_channel }} + + test: + runs-on: ubuntu-latest + needs: check-version + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version-file: '.nvmrc' + - run: npm ci + - run: npm run build + - run: npm test + - run: npm run test:e2e + + upload-assets: + runs-on: ubuntu-latest + needs: [check-version, test] + permissions: + contents: write + id-token: write + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version-file: '.nvmrc' + registry-url: 'https://registry.npmjs.org' + + # Build + - run: npm ci + - run: npm run build + + # Build platform-specific tarballs + - name: Install linux toolchain + run: | + sudo apt update + sudo apt install nsis p7zip-full p7zip-rar -y + + - name: Build all tarballs in parallel + run: | + npx oclif pack tarballs --targets=linux-x64,win32-x64,darwin-arm64 --no-xz --parallel + + # Create GitHub Release (draft - will be published manually from GitHub UI or CLI) + - name: Create GitHub Release + run: | + gh release create v${{ needs.check-version.outputs.version }} \ + --title "Release v${{ needs.check-version.outputs.version }} ${{ needs.check-version.outputs.oclif_channel == 'latest' && 'Latest' || needs.check-version.outputs.oclif_channel }}" \ + --generate-notes \ + --draft \ + --prerelease=${{ needs.check-version.outputs.oclif_channel != 'latest' }} \ + --discussion-category "Announcements" \ + dist/*.tar.gz + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # S3 Distribution + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: us-east-1 + + - name: Upload and promote to S3 + run: | + # Enable oclif debug logging + export DEBUG=oclif:* + + # Upload tarballs + npx oclif upload tarballs \ + --targets=linux-x64,win32-x64,darwin-arm64 \ + --no-xz + + # Get shortened SHA (first 7 characters) + SHORT_SHA=$(echo ${{ github.sha }} | cut -c1-7) + echo "Using shortened SHA: $SHORT_SHA" + + # Promote to channel + npx oclif promote \ + --channel=${{ needs.check-version.outputs.oclif_channel }} \ + --version=${{ needs.check-version.outputs.version }} \ + --sha=$SHORT_SHA \ + --indexes \ + --targets=linux-x64,win32-x64,darwin-arm64 \ + --ignore-missing + + npm-publish: + runs-on: ubuntu-latest + needs: [check-version, test, upload-assets] + permissions: + id-token: write + contents: write + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version-file: '.nvmrc' + registry-url: 'https://registry.npmjs.org' + + # Clean build for npm publishing + - run: npm ci + - run: npm run build + + # NPM Release + - name: Create NPM release + run: npm publish --tag ${{ needs.check-version.outputs.oclif_channel }} --provenance --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.HD_CLI_NPM_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7bb2e6dc..af062956 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,150 +1,45 @@ -name: Production Release -run-name: Production Release ${{ github.ref_name }} (pushed by ${{ github.actor }}) - +name: Release on: push: - tags: - - v* + branches: + - main + workflow_dispatch: + permissions: contents: read jobs: - check-version: + release-please: runs-on: ubuntu-latest - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - with: - node-version-file: '.nvmrc' - - uses: ./.github/actions/verify-version - id: verify-version - - name: Verify tag matches version - run: | - VERSION=${{ steps.verify-version.outputs.version }} - TAG_VERSION=${GITHUB_REF#refs/tags/v} - if [ "$VERSION" != "$TAG_VERSION" ]; then - echo "Error: Package version ($VERSION) does not match tag version ($TAG_VERSION)" - exit 1 - fi - - name: Determine Oclif channel - run: | - VERSION=${{ steps.verify-version.outputs.version }} - if [[ "$VERSION" == *"-beta"* ]]; then - echo "oclif_channel=beta" >> $GITHUB_OUTPUT - elif [[ "$VERSION" == *"-alpha"* ]]; then - echo "oclif_channel=alpha" >> $GITHUB_OUTPUT - elif [[ "$VERSION" == *"-next"* ]]; then - echo "oclif_channel=next" >> $GITHUB_OUTPUT - else - echo "oclif_channel=latest" >> $GITHUB_OUTPUT - fi outputs: - version: ${{ steps.verify-version.outputs.version }} - oclif_channel: ${{ steps.determine-channel.outputs.oclif_channel }} - - test: - runs-on: ubuntu-latest - needs: check-version - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - with: - node-version-file: '.nvmrc' - - run: npm ci - - run: npm run build - - run: npm test - - run: npm run test:e2e - - upload-assets: - runs-on: ubuntu-latest - needs: [check-version, test] + release_created: ${{ steps.release.outputs.release_created }} permissions: contents: write - id-token: write + pull-requests: write steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - with: - node-version-file: '.nvmrc' - registry-url: 'https://registry.npmjs.org' - - # Build - - run: npm ci - - run: npm run build - - # Build platform-specific tarballs - - name: Install linux toolchain - run: | - sudo apt update - sudo apt install nsis p7zip-full p7zip-rar -y - - - name: Build all tarballs in parallel - run: | - npx oclif pack tarballs --targets=linux-x64,win32-x64,darwin-arm64 --no-xz --parallel - - # Create GitHub Release (draft - will be published manually from GitHub UI or CLI) - - name: Create GitHub Release - run: | - gh release create v${{ needs.check-version.outputs.version }} \ - --title "Release v${{ needs.check-version.outputs.version }} ${{ needs.check-version.outputs.oclif_channel == 'latest' && 'Latest' || needs.check-version.outputs.oclif_channel }}" \ - --generate-notes \ - --draft \ - --prerelease=${{ needs.check-version.outputs.oclif_channel != 'latest' }} \ - --discussion-category "Announcements" \ - dist/*.tar.gz - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - # S3 Distribution - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v4 + - uses: googleapis/release-please-action@a02a34c4d625f9be7cb89156071d8567266a2445 # v4.2.0 + id: release with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws-region: us-east-1 - - - name: Upload and promote to S3 - run: | - # Enable oclif debug logging - export DEBUG=oclif:* - - # Upload tarballs - npx oclif upload tarballs \ - --targets=linux-x64,win32-x64,darwin-arm64 \ - --no-xz - - # Get shortened SHA (first 7 characters) - SHORT_SHA=$(echo ${{ github.sha }} | cut -c1-7) - echo "Using shortened SHA: $SHORT_SHA" + token: ${{ secrets.GITHUB_TOKEN }} + config-file: release-please-config.json + manifest-file: .release-please-manifest.json - # Promote to channel - npx oclif promote \ - --channel=${{ needs.check-version.outputs.oclif_channel }} \ - --version=${{ needs.check-version.outputs.version }} \ - --sha=$SHORT_SHA \ - --indexes \ - --targets=linux-x64,win32-x64,darwin-arm64 \ - --ignore-missing - - npm-publish: + release: + needs: release-please + if: ${{ needs.release-please.outputs.release_created }} runs-on: ubuntu-latest - needs: [check-version, test, upload-assets] permissions: - id-token: write contents: write + id-token: write steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: '.nvmrc' registry-url: 'https://registry.npmjs.org' - - # Clean build for npm publishing - run: npm ci - run: npm run build - - # NPM Release - name: Create NPM release - run: npm publish --tag ${{ needs.check-version.outputs.oclif_channel }} --provenance --access public + run: npm publish --provenance --access public env: NODE_AUTH_TOKEN: ${{ secrets.HD_CLI_NPM_TOKEN }} diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 00000000..adf51686 --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "1.1.0-beta.1" +} \ No newline at end of file diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 00000000..d7ab3433 --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "release-type": "node", + "pull-request-title-pattern": "chore: release v${version}", + "bootstrap-sha": "32c9d5ed00c0e6022e66d5bf31fbb7ed20841202", + "packages": { + ".": { + "include-component-in-tag": false, + "changelog-path": "CHANGELOG.md", + "changelog-sections": [ + { "type": "feat", "section": "Features", "hidden": false }, + { "type": "fix", "section": "Bug Fixes", "hidden": false }, + { "type": "perf", "section": "Core", "hidden": false }, + { "type": "docs", "section": "Core", "hidden": false }, + { "type": "refactor", "section": "Core", "hidden": false }, + { "type": "test", "section": "Core", "hidden": false }, + { "type": "chore", "section": "Miscellaneous", "hidden": false }, + { "type": "build", "section": "Miscellaneous", "hidden": false }, + { "type": "ci", "section": "Miscellaneous", "hidden": false } + ] + } + } +} \ No newline at end of file From 1974951806e2b68d354f060b647f9d05eec373c3 Mon Sep 17 00:00:00 2001 From: Edward Ezekiel Date: Fri, 18 Apr 2025 10:05:28 -0500 Subject: [PATCH 26/35] chore: add developer docs for the release process --- DEVELOPER.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DEVELOPER.md b/DEVELOPER.md index 4b6aceb6..712eee3e 100644 --- a/DEVELOPER.md +++ b/DEVELOPER.md @@ -7,7 +7,7 @@ The CLI currently uses tag-based releases managed through `.github/workflows/manual-release.yml`. To create a release: 1. Run one of the following npm commands: - - `npm run release` - Test the release process without making changes (--dry-run by default) + - `npm run release:dry-run` - Test the release process without making changes - `npm run release:publish:beta` - Create and publish a beta release - `npm run release:publish:latest` - Create and publish a latest release From badde3c8bc1abbe8b3f390a0b8a06d375954df1b Mon Sep 17 00:00:00 2001 From: Edward Ezekiel Date: Fri, 18 Apr 2025 10:06:31 -0500 Subject: [PATCH 27/35] chore: add missing end of files --- .release-please-manifest.json | 2 +- e2e/scan/eol.test.ts | 2 +- release-please-config.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index adf51686..92475378 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { ".": "1.1.0-beta.1" -} \ No newline at end of file +} diff --git a/e2e/scan/eol.test.ts b/e2e/scan/eol.test.ts index 9265e469..3bbb1d59 100644 --- a/e2e/scan/eol.test.ts +++ b/e2e/scan/eol.test.ts @@ -319,4 +319,4 @@ describe('scan:eol e2e directory', () => { match(stdout, /No End-of-Life or Supported components found in scan/, 'Should show "No EOL" message'); }); }); -}); \ No newline at end of file +}); diff --git a/release-please-config.json b/release-please-config.json index d7ab3433..aaa11a31 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -20,4 +20,4 @@ ] } } -} \ No newline at end of file +} From 470bf886176cdc9c3d0d64ce5805c71225be8d66 Mon Sep 17 00:00:00 2001 From: Edward Ezekiel Date: Fri, 18 Apr 2025 10:21:55 -0500 Subject: [PATCH 28/35] chore: tighten permission on npm-publish step Also add back in dry run for npm publish, just in case... --- .github/workflows/manual-release.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/manual-release.yml b/.github/workflows/manual-release.yml index 655b8871..d9a25971 100644 --- a/.github/workflows/manual-release.yml +++ b/.github/workflows/manual-release.yml @@ -131,7 +131,6 @@ jobs: needs: [check-version, test, upload-assets] permissions: id-token: write - contents: write steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 @@ -143,6 +142,12 @@ jobs: - run: npm ci - run: npm run build + # Dry run NPM publish + - name: Dry run NPM publish + run: npm publish --tag ${{ needs.check-version.outputs.oclif_channel }} --provenance --access public --dry-run + env: + NODE_AUTH_TOKEN: ${{ secrets.HD_CLI_NPM_TOKEN }} + # NPM Release - name: Create NPM release run: npm publish --tag ${{ needs.check-version.outputs.oclif_channel }} --provenance --access public From 58afc65ac9f163de43fd857904242008a9ab14fd Mon Sep 17 00:00:00 2001 From: Edward Ezekiel Date: Fri, 18 Apr 2025 10:23:29 -0500 Subject: [PATCH 29/35] chore: cleanup release script in package.json --- DEVELOPER.md | 2 +- package.json | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/DEVELOPER.md b/DEVELOPER.md index 712eee3e..4b6aceb6 100644 --- a/DEVELOPER.md +++ b/DEVELOPER.md @@ -7,7 +7,7 @@ The CLI currently uses tag-based releases managed through `.github/workflows/manual-release.yml`. To create a release: 1. Run one of the following npm commands: - - `npm run release:dry-run` - Test the release process without making changes + - `npm run release` - Test the release process without making changes (--dry-run by default) - `npm run release:publish:beta` - Create and publish a beta release - `npm run release:publish:latest` - Create and publish a latest release diff --git a/package.json b/package.json index f1514812..8ba34e8d 100644 --- a/package.json +++ b/package.json @@ -26,8 +26,7 @@ "prepack": "oclif manifest && oclif readme", "pretest": "npm run lint && npm run typecheck", "readme": "npm run ci:fix && npm run build && npm exec oclif readme", - "release": "chmod +x scripts/release.sh && ./scripts/release.sh", - "release:dry-run": "npm run release", + "release": "./scripts/release.sh", "pre:release:publish": "npm run prepack && git add README.md", "release:publish:beta": "npm run release -- --publish", "release:publish:latest": "npm run release -- --latest --publish", From 9b8ee2bd8e8bc7f8fc0a6c211e66069fae889d8a Mon Sep 17 00:00:00 2001 From: Edward Ezekiel Date: Thu, 17 Apr 2025 09:24:30 -0500 Subject: [PATCH 30/35] chore: add back in e2e specs for sbom scanning --- e2e/scan/eol.test.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/e2e/scan/eol.test.ts b/e2e/scan/eol.test.ts index 3bbb1d59..dcdc9ca5 100644 --- a/e2e/scan/eol.test.ts +++ b/e2e/scan/eol.test.ts @@ -50,6 +50,15 @@ describe('scan:eol e2e', () => { match(stdout, /EOL Date: 2019-07-24/, 'Should show correct EOL date for bootstrap'); }); + it('scans a directory for EOL components', async () => { + const cmd = `scan:eol --dir ${process.cwd()}`; + const { stdout } = await run(cmd); + + // Match command output patterns for no EOL components + match(stdout, /No End-of-Life or Supported components found in scan/, 'Should show no EOL components message'); + match(stdout, /Use --all flag to view all components/, 'Should provide instructions to view all components'); + }); + it('saves report when --save flag is used', async () => { const cmd = `scan:eol --purls=${simplePurls} --dir=${fixturesDir} --save`; await run(cmd); From 4dde76cc4c265326c2c59ebac42ac9cfb38c17a3 Mon Sep 17 00:00:00 2001 From: Edward Ezekiel Date: Thu, 17 Apr 2025 14:32:55 -0500 Subject: [PATCH 31/35] feat: enable vuln count feature --- e2e/scan/eol.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/scan/eol.test.ts b/e2e/scan/eol.test.ts index dcdc9ca5..cc9b0d9c 100644 --- a/e2e/scan/eol.test.ts +++ b/e2e/scan/eol.test.ts @@ -118,7 +118,7 @@ describe('scan:eol e2e', () => { match(stdout, /┌.*┬.*┬.*┬.*┬.*┐/, 'Should show table top border'); match( stdout, - /│ NAME\s*│ VERSION\s*│ EOL\s*│ DAYS EOL\s*│ TYPE\s*│/, // TODO: add vulns to monorepo api + /│ NAME\s*│ VERSION\s*│ EOL\s*│ DAYS EOL\s*│ TYPE\s*│ # OF VULNS*|/, 'Should show table headers', ); match(stdout, /├.*┼.*┼.*┼.*┼.*┤/, 'Should show table header separator'); From cdc0c8579b3bdc3b1eaff8c49172a420c9a94c0a Mon Sep 17 00:00:00 2001 From: Edward Ezekiel Date: Thu, 17 Apr 2025 14:37:39 -0500 Subject: [PATCH 32/35] chore: fix linting error --- e2e/scan/eol.test.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/e2e/scan/eol.test.ts b/e2e/scan/eol.test.ts index cc9b0d9c..2b0328e6 100644 --- a/e2e/scan/eol.test.ts +++ b/e2e/scan/eol.test.ts @@ -116,11 +116,7 @@ describe('scan:eol e2e', () => { // Match table header match(stdout, /┌.*┬.*┬.*┬.*┬.*┐/, 'Should show table top border'); - match( - stdout, - /│ NAME\s*│ VERSION\s*│ EOL\s*│ DAYS EOL\s*│ TYPE\s*│ # OF VULNS*|/, - 'Should show table headers', - ); + match(stdout, /│ NAME\s*│ VERSION\s*│ EOL\s*│ DAYS EOL\s*│ TYPE\s*│ # OF VULNS*|/, 'Should show table headers'); match(stdout, /├.*┼.*┼.*┼.*┼.*┤/, 'Should show table header separator'); // Match table content From 9978443e30628218afe87206c76bfa896d96c692 Mon Sep 17 00:00:00 2001 From: Edward Ezekiel Date: Thu, 17 Apr 2025 16:34:57 -0500 Subject: [PATCH 33/35] feat: hide extra columns on ok and unknown tables When displaying a table for OK and UNKNOWN components, by definition it will not have data for the EOL or DAYS EOL columns. So, we hide these columns in that case. --- src/ui/eol.ui.ts | 27 ++++++++ test/ui/eol.ui.test.ts | 67 +++++++++++++++++++ .../utils/mocks/scan-result-component.mock.ts | 4 +- 3 files changed, 96 insertions(+), 2 deletions(-) diff --git a/src/ui/eol.ui.ts b/src/ui/eol.ui.ts index e33b34fd..d655134a 100644 --- a/src/ui/eol.ui.ts +++ b/src/ui/eol.ui.ts @@ -86,6 +86,16 @@ export function createStatusDisplay( export function createTableForStatus( grouped: Record, status: ComponentStatus, +) { + if (status === 'EOL' || status === 'SUPPORTED') { + return createTableForEOLOrSupported(grouped, status); + } + return createTableForOKOrUnknown(grouped, status); +} + +function createTableForEOLOrSupported( + grouped: Record, + status: 'EOL' | 'SUPPORTED', ) { const data = grouped[status].map((component) => convertComponentToTableRow(component)); @@ -102,6 +112,23 @@ export function createTableForStatus( }); } +function createTableForOKOrUnknown( + grouped: Record, + status: 'OK' | 'UNKNOWN', +) { + const data = grouped[status].map((component) => convertComponentToTableRow(component)); + + return makeTable({ + data, + columns: [ + { key: 'name', name: 'NAME', width: MAX_TABLE_COLUMN_WIDTH }, + { key: 'version', name: 'VERSION', width: 10 }, + { key: 'type', name: 'TYPE', width: 12 }, + { key: 'vulnCount', name: '# OF VULNS', width: 12 }, + ], + }); +} + export function convertComponentToTableRow(component: InsightsEolScanComponent) { const purlParts = PackageURL.fromString(component.purl); const { eolAt, daysEol, vulnCount } = component.info; diff --git a/test/ui/eol.ui.test.ts b/test/ui/eol.ui.test.ts index 36843bb3..83b7441a 100644 --- a/test/ui/eol.ui.test.ts +++ b/test/ui/eol.ui.test.ts @@ -101,6 +101,73 @@ describe('EOL UI', () => { // Act const table = createTableForStatus(grouped, 'EOL'); + // Assert + assert.strictEqual(typeof table, 'string'); + // The table should be empty except for headers + assert.doesNotMatch(table, /test1/); + }); + it('creates a table with components of matching status without EOL columns', () => { + // Arrange + const components = createMockScan([ + createMockComponent('pkg:npm/test1@1.0.0', 'OK'), + createMockComponent('pkg:npm/test2@2.0.0', 'EOL', new Date('2023-01-01'), 365), + createMockComponent('pkg:npm/test3@3.0.0', 'OK'), + ]).components; + const grouped = groupComponentsByStatus(components); + + // Act + const table = createTableForStatus(grouped, 'OK'); + + // Assert + assert.strictEqual(typeof table, 'string'); + // Check that the table contains the expected columns in order + const lines = table.split('\n'); + const headerLine = lines[1]; // Second line contains headers + assert.match(headerLine, /NAME.*VERSION.*TYPE.*# OF VULNS/); + // Verify EOL and DAYS EOL columns are not present + assert.doesNotMatch(headerLine, /EOL/); + assert.doesNotMatch(headerLine, /DAYS EOL/); + // Check data rows + assert.match(table, /test1.*1.0.0.*npm.*0/); + assert.match(table, /test3.*3.0.0.*npm.*0/); + }); + + it('creates a table for UNKNOWN status without EOL columns', () => { + // Arrange + const components = createMockScan([ + createMockComponent('pkg:npm/test1@1.0.0', 'UNKNOWN'), + createMockComponent('pkg:npm/test2@2.0.0', 'OK'), + createMockComponent('pkg:npm/test3@3.0.0', 'UNKNOWN'), + ]).components; + const grouped = groupComponentsByStatus(components); + + // Act + const table = createTableForStatus(grouped, 'UNKNOWN'); + + // Assert + assert.strictEqual(typeof table, 'string'); + // Check that the table contains the expected columns in order + const lines = table.split('\n'); + const headerLine = lines[1]; // Second line contains headers + assert.match(headerLine, /NAME.*VERSION.*TYPE.*# OF VULNS/); + // Verify EOL and DAYS EOL columns are not present + assert.doesNotMatch(headerLine, /EOL/); + assert.doesNotMatch(headerLine, /DAYS EOL/); + // Check data rows + assert.match(table, /test1.*1.0.0.*npm.*0/); + assert.match(table, /test3.*3.0.0.*npm.*0/); + }); + + it('returns empty table when no components match status', () => { + // Arrange + const components = createMockScan([ + createMockComponent('pkg:npm/test1@1.0.0', 'EOL', new Date('2023-01-01'), 365), + ]).components; + const grouped = groupComponentsByStatus(components); + + // Act + const table = createTableForStatus(grouped, 'OK'); + // Assert assert.strictEqual(typeof table, 'string'); // The table should be empty except for headers diff --git a/test/utils/mocks/scan-result-component.mock.ts b/test/utils/mocks/scan-result-component.mock.ts index b2896e57..f0edda6e 100644 --- a/test/utils/mocks/scan-result-component.mock.ts +++ b/test/utils/mocks/scan-result-component.mock.ts @@ -1,9 +1,9 @@ import type { ScanResult } from '../../../src/api/types/hd-cli.types.ts'; -import type { InsightsEolScanComponent } from '../../../src/api/types/nes.types.ts'; +import type { ComponentStatus, InsightsEolScanComponent } from '../../../src/api/types/nes.types.ts'; export const createMockComponent = ( purl: string, - status: 'OK' | 'EOL' | 'SUPPORTED' = 'OK', + status: ComponentStatus = 'OK', eolAt: Date | null = null, daysEol: number | null = null, vulnCount = 0, From 4e6f5c965f9315293120c4fc730acec2df224b5b Mon Sep 17 00:00:00 2001 From: Edward Ezekiel Date: Thu, 17 Apr 2025 16:38:40 -0500 Subject: [PATCH 34/35] chore: simplify method for creating table --- src/ui/eol.ui.ts | 42 +++++++++++++----------------------------- 1 file changed, 13 insertions(+), 29 deletions(-) diff --git a/src/ui/eol.ui.ts b/src/ui/eol.ui.ts index d655134a..455fc538 100644 --- a/src/ui/eol.ui.ts +++ b/src/ui/eol.ui.ts @@ -86,38 +86,22 @@ export function createStatusDisplay( export function createTableForStatus( grouped: Record, status: ComponentStatus, -) { - if (status === 'EOL' || status === 'SUPPORTED') { - return createTableForEOLOrSupported(grouped, status); - } - return createTableForOKOrUnknown(grouped, status); -} - -function createTableForEOLOrSupported( - grouped: Record, - status: 'EOL' | 'SUPPORTED', -) { - const data = grouped[status].map((component) => convertComponentToTableRow(component)); - - return makeTable({ - data, - columns: [ - { key: 'name', name: 'NAME', width: MAX_TABLE_COLUMN_WIDTH }, - { key: 'version', name: 'VERSION', width: 10 }, - { key: 'eol', name: 'EOL', width: 12 }, - { key: 'daysEol', name: 'DAYS EOL', width: 10 }, - { key: 'type', name: 'TYPE', width: 12 }, - { key: 'vulnCount', name: '# OF VULNS', width: 12 }, - ], - }); -} - -function createTableForOKOrUnknown( - grouped: Record, - status: 'OK' | 'UNKNOWN', ) { const data = grouped[status].map((component) => convertComponentToTableRow(component)); + if (status === 'EOL' || status === 'SUPPORTED') { + return makeTable({ + data, + columns: [ + { key: 'name', name: 'NAME', width: MAX_TABLE_COLUMN_WIDTH }, + { key: 'version', name: 'VERSION', width: 10 }, + { key: 'eol', name: 'EOL', width: 12 }, + { key: 'daysEol', name: 'DAYS EOL', width: 10 }, + { key: 'type', name: 'TYPE', width: 12 }, + { key: 'vulnCount', name: '# OF VULNS', width: 12 }, + ], + }); + } return makeTable({ data, columns: [ From daf4b36c6f33ef498e0fa914646c881c8f1f49a6 Mon Sep 17 00:00:00 2001 From: Edward Ezekiel Date: Fri, 18 Apr 2025 10:40:44 -0500 Subject: [PATCH 35/35] chore: delete invalid spec --- e2e/scan/eol.test.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/e2e/scan/eol.test.ts b/e2e/scan/eol.test.ts index 2b0328e6..58b04393 100644 --- a/e2e/scan/eol.test.ts +++ b/e2e/scan/eol.test.ts @@ -50,15 +50,6 @@ describe('scan:eol e2e', () => { match(stdout, /EOL Date: 2019-07-24/, 'Should show correct EOL date for bootstrap'); }); - it('scans a directory for EOL components', async () => { - const cmd = `scan:eol --dir ${process.cwd()}`; - const { stdout } = await run(cmd); - - // Match command output patterns for no EOL components - match(stdout, /No End-of-Life or Supported components found in scan/, 'Should show no EOL components message'); - match(stdout, /Use --all flag to view all components/, 'Should provide instructions to view all components'); - }); - it('saves report when --save flag is used', async () => { const cmd = `scan:eol --purls=${simplePurls} --dir=${fixturesDir} --save`; await run(cmd);