feat: automated LFX Mentorship proposal intake system#1884
feat: automated LFX Mentorship proposal intake system#1884nate-double-u wants to merge 1 commit into
Conversation
ee8af55 to
900c742
Compare
|
/cc @mlehotskylf |
|
There's a live testable version of this here: I opened a PR as it's easier to see what's going on this way. |
900c742 to
93d5be6
Compare
93d5be6 to
5e87fc5
Compare
There was a problem hiding this comment.
Pull request overview
This PR replaces the manual PR-based LFX Mentorship proposal intake with a GitHub Issue Form plus a set of GitHub Actions workflows to validate proposals, manage approvals via slash commands, sync proposal state to a Project v2 board, and export CNCF-approved proposals into term artifacts (JSON/README/CSV).
Changes:
- Add an LFX program proposal Issue Form and supporting configuration (terms, quotas, approvers).
- Introduce workflows for validation, approvals, board sync, CNCF landscape project/term syncing, exports, and merge notifications.
- Update contributor-facing documentation to point users to the new issue-form workflow.
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 18 comments.
Show a summary per file
| File | Description |
|---|---|
| programs/lfx-mentorship/README.md | Documents the new “propose a program” issue-form workflow and approval lifecycle. |
| mentors/README.md | Updates mentor submission guidance to use the issue form. |
| CONTRIBUTING.md | Updates contribution instructions for LFX proposals to use the issue form. |
| programs/lfx-mentorship/automation/terms.yml | Defines active terms as the source of truth for dropdowns/exports. |
| programs/lfx-mentorship/automation/quotas.yml | Adds per-project proposal quota configuration used by validation. |
| programs/lfx-mentorship/automation/approvers.yml | Adds global and per-project fallback approver configuration. |
| programs/lfx-mentorship/automation/ADMIN_GUIDE.md | Adds an admin/operator guide for running and maintaining the automation. |
| .github/ISSUE_TEMPLATE/lfx-program-proposal.yml | Introduces the structured issue form for submitting proposals. |
| .github/settings.yml | Adds labels for the proposal lifecycle gates and term/year tracking. |
| .github/workflows/lfx-proposal-validate.yml | Validates issue-form content, manages gate/term labels, and syncs board status. |
| .github/workflows/lfx-proposal-approvals.yml | Implements /approve, /confirm, /cncf-approve and board sync. |
| .github/workflows/lfx-export.yml | Exports CNCF-approved proposals into term artifacts and opens an export PR. |
| .github/workflows/lfx-export-merge-notify.yml | Notifies proposal issues when an export PR merges. |
| .github/workflows/lfx-proposal-board-sync.yml | Syncs issue label state to Project v2 board status on issue events. |
| .github/workflows/landscape-projects-sync.yml | Syncs CNCF project dropdown and term dropdowns from landscape + terms.yml. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| the proposal form: | ||
|
|
||
| CNCF will select the projects that will participate in the LFX mentorship round and they will appear on the LFX Mentorship Platform website after the selection. | ||
| **[Submit a program proposal](https://github.com/nate-double-u/mentoring/issues/new?template=lfx-program-proposal.yml)** |
There was a problem hiding this comment.
We'll update this when we move from draft to ready for review. For now I want to use dev env links.
| 1. **Be a maintainer or approver** of the CNCF project you are proposing | ||
| work for, and have a maintainer's explicit support | ||
| 2. **File an issue** using the | ||
| [LFX Mentorship Program Proposal](https://github.com/nate-double-u/mentoring/issues/new?template=lfx-program-proposal.yml) |
There was a problem hiding this comment.
We'll update this when we move from draft to ready for review. For now I want to use dev env links.
| if (labels.includes('Applications Closed')) status = 'Applications Closed'; | ||
| else if (labels.includes('Open for Applications')) status = 'Open for Applications'; | ||
| else if (labels.includes('Mentors added')) status = 'Mentors added'; | ||
| else if (labels.includes('LFX Approved')) status = 'LFX Approved'; | ||
| else if (labels.includes('Posted to LFX')) status = 'Posted to LFX'; |
| 2. **Find a project idea**: Either come up with a new mentorship project idea or find an existing mentorship project idea that you would like to mentor for. | ||
| The definition of a good project idea varies from program to program. Different mentorship programs and project initiatives have their own unique focuses and areas of emphasis. For instance, some projects place a greater emphasis on coding and software development, while others prioritise documentation and technical writing. The specific goals and objectives of each program may vary, but generally, they strive to provide valuable learning experiences and support to participants in their respective fields. | ||
| 3. **Submit your project idea**: Submit your project idea to the [CNCF mentoring repository](https://github.com/cncf/mentoring). You can use the [project idea template](https://github.com/cncf/mentoring/blob/main/PROJECT_IDEA_TEMPLATE.md). Information on how to submit your project idea will be provided in the program announcement that will be sent out. | ||
| 3. **Submit your proposal**: File an issue using the [LFX Mentorship Program Proposal](https://github.com/nate-double-u/mentoring/issues/new?template=lfx-program-proposal.yml) form. The form guides you through all required fields. See the [LFX Mentorship README](https://github.com/nate-double-u/mentoring/blob/main/programs/lfx-mentorship/README.md#how-to-propose-a-program) for details on validation, approvals, and what to expect after submission. |
There was a problem hiding this comment.
We'll update this when we move from draft to ready for review. For now I want to use dev env links.
| // ── Inline issue-form parser ── | ||
| // Splits on ### headings, returns { label: value } map | ||
| function parseIssueForm(body) { | ||
| const fields = {}; | ||
| const sections = body.split(/^### +/m).slice(1); | ||
| for (const section of sections) { | ||
| const nl = section.indexOf('\n'); | ||
| if (nl === -1) continue; | ||
| const label = section.slice(0, nl).trim(); | ||
| let value = section.slice(nl + 1).trim(); | ||
| if (value === '_No response_') value = ''; | ||
| fields[label] = value; | ||
| } | ||
| return fields; | ||
| } | ||
|
|
||
| const fields = parseIssueForm(body); |
|
|
||
| overrides: | ||
| kubernetes: 8 | ||
| open-telemetry: 8 |
| # <project-slug>: # must match the dropdown value in the issue form | ||
| # fallback_teams: # GitHub teams (org/team) whose members may /approve | ||
| # - org/team-name | ||
| # fallback_handles: # individual GitHub handles | ||
| # - username |
| const projectKey = project.toLowerCase().replace(/\s+/g, '-'); | ||
| const quota = overrides[projectKey] || defaultQ; | ||
|
|
|
|
||
| // 3a. Per-project fallback handles and teams | ||
| if (project) { | ||
| const projectKey = project.toLowerCase().replace(/\s+/g, '-'); |
| overrides: | ||
| kubernetes: 8 | ||
| open-telemetry: 8 | ||
| ``` |
Replace the manual PR-based LFX Mentorship proposal workflow with a GitHub Issue Form and GitHub Actions automation pipeline. See PR description and cncf#1883 for full details. Assisted-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Nate W <natew@cncf.io>
5e87fc5 to
0a80010
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 15 changed files in this pull request and generated 10 comments.
Comments suppressed due to low confidence (12)
.github/workflows/lfx-export.yml:91
- GitHub typically stores issue bodies with
\r\nline endings. The substring checkbody.includes(### Term\n\n${term})uses bare\nand will not match a body containing### Term\r\n\r\n2026 Term 3 (Sep-Nov). As a result, the export may filter out legitimately approved proposals and report "no changes," producing an empty export. Consider normalizing the body (e.g., replacing\r\nwith\n) before substring checks, or using the parsed-form fields rather than raw substring matching.
const termIssues = issues.filter(iss => {
const body = iss.body || '';
return body.includes(`### Term\n\n${term}`);
});
.github/workflows/lfx-proposal-validate.yml:222
- The same
\r\nversus\nmismatch as in the export workflow applies to the quota check:b.includes(### CNCF Project\n\n${project})will likely fail to match issue bodies that have CRLF line endings, so the quota counter will undercount existing proposals. Consider normalizing line endings before comparing, or parsing the form fields with the existingparseIssueFormhelper instead of substring matching the raw body.
const sameProjectTerm = issues.filter(iss => {
if (iss.number === issue_number) return false;
const b = iss.body || '';
return b.includes(`### CNCF Project\n\n${project}`)
&& b.includes(`### Term\n\n${term}`);
});
.github/workflows/lfx-proposal-approvals.yml:250
- The project-key normalization
project.toLowerCase().replace(/\s+/g, '-')only converts whitespace to hyphens, not camelCase boundaries. The dropdown value "OpenTelemetry" becomes the keyopentelemetry, butapprovers.ymlandquotas.ymluseopen-telemetry. As a consequence, both the OpenTelemetry quota override (open-telemetry: 8) and the OpenTelemetry fallback approvers / fallback team (open-telemetry/governance-committee,maryliag) will not be matched at runtime. Either rename the YAML keys to match the slug actually produced, or normalize the project key consistently (e.g., look up a canonical slug fromprojects.yml).
if (project) {
const projectKey = project.toLowerCase().replace(/\s+/g, '-');
const re = new RegExp(`^${projectKey.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}:`, 'im');
const parts = raw.split(re);
.github/workflows/lfx-proposal-validate.yml:208
- Same key-normalization issue as in the approvals workflow:
project.toLowerCase().replace(/\s+/g, '-')producesopentelemetryfor the dropdown value "OpenTelemetry", which will not match theopen-telemetryoverride inquotas.yml. The default quota will silently apply instead of the override.
const projectKey = project.toLowerCase().replace(/\s+/g, '-');
const quota = overrides[projectKey] || defaultQ;
.github/workflows/lfx-proposal-approvals.yml:234
- The CSV is parsed with a naive
line.split(','), which breaks if any cell contains a quoted comma (theOWNERScolumn or company names sometimes do). A maintainer in a row that has any earlier comma-containing cell will then be looked up against the wrong column, silently denying authorization. Consider using a small CSV parser (orcsv-parse) for robustness.
const normalProject = project.toLowerCase();
for (const line of csv.split('\n').slice(1)) {
if (!line.trim()) continue;
const cols = line.split(',');
const csvProject = (cols[1] || '').trim().toLowerCase();
const csvGh = (cols[4] || '').trim().toLowerCase();
if (csvGh === commenter.toLowerCase()
&& (csvProject.startsWith(normalProject) || csvProject.includes(normalProject))) {
authorized = true;
authSource = `project-maintainers.csv (${cols[1].trim()})`;
break;
}
}
.github/workflows/lfx-export.yml:229
- The
monthMaponly covers Mar/May/Jun/Aug/Sep/Nov. If a future term interms.ymluses any other abbreviation (e.g., "Dec-Feb" or "Apr-Jun"), the README title will silently fall back to the abbreviation rather than expanding it, producing inconsistent README headings. Consider including all 12 months.
const monthMap = {
'Mar': 'March', 'May': 'May', 'Jun': 'June',
'Aug': 'August', 'Sep': 'September', 'Nov': 'November',
};
const monthParts = months.split('-').map(m => monthMap[m.trim()] || m.trim());
const readmeTitle = `Term ${termNum} - ${year} ${monthParts.join(' - ')}`;
.github/workflows/lfx-export.yml:413
- The branch tree URL embeds the branch name
automation/lfx-export-${year}-${termDir}followed by a path. GitHub disambiguates branch-vs-path by trying progressively longer prefixes against existing refs, but URLs containing slashes in the branch name are fragile and can 404 if a similarly-named branch is later created. Prefertree/<encoded-branch-name>/...only after verifying the branch exists, or link to the export PR (which is more robust and provides better navigation context).
await github.rest.issues.createComment({
owner, repo, issue_number,
body: `📦 This proposal has been included in the **${term}** export.\n\n` +
`Export files: [\`programs/lfx-mentorship/${year}/${termDir}/\`](/${owner}/${repo}/tree/automation/lfx-export-${year}-${termDir}/programs/lfx-mentorship/${year}/${termDir}/)\n\n` +
`The export PR is awaiting review. Once merged and posted to LFX, this issue will be updated again.`,
});
.github/workflows/lfx-export.yml:416
- The export adds the
Exportedlabel and posts a comment before the auto-generated PR is reviewed/merged. If the export PR is later closed without merging (e.g., an issue with the data), each issue will still be labeledExportedand the proposers told it has been "included in the export". Consider deferring labelling/notification until the export PR actually merges (thelfx-export-merge-notify.ymlalready has the right hook for this).
for (const num of issues) {
const issue_number = parseInt(num);
// Add Exported label
await github.rest.issues.addLabels({
owner, repo, issue_number,
labels: ['Exported'],
});
// Comment linking to the export
await github.rest.issues.createComment({
owner, repo, issue_number,
body: `📦 This proposal has been included in the **${term}** export.\n\n` +
`Export files: [\`programs/lfx-mentorship/${year}/${termDir}/\`](/${owner}/${repo}/tree/automation/lfx-export-${year}-${termDir}/programs/lfx-mentorship/${year}/${termDir}/)\n\n` +
`The export PR is awaiting review. Once merged and posted to LFX, this issue will be updated again.`,
});
console.log(`Notified issue #${issue_number}`);
}
.github/workflows/lfx-proposal-validate.yml:325
- When a maintainer edits the issue body to fix validation errors, this workflow re-runs and re-adds
Awaiting Maintainer/Contribex ApprovalandAwaiting Mentor Confirmation. That's correct on the first pass, but subsequent edits after one of the two/approve//confirmsteps already happened look fine because of the!currentLabels.includes(...Approved)guard. However, edits afterValidation Passedwas previously set still bypass the awaiting check by readingcurrentLabelsfrom the event payload (which lags behind any labels the approvals workflow added through the API). If approvals/confirmations occurred and the issue is then edited, this could erroneously re-add awaiting labels. Consider re-fetching the issue's current labels (as the board-sync step does) rather than trustingcontext.payload.issue.labels.
const currentLabels = (context.payload.issue.labels || []).map(l => l.name);
const pass = errors.length === 0;
const addLabel = pass ? 'Validation Passed' : 'Validation Failed';
const dropLabel = pass ? 'Validation Failed' : 'Validation Passed';
if (currentLabels.includes(dropLabel)) {
try { await github.rest.issues.removeLabel({ owner, repo, issue_number, name: dropLabel }); } catch {}
}
if (!currentLabels.includes(addLabel)) {
await github.rest.issues.addLabels({ owner, repo, issue_number, labels: [addLabel] });
}
if (currentLabels.includes('Needs Validation')) {
try { await github.rest.issues.removeLabel({ owner, repo, issue_number, name: 'Needs Validation' }); } catch {}
}
// ── Over Quota label ──
const isOverQuota = warnings.some(w => w.startsWith('**Over quota:**'));
if (isOverQuota && !currentLabels.includes('Over Quota')) {
await github.rest.issues.addLabels({ owner, repo, issue_number, labels: ['Over Quota'] });
} else if (!isOverQuota && currentLabels.includes('Over Quota')) {
try { await github.rest.issues.removeLabel({ owner, repo, issue_number, name: 'Over Quota' }); } catch {}
}
// ── Set awaiting labels when validation passes ──
// These signal to maintainers and mentors that slash commands are expected.
if (pass) {
const awaiting = [];
if (!currentLabels.includes('Maintainer/Contribex Approved')
&& !currentLabels.includes('Awaiting Maintainer/Contribex Approval')) {
awaiting.push('Awaiting Maintainer/Contribex Approval');
}
if (!currentLabels.includes('Mentors Confirmed')
&& !currentLabels.includes('Awaiting Mentor Confirmation')) {
awaiting.push('Awaiting Mentor Confirmation');
}
if (awaiting.length) {
await github.rest.issues.addLabels({ owner, repo, issue_number, labels: awaiting });
}
}
.github/workflows/lfx-proposal-validate.yml:208
- The
quota = overrides[projectKey] || defaultQfalls through todefaultQwhen the override value is0, which is a valid configuration meaning "do not allow proposals for this project this term." Use a hasOwnProperty check (or nullish coalescing??) to honour a0override.
const quota = overrides[projectKey] || defaultQ;
.github/workflows/lfx-proposal-approvals.yml:160
projects.ymlis read by the approvals and export workflows but is not part of this PR (it's only generated bylandscape-projects-sync.yml). Until the sync runs successfully and a PR with the file is merged, every/approveinvocation will hit thetry/catchat line 157, log "projects.yml org lookup failed", and fall through to the CSV/approvers checks (still functional, but the.projecttier is silently disabled). Consider committing an initialprojects.ymlso the workflow tier-1 path works from day one.
if (project) {
try {
const projYaml = fs.readFileSync('programs/lfx-mentorship/automation/projects.yml', 'utf8');
// Find the entry for this project and extract repo_url + has_dot_project
const projRe = new RegExp(
`- name: ${project.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\n` +
`[\\s\\S]*?(?=\\n- name:|$)`, 'm'
);
const projMatch = projYaml.match(projRe);
if (projMatch) {
const block = projMatch[0];
const urlMatch = block.match(/repo_url:\s*(\S+)/);
if (urlMatch) {
const urlParts = urlMatch[1].match(/github\.com\/([^/]+)\//);
if (urlParts) ghOrg = urlParts[1];
}
hasDotProject = /has_dot_project:\s*true/i.test(block);
}
} catch (e) {
console.log(`projects.yml org lookup failed: ${e.message}`);
}
}
.github/workflows/lfx-proposal-approvals.yml:316
- When the approvals workflow fails to reach
cncf/foundation/project-maintainers.csv(network blip, GitHub outage, repo rename), thetry/catchswallows the error andauthorizedremainsfalse. The legitimate maintainer's/approveis then rejected with a misleading "not authorized" message. Consider distinguishing "lookup failed" from "lookup succeeded but user not present" and replying differently (e.g., asking the user to retry).
// 2. Check cncf/foundation project-maintainers.csv
if (!authorized && project) {
try {
const resp = await fetch(
'https://raw.githubusercontent.com/cncf/foundation/main/project-maintainers.csv'
);
if (resp.ok) {
const csv = await resp.text();
// CSV columns: Maturity, Project, Name, Company, GitHub, OWNERS
// Project column may include subgroup: "OpenTelemetry (Governance Committee)"
// Maturity is only on the first row of each group; subsequent rows are empty.
const normalProject = project.toLowerCase();
for (const line of csv.split('\n').slice(1)) {
if (!line.trim()) continue;
const cols = line.split(',');
const csvProject = (cols[1] || '').trim().toLowerCase();
const csvGh = (cols[4] || '').trim().toLowerCase();
if (csvGh === commenter.toLowerCase()
&& (csvProject.startsWith(normalProject) || csvProject.includes(normalProject))) {
authorized = true;
authSource = `project-maintainers.csv (${cols[1].trim()})`;
break;
}
}
}
} catch (e) {
console.log(`project-maintainers.csv check failed: ${e.message}`);
}
}
// 3. Check approvers.yml — per-project fallbacks then global_approvers
if (!authorized) {
try {
const raw = fs.readFileSync('programs/lfx-mentorship/automation/approvers.yml', 'utf8');
// 3a. Per-project fallback handles and teams
if (project) {
const projectKey = project.toLowerCase().replace(/\s+/g, '-');
const re = new RegExp(`^${projectKey.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}:`, 'im');
const parts = raw.split(re);
if (parts.length > 1) {
const section = parts[1].split(/^\S/m)[0];
// Check fallback_handles
const handleBlock = section.match(/fallback_handles:\s*\n((?:\s+-\s+\S+.*\n?)*)/);
if (handleBlock) {
const handles = [...handleBlock[1].matchAll(/-\s+(\S+)/g)].map(m => m[1].toLowerCase());
if (handles.includes(commenter.toLowerCase())) {
authorized = true;
authSource = 'approvers.yml (fallback handle)';
}
}
// Check fallback_teams
if (!authorized) {
const teamBlock = section.match(/fallback_teams:\s*\n((?:\s+-\s+\S+.*\n?)*)/);
if (teamBlock) {
const teams = [...teamBlock[1].matchAll(/-\s+(\S+)/g)].map(m => m[1]);
for (const team of teams) {
const [org, slug] = team.split('/');
if (!org || !slug) continue;
try {
const { data } = await github.rest.teams.getMembershipForUserInOrg({
org, team_slug: slug, username: commenter,
});
if (data.state === 'active') {
authorized = true;
authSource = `approvers.yml (team ${team})`;
break;
}
} catch {
// No access to check this team — skip
}
}
}
}
}
}
// 3b. Global approvers — can /approve for any project
if (!authorized) {
const globalBlock = raw.match(/global_approvers:\s*\n((?:\s+-\s+\S+.*\n?)*)/);
if (globalBlock) {
const globals = [...globalBlock[1].matchAll(/-\s+(\S+)/g)].map(m => m[1].toLowerCase());
if (globals.includes(commenter.toLowerCase())) {
authorized = true;
authSource = 'approvers.yml (global approver)';
}
}
}
} catch (e) {
console.log(`approvers.yml check failed: ${e.message}`);
}
}
if (!authorized) {
const orgNote = ghOrg
? `- A \`project-maintainers\` team member in [\`${ghOrg}/.project/maintainers.yaml\`](https://github.com/${ghOrg}/.project/blob/main/maintainers.yaml)\n`
: '';
await reply('❌',
`@${commenter} is not authorized to \`/approve\` proposals for **${project}**.\n\n` +
`Approvers must be:\n` +
orgNote +
`- Listed in [project-maintainers.csv](https://github.com/cncf/foundation/blob/main/project-maintainers.csv)\n` +
`- A fallback approver in \`approvers.yml\`\n\n` +
`If this is an error, contact a CNCF program admin.`);
return;
}
| // ── Detect slash command ── | ||
| // Scan every line — the command may not be the first line (e.g. | ||
| // someone writes context before their /approve). | ||
| let command = null; | ||
| for (const line of commentBody.split('\n')) { | ||
| const m = line.trim().match(/^\/(cncf-approve|approve|confirm)\b/); | ||
| if (m) { command = m[1]; break; } | ||
| } |
| Term to export (must match the dropdown value in the issue form, | ||
| e.g. "2026 Term 3 (Sep-Nov)") | ||
| required: true | ||
| type: choice | ||
| options: | ||
| - "2026 Term 3 (Sep-Nov)" | ||
| - "2027 Term 1 (Mar-May)" |
| try: | ||
| req = urllib.request.Request( | ||
| f"https://api.github.com/repos/{org}/.project", | ||
| method="HEAD", | ||
| ) | ||
| if gh_token: | ||
| req.add_header("Authorization", f"Bearer {gh_token}") | ||
| req.add_header("User-Agent", "cncf-mentoring-sync") | ||
| urllib.request.urlopen(req) | ||
| checked_orgs[org] = True | ||
| proj["has_dot_project"] = True | ||
| print(f" {org}/.project: found") | ||
| except urllib.error.HTTPError as e: | ||
| if e.code == 404: | ||
| checked_orgs[org] = False | ||
| else: | ||
| print(f" {org}/.project: HTTP {e.code}, skipping") | ||
| checked_orgs[org] = False | ||
| except Exception as e: | ||
| print(f" {org}/.project: error ({e}), skipping") | ||
| checked_orgs[org] = False |
| - name: Sync board status | ||
| if: always() | ||
| uses: actions/github-script@v7 | ||
| with: | ||
| github-token: ${{ secrets.PROJECT_TOKEN }} | ||
| script: | |
| on: | ||
| issues: | ||
| types: [opened, edited, reopened] | ||
|
|
||
| permissions: | ||
| issues: write | ||
| contents: read | ||
|
|
||
| jobs: | ||
| validate: | ||
| if: >- | ||
| contains(github.event.issue.labels.*.name, 'lfx mentorship') | ||
| && contains(github.event.issue.labels.*.name, 'Proposal') | ||
| runs-on: ubuntu-latest |
|
|
||
| // ── Helper regexes ── | ||
| const emailRe = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; | ||
| const urlRe = /^https?:\/\/\S+$/; |
| const issues = await github.paginate(github.rest.issues.listForRepo, { | ||
| owner, repo, state: 'open', | ||
| labels: 'lfx mentorship,Proposal,CNCF Approved', | ||
| per_page: 100, | ||
| }); | ||
|
|
||
| const termIssues = issues.filter(iss => { | ||
| const body = iss.body || ''; | ||
| return body.includes(`### Term\n\n${term}`); | ||
| }); | ||
|
|
||
| console.log(`Found ${termIssues.length} approved proposals for ${term}`); | ||
|
|
||
| if (termIssues.length === 0) { | ||
| core.setFailed(`No CNCF-approved proposals found for term: ${term}`); | ||
| return; | ||
| } |
| global_approvers: | ||
| - nate-double-u | ||
| - dkrook | ||
| - idvoretskyi | ||
| - RobertKielty | ||
| - thisisobate | ||
| - Swil78 | ||
| - mlehotskylf | ||
|
|
||
| kubernetes: | ||
| fallback_teams: | ||
| - kubernetes/sig-contribex | ||
|
|
||
| open-telemetry: | ||
| fallback_teams: | ||
| - open-telemetry/governance-committee | ||
| fallback_handles: | ||
| - maryliag # GC liaison for mentorship |
| - name: Validation Passed | ||
| color: 0E8A16 | ||
| description: Format validation passed | ||
| - name: Validation Failed | ||
| color: B60205 | ||
| description: Format validation failed | ||
|
|
||
| # CNCF admin gate | ||
| - name: Awaiting CNCF Admin Approval | ||
| color: FBCA04 | ||
| description: Needs /cncf-approve from admin | ||
| - name: CNCF Approved | ||
| color: 0E8A16 | ||
| description: CNCF admin approved | ||
|
|
||
| # Post-approval lifecycle | ||
| - name: Exported | ||
| color: C2E0C6 | ||
| description: Included in export, ready to post to LFX | ||
| - name: Posted to LFX | ||
| color: 0E8A16 | ||
| description: Program exported to LFX platform | ||
| - name: LFX Approved | ||
| color: 0E8A16 | ||
| description: LFX platform approved the program | ||
| - name: Mentors Registered | ||
| color: 0E8A16 | ||
| description: Mentors added on LFX platform | ||
| - name: Open for Applications | ||
| color: 0E8A16 | ||
| description: LFX applications are open |
| name: LFX Mentorship Program Proposal | ||
| description: Propose a mentorship program for an upcoming LFX Mentorship term. | ||
| title: "[CNCF LFX Proposal] <project> <program name>" | ||
| labels: ["lfx mentorship", "Proposal", "Needs Validation"] |
Resolves Proposal: streamline LFX Mentorship program intake #1883
Replace the manual PR-based LFX Mentorship proposal workflow with a GitHub Issue Form and GitHub Actions automation pipeline.
What this adds
Issue form (.github/ISSUE_TEMPLATE/lfx-program-proposal.yml)
Automation workflows
Configuration files
Documentation updates
Closes: #1883