diff --git a/.github/workflows/frontend-licenses-update.yml b/.github/workflows/frontend-licenses-update.yml index ac8676c8a..59cde2c50 100644 --- a/.github/workflows/frontend-licenses-update.yml +++ b/.github/workflows/frontend-licenses-update.yml @@ -32,18 +32,29 @@ jobs: with: egress-policy: audit - - name: Check out code + - name: Checkout PR head (default) uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 + persist-credentials: false - name: Setup GitHub App Bot + if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false) id: setup-bot uses: ./.github/actions/setup-bot with: app-id: ${{ secrets.GH_APP_ID }} private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} + - name: Checkout BASE branch (safe script) + if: github.event_name == 'pull_request' + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ github.event.pull_request.base.sha }} + path: base + fetch-depth: 1 + persist-credentials: false + - name: Set up Node.js uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 with: @@ -53,12 +64,45 @@ jobs: - name: Install frontend dependencies working-directory: frontend - run: npm ci + env: + NPM_CONFIG_IGNORE_SCRIPTS: "true" + run: npm ci --ignore-scripts --audit=false --fund=false - - name: Generate frontend license report + - name: Generate frontend license report (internal PR) + if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false working-directory: frontend + env: + PR_IS_FORK: "false" run: npm run generate-licenses + - name: Generate frontend license report (fork PRs, pinned) + if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true + env: + NPM_CONFIG_IGNORE_SCRIPTS: "true" + working-directory: frontend + run: | + mkdir -p src/assets + npx --yes license-checker@25.0.1 --production --json > src/assets/3rdPartyLicenses.json + + - name: Postprocess with project script (BASE version) + if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true + env: + PR_IS_FORK: "true" + run: | + node base/frontend/scripts/generate-licenses.js \ + --input frontend/src/assets/3rdPartyLicenses.json + + - name: Copy postprocessed artifacts back (fork PRs) + if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true + run: | + mkdir -p frontend/src/assets + if [ -f "base/frontend/src/assets/3rdPartyLicenses.json" ]; then + cp base/frontend/src/assets/3rdPartyLicenses.json frontend/src/assets/3rdPartyLicenses.json + fi + if [ -f "base/frontend/src/assets/license-warnings.json" ]; then + cp base/frontend/src/assets/license-warnings.json frontend/src/assets/license-warnings.json + fi + - name: Check for license warnings run: | if [ -f "frontend/src/assets/license-warnings.json" ]; then @@ -69,7 +113,7 @@ jobs: # PR Event: Check licenses and comment on PR - name: Delete previous license check comments - if: github.event_name == 'pull_request' + if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 with: github-token: ${{ steps.setup-bot.outputs.token }} @@ -102,7 +146,7 @@ jobs: } - name: Comment on PR - License Check Results - if: github.event_name == 'pull_request' + if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 with: github-token: ${{ steps.setup-bot.outputs.token }} diff --git a/frontend/scripts/generate-licenses.js b/frontend/scripts/generate-licenses.js index aaac69800..779274fb4 100644 --- a/frontend/scripts/generate-licenses.js +++ b/frontend/scripts/generate-licenses.js @@ -1,47 +1,57 @@ #!/usr/bin/env node -const { execSync } = require('child_process'); -const fs = require('fs'); -const path = require('path'); +import { execSync } from 'child_process'; +import { existsSync, mkdirSync, writeFileSync } from 'fs'; +import { join, dirname } from 'path'; + +import { argv } from 'node:process'; +const inputIdx = argv.indexOf('--input'); +const INPUT_FILE = inputIdx > -1 ? argv[inputIdx + 1] : null; +const POSTPROCESS_ONLY = !!INPUT_FILE; /** * Generate 3rd party licenses for frontend dependencies * This script creates a JSON file similar to the Java backend's 3rdPartyLicenses.json */ -const OUTPUT_FILE = path.join(__dirname, '..', 'src', 'assets', '3rdPartyLicenses.json'); -const PACKAGE_JSON = path.join(__dirname, '..', 'package.json'); +const OUTPUT_FILE = join(__dirname, '..', 'src', 'assets', '3rdPartyLicenses.json'); +const PACKAGE_JSON = join(__dirname, '..', 'package.json'); // Ensure the output directory exists -const outputDir = path.dirname(OUTPUT_FILE); -if (!fs.existsSync(outputDir)) { - fs.mkdirSync(outputDir, { recursive: true }); +const outputDir = dirname(OUTPUT_FILE); +if (!existsSync(outputDir)) { + mkdirSync(outputDir, { recursive: true }); } console.log('🔍 Generating frontend license report...'); try { - // Install license-checker if not present - try { - require.resolve('license-checker'); - } catch (e) { - console.log('📦 Installing license-checker...'); - execSync('npm install --save-dev license-checker', { stdio: 'inherit' }); + // Safety guard: don't run this script on fork PRs (workflow setzt PR_IS_FORK) + if (process.env.PR_IS_FORK === 'true' && !POSTPROCESS_ONLY) { + console.error('Fork PR detected: only --input (postprocess-only) mode is allowed.'); + process.exit(2); } - // Generate license report using license-checker (more reliable) - const licenseReport = execSync('npx license-checker --production --json', { - encoding: 'utf8', - cwd: path.dirname(PACKAGE_JSON) - }); - let licenseData; - try { - licenseData = JSON.parse(licenseReport); - } catch (parseError) { - console.error('❌ Failed to parse license data:', parseError.message); - console.error('Raw output:', licenseReport.substring(0, 500) + '...'); - process.exit(1); + // Generate license report using pinned license-checker; disable lifecycle scripts + if (POSTPROCESS_ONLY) { + licenseData = JSON.parse(require('fs').readFileSync(INPUT_FILE, 'utf8')); + } else { + const licenseReport = execSync( + 'npx --yes license-checker@25.0.1 --production --json', + { + encoding: 'utf8', + cwd: dirname(PACKAGE_JSON), + env: { ...process.env, NPM_CONFIG_IGNORE_SCRIPTS: 'true' } + } + ); + try { + licenseData = JSON.parse(licenseReport); + } catch (parseError) { + console.error('❌ Failed to parse license data:', parseError.message); + console.error('Raw output:', licenseReport.substring(0, 500) + '...'); + process.exit(1); + } } if (!licenseData || typeof licenseData !== 'object') { @@ -152,8 +162,8 @@ try { }); // Write license warnings to a separate file for CI/CD - const warningsFile = path.join(__dirname, '..', 'src', 'assets', 'license-warnings.json'); - fs.writeFileSync(warningsFile, JSON.stringify({ + const warningsFile = join(__dirname, '..', 'src', 'assets', 'license-warnings.json'); + writeFileSync(warningsFile, JSON.stringify({ warnings: problematicLicenses, generated: new Date().toISOString() }, null, 2)); @@ -163,7 +173,7 @@ try { } // Write to file - fs.writeFileSync(OUTPUT_FILE, JSON.stringify(transformedData, null, 4)); + writeFileSync(OUTPUT_FILE, JSON.stringify(transformedData, null, 4)); console.log(`✅ License report generated successfully!`); console.log(`📄 Found ${transformedData.dependencies.length} dependencies`);