Skip to content
Merged
Show file tree
Hide file tree
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 Feb 2, 2026
7c675a3
feat(license): migrate historical exception data to unified JSON
jeefy Feb 2, 2026
ebe1284
feat(license): add scripts to generate CSV and SPDX from JSON
jeefy Feb 2, 2026
9be1ded
feat(license): add static website for browsing exceptions
jeefy Feb 2, 2026
3924e0f
ci: add GitHub Pages deployment for license exceptions site
jeefy Feb 2, 2026
c987db3
docs(license): update README with new data structure and site info
jeefy Feb 2, 2026
c71c921
ci: add automated triage for license exception issues
jeefy Feb 2, 2026
24ea1d2
ci: add workflow to create PR when exception is approved
jeefy Feb 2, 2026
25a8e82
ci: add validation workflow for exceptions.json
jeefy Feb 2, 2026
4928438
docs: add link to exceptions database in issue template
jeefy Feb 2, 2026
c194889
feat(license-exceptions): add E2E tests and blanket exceptions page
jeefy Feb 4, 2026
1efeec4
fix(license-exceptions): use deterministic timestamp in SPDX generation
jeefy Feb 4, 2026
95e7a11
Update license-exceptions/tests/package-exceptions.spec.js
jeefy Feb 4, 2026
4feac20
Update license-exceptions/CNCF-licensing-exceptions.csv
jeefy Feb 4, 2026
92d0036
Update license-exceptions/tests/package-exceptions.spec.js
jeefy Feb 4, 2026
0368959
fix(license-exceptions): correct date typo in exception comments
jeefy Feb 4, 2026
358aa8a
feat(license-exceptions): add project column, denied status, and back…
jeefy Feb 5, 2026
9d82865
fix(ci): add 'denied' status to validation workflow
jeefy Feb 5, 2026
3f50abf
swap to netlify
jeefy Feb 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/ISSUE_TEMPLATE/license-exception-request.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,16 @@ body:

<br />

# Check for existing exceptions

Before submitting, please check if an exception already exists for your component(s):

**[Search the exceptions database](https://exceptions.cncf.io/)**

If an exception already exists for the same package and license, you do not need to request a new one.
Comment thread
jeefy marked this conversation as resolved.

<br />

---

# License Exception Request form
Expand Down
50 changes: 50 additions & 0 deletions .github/workflows/deploy-license-site.yml
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
46 changes: 46 additions & 0 deletions .github/workflows/e2e-license-site.yml
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
148 changes: 148 additions & 0 deletions .github/workflows/license-exception-approved.yml
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++;
}
Comment thread
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/).`
});
Comment thread
jeefy marked this conversation as resolved.
}
114 changes: 114 additions & 0 deletions .github/workflows/license-exception-triage.yml
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 }}');
Comment thread
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`;
}
Comment thread
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
});
Loading
Loading