-
Notifications
You must be signed in to change notification settings - Fork 812
Improve viewing/tracking license exceptions #1294
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
jeefy
merged 19 commits into
cncf:main
from
jeefy:feat/license-exceptions-modernization
Feb 18, 2026
Merged
Changes from all commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
ab06ae8
feat(license): add JSON schema for centralized exception data
jeefy 7c675a3
feat(license): migrate historical exception data to unified JSON
jeefy ebe1284
feat(license): add scripts to generate CSV and SPDX from JSON
jeefy 9be1ded
feat(license): add static website for browsing exceptions
jeefy 3924e0f
ci: add GitHub Pages deployment for license exceptions site
jeefy c987db3
docs(license): update README with new data structure and site info
jeefy c71c921
ci: add automated triage for license exception issues
jeefy 24ea1d2
ci: add workflow to create PR when exception is approved
jeefy 25a8e82
ci: add validation workflow for exceptions.json
jeefy 4928438
docs: add link to exceptions database in issue template
jeefy c194889
feat(license-exceptions): add E2E tests and blanket exceptions page
jeefy 1efeec4
fix(license-exceptions): use deterministic timestamp in SPDX generation
jeefy 95e7a11
Update license-exceptions/tests/package-exceptions.spec.js
jeefy 4feac20
Update license-exceptions/CNCF-licensing-exceptions.csv
jeefy 92d0036
Update license-exceptions/tests/package-exceptions.spec.js
jeefy 0368959
fix(license-exceptions): correct date typo in exception comments
jeefy 358aa8a
feat(license-exceptions): add project column, denied status, and back…
jeefy 9d82865
fix(ci): add 'denied' status to validation workflow
jeefy 3f50abf
swap to netlify
jeefy File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| name: License Exceptions Site Tests | ||
|
|
||
| on: | ||
| push: | ||
| branches: [main] | ||
| paths: | ||
| - 'license-exceptions/**' | ||
| pull_request: | ||
| paths: | ||
| - 'license-exceptions/**' | ||
| workflow_dispatch: | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| test: | ||
| name: Run E2E Tests | ||
| runs-on: ubuntu-latest | ||
| defaults: | ||
| run: | ||
| working-directory: license-exceptions | ||
|
|
||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@v4 | ||
|
|
||
| - name: Setup Node.js | ||
| uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: '20' | ||
| cache: 'npm' | ||
| cache-dependency-path: license-exceptions/package-lock.json | ||
|
|
||
| - name: Install dependencies | ||
| run: npm ci | ||
|
|
||
| - name: Install Playwright Browsers | ||
| run: npx playwright install chromium --with-deps | ||
|
|
||
| - name: Run Playwright tests | ||
| run: npm test | ||
|
|
||
| - name: Upload test results | ||
| uses: actions/upload-artifact@v4 | ||
| if: ${{ !cancelled() }} | ||
| with: | ||
| name: playwright-report | ||
| path: license-exceptions/playwright-report/ | ||
| retention-days: 7 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| name: E2E Tests - License Exceptions Site | ||
|
|
||
| on: | ||
| pull_request: | ||
| paths: | ||
| - 'license-exceptions/site/**' | ||
| - 'license-exceptions/exceptions.json' | ||
| - 'license-exceptions/tests/**' | ||
| - 'license-exceptions/playwright.config.js' | ||
| - 'license-exceptions/package.json' | ||
|
|
||
| jobs: | ||
| e2e-tests: | ||
| name: Run E2E Tests | ||
| runs-on: ubuntu-latest | ||
| defaults: | ||
| run: | ||
| working-directory: license-exceptions | ||
|
|
||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@v4 | ||
|
|
||
| - name: Setup Node.js | ||
| uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: '20' | ||
| cache: 'npm' | ||
| cache-dependency-path: license-exceptions/package-lock.json | ||
|
|
||
| - name: Install dependencies | ||
| run: npm ci | ||
|
|
||
| - name: Install Playwright Browsers | ||
| run: npx playwright install chromium --with-deps | ||
|
|
||
| - name: Run Playwright tests | ||
| run: npm test | ||
|
|
||
| - name: Upload test results | ||
| uses: actions/upload-artifact@v4 | ||
| if: ${{ !cancelled() }} | ||
| with: | ||
| name: playwright-report | ||
| path: license-exceptions/playwright-report/ | ||
| retention-days: 7 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,148 @@ | ||
| name: License Exception Decision | ||
|
|
||
| on: | ||
| issues: | ||
| types: [labeled] | ||
|
|
||
| jobs: | ||
| create-pr: | ||
| if: github.event.label.name == 'approved' || github.event.label.name == 'denied' | ||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| contents: write | ||
| pull-requests: write | ||
| issues: write | ||
|
|
||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@v4 | ||
|
|
||
| - name: Setup Node.js | ||
| uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: '20' | ||
|
|
||
| - name: Parse issue for components | ||
| id: parse | ||
| uses: actions/github-script@v7 | ||
| with: | ||
| script: | | ||
| const body = context.payload.issue.body || ''; | ||
| const issueNumber = context.issue.number; | ||
| const today = new Date().toISOString().split('T')[0]; | ||
| const labelName = context.payload.label.name; | ||
| const status = labelName === 'approved' ? 'approved' : 'denied'; | ||
|
|
||
| // Extract project name | ||
| const projectMatch = body.match(/For which CNCF project[^]*?\n\n([^\n]+)/); | ||
| const project = projectMatch ? projectMatch[1].trim() : 'Unknown'; | ||
|
|
||
| // Extract scope/usage context if present | ||
| const scopeMatch = body.match(/(?:How|Where|What).*(?:used|usage|scope|context)[^]*?\n\n([^\n]+)/i); | ||
| const defaultScope = scopeMatch ? scopeMatch[1].trim() : undefined; | ||
|
|
||
| // Extract component table - parse all columns including scope | ||
| const tableMatch = body.match(/\|[^\n]*Component[^\n]*\|[\s\S]*?\n\|[-|\s]+\|\n([\s\S]*?)(?=\n\n|\n###|$)/); | ||
| const newExceptions = []; | ||
|
|
||
| if (tableMatch) { | ||
| const rows = tableMatch[1].trim().split('\n'); | ||
| let idx = 1; | ||
| for (const row of rows) { | ||
| const cells = row.split('|').map(c => c.trim()).filter(c => c); | ||
| if (cells.length >= 4 && cells[0]) { | ||
| // Try to extract scope from table (column 5 if present) or use default | ||
| const scope = (cells.length >= 5 && cells[4]) ? cells[4] : defaultScope; | ||
|
|
||
| newExceptions.push({ | ||
| id: `exc-${today}-${String(idx).padStart(3, '0')}`, | ||
| package: cells[0], | ||
| packageUrl: cells[1] || undefined, | ||
| license: cells[3], | ||
| project: project, | ||
| approvedDate: today, | ||
| issueUrl: `https://github.com/${context.repo.owner}/${context.repo.repo}/issues/${issueNumber}`, | ||
| status: status, | ||
| scope: scope, | ||
| comment: cells.length >= 6 ? cells[5] : undefined | ||
| }); | ||
| idx++; | ||
| } | ||
|
jeefy marked this conversation as resolved.
|
||
| } | ||
| } | ||
|
|
||
| // Write to temp file for next step | ||
| const fs = require('fs'); | ||
| fs.writeFileSync('/tmp/new-exceptions.json', JSON.stringify(newExceptions, null, 2)); | ||
|
|
||
| core.setOutput('project', project); | ||
| core.setOutput('count', newExceptions.length); | ||
| core.setOutput('date', today); | ||
| core.setOutput('status', status); | ||
|
|
||
| - name: Update exceptions.json | ||
| run: | | ||
| node -e " | ||
| const fs = require('fs'); | ||
| const newExceptions = JSON.parse(fs.readFileSync('/tmp/new-exceptions.json', 'utf-8')); | ||
| const data = JSON.parse(fs.readFileSync('license-exceptions/exceptions.json', 'utf-8')); | ||
|
|
||
| // Add new exceptions at the beginning | ||
| data.exceptions.unshift(...newExceptions); | ||
|
|
||
| // Update lastUpdated | ||
| data.lastUpdated = new Date().toISOString().split('T')[0]; | ||
|
|
||
| fs.writeFileSync('license-exceptions/exceptions.json', JSON.stringify(data, null, 2)); | ||
| console.log('Added', newExceptions.length, 'exceptions'); | ||
| " | ||
|
|
||
| - name: Generate derived formats | ||
| run: | | ||
| cd license-exceptions | ||
| node scripts/generate-all.js | ||
|
|
||
| - name: Create Pull Request | ||
| id: create-pr | ||
| uses: peter-evans/create-pull-request@v6 | ||
| with: | ||
| token: ${{ secrets.GITHUB_TOKEN }} | ||
| branch: license-exception-${{ github.event.issue.number }} | ||
| title: "Record license exception decision (${{ steps.parse.outputs.status }}) for ${{ steps.parse.outputs.project }} (#${{ github.event.issue.number }})" | ||
| body: | | ||
| ## License Exception Decision | ||
|
|
||
| This PR records the license exception decision from #${{ github.event.issue.number }}. | ||
|
|
||
| **Project:** ${{ steps.parse.outputs.project }} | ||
| **Decision:** ${{ steps.parse.outputs.status }} | ||
| **Decision Date:** ${{ steps.parse.outputs.date }} | ||
| **Exceptions Recorded:** ${{ steps.parse.outputs.count }} | ||
|
|
||
| Closes #${{ github.event.issue.number }} | ||
| commit-message: "feat(license): record ${{ steps.parse.outputs.status }} exceptions for ${{ steps.parse.outputs.project }} (#${{ github.event.issue.number }})" | ||
| add-paths: | | ||
| license-exceptions/exceptions.json | ||
| license-exceptions/CNCF-licensing-exceptions.csv | ||
| license-exceptions/cncf-exceptions-current.spdx | ||
|
|
||
| - name: Comment on issue | ||
| uses: actions/github-script@v7 | ||
| with: | ||
| script: | | ||
| const prNumber = '${{ steps.create-pr.outputs.pull-request-number }}'; | ||
| const prUrl = '${{ steps.create-pr.outputs.pull-request-url }}'; | ||
| const status = '${{ steps.parse.outputs.status }}'; | ||
|
|
||
| if (prNumber) { | ||
| const emoji = status === 'approved' ? '✅' : '❌'; | ||
| const title = status === 'approved' ? 'Exception Approved' : 'Exception Denied'; | ||
| const action = status === 'approved' ? 'add these approved exceptions' : 'record these denied exceptions'; | ||
|
|
||
| await github.rest.issues.createComment({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| issue_number: context.issue.number, | ||
| body: `## ${emoji} ${title}\n\nA pull request has been created to ${action} to the database:\n\n${prUrl}\n\nOnce merged, the exceptions will appear in the [exceptions database](https://exceptions.cncf.io/).` | ||
| }); | ||
|
jeefy marked this conversation as resolved.
|
||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| name: License Exception Triage | ||
|
|
||
| on: | ||
| issues: | ||
| types: [opened, edited] | ||
|
|
||
| jobs: | ||
| triage: | ||
| if: contains(github.event.issue.labels.*.name, 'licensing') | ||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| issues: write | ||
| contents: read | ||
|
|
||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@v4 | ||
|
|
||
| - name: Setup Node.js | ||
| uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: '20' | ||
|
|
||
| - name: Parse issue and check duplicates | ||
| id: parse | ||
| uses: actions/github-script@v7 | ||
| with: | ||
| script: | | ||
| // Parse issue body | ||
| const body = context.payload.issue.body || ''; | ||
|
|
||
| // Extract project name (after "For which CNCF project" question) | ||
| const projectMatch = body.match(/For which CNCF project[^]*?\n\n([^\n]+)/); | ||
| const project = projectMatch ? projectMatch[1].trim() : 'Unknown'; | ||
|
|
||
| // Extract component table rows | ||
| const tableMatch = body.match(/\|[^\n]*Component[^\n]*\|[\s\S]*?\n\|[-|\s]+\|\n([\s\S]*?)(?=\n\n|\n###|$)/); | ||
| const components = []; | ||
|
|
||
| if (tableMatch) { | ||
| const rows = tableMatch[1].trim().split('\n'); | ||
| for (const row of rows) { | ||
| const cells = row.split('|').map(c => c.trim()).filter(c => c); | ||
| if (cells.length >= 1 && cells[0]) { | ||
| components.push(cells[0]); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Read exceptions.json | ||
| const fs = require('fs'); | ||
| const exceptionsData = JSON.parse(fs.readFileSync('license-exceptions/exceptions.json', 'utf-8')); | ||
| const existingPackages = new Set(exceptionsData.exceptions.map(e => e.package.toLowerCase())); | ||
|
|
||
| // Check for duplicates | ||
| const duplicates = components.filter(c => { | ||
| const normalized = c.toLowerCase().replace(/^https?:\/\//, '').replace(/\/$/, ''); | ||
| return existingPackages.has(normalized); | ||
| }); | ||
|
|
||
| core.setOutput('project', project); | ||
| core.setOutput('component_count', components.length); | ||
| core.setOutput('duplicates', JSON.stringify(duplicates)); | ||
| core.setOutput('has_duplicates', duplicates.length > 0); | ||
|
|
||
| - name: Add labels | ||
| uses: actions/github-script@v7 | ||
| with: | ||
| script: | | ||
| const hasDuplicates = ${{ steps.parse.outputs.has_duplicates }}; | ||
|
|
||
| const labels = ['needs-review']; | ||
| if (hasDuplicates) { | ||
| labels.push('possible-duplicate'); | ||
| } | ||
|
|
||
| await github.rest.issues.addLabels({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| issue_number: context.issue.number, | ||
| labels: labels | ||
| }); | ||
|
|
||
| - name: Post triage comment | ||
| uses: actions/github-script@v7 | ||
| with: | ||
| script: | | ||
| const project = `${{ steps.parse.outputs.project }}`; | ||
| const componentCount = ${{ steps.parse.outputs.component_count }}; | ||
| const duplicates = JSON.parse('${{ steps.parse.outputs.duplicates }}'); | ||
|
jeefy marked this conversation as resolved.
|
||
|
|
||
| let comment = `## Automated Triage Summary\n\n`; | ||
| comment += `**Project:** ${project}\n`; | ||
| comment += `**Components Requested:** ${componentCount}\n\n`; | ||
|
|
||
| if (duplicates.length > 0) { | ||
| comment += `### ⚠️ Possible Duplicates Detected\n\n`; | ||
| comment += `The following components may already have an exception:\n\n`; | ||
| for (const dup of duplicates) { | ||
| comment += `- \`${dup}\`\n`; | ||
| } | ||
| comment += `\nPlease check the [exceptions database](https://exceptions.cncf.io/) to verify.\n\n`; | ||
| } | ||
|
jeefy marked this conversation as resolved.
|
||
|
|
||
| comment += `---\n`; | ||
| comment += `*This issue will be reviewed by the CNCF staff and Legal Committee. `; | ||
| comment += `See [process documentation](https://github.com/cncf/foundation/blob/main/policies-guidance/allowed-third-party-license-policy.md#process-for-applying-for-an-exception).*`; | ||
|
|
||
| await github.rest.issues.createComment({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| issue_number: context.issue.number, | ||
| body: comment | ||
| }); | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.