Skip to content

feat: automated LFX Mentorship proposal intake system#1884

Draft
nate-double-u wants to merge 1 commit into
cncf:mainfrom
nate-double-u:lfx-intake-automation
Draft

feat: automated LFX Mentorship proposal intake system#1884
nate-double-u wants to merge 1 commit into
cncf:mainfrom
nate-double-u:lfx-intake-automation

Conversation

@nate-double-u
Copy link
Copy Markdown
Member

@nate-double-u nate-double-u commented May 12, 2026

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)

  • Structured form with dropdowns for CNCF project (synced from the landscape) and term, fields for program name, description, technologies, skills, mentors (pipe-separated), upstream issue URL, and application prerequisites.

Automation workflows

  • landscape-projects-sync: weekly sync of CNCF project list from the landscape API; also syncs term dropdowns from terms.yml
  • lfx-proposal-validate: format validation (char limits, mentor fields, quota checks), manages gate labels
  • lfx-proposal-approvals: slash commands (/approve, /confirm, /cncf-approve) with four-tier authorization (dot-project, maintainers CSV, fallback config, global approvers)
  • lfx-export: generates JSON, README, and CSV for approved proposals; creates PR, notifies issues, syncs board
  • lfx-proposal-board-sync: maps issue labels to Project v2 board status columns via GraphQL
  • lfx-export-merge-notify: comments on issues when export PR merges

Configuration files

  • automation/terms.yml: single source of truth for active terms
  • automation/approvers.yml: global approvers and per-project fallback authorization
  • automation/quotas.yml: per-project per-term proposal limits

Documentation updates

  • programs/lfx-mentorship/README.md: new 'How to propose a program' section with form fields, approval workflow, slash command reference
  • CONTRIBUTING.md: updated to point at issue form instead of PRs
  • mentors/README.md: updated submission steps and announcement channels
  • settings.yml: labels for the full proposal lifecycle

Closes: #1883

@nate-double-u
Copy link
Copy Markdown
Member Author

/cc @mlehotskylf

@nate-double-u nate-double-u marked this pull request as draft May 12, 2026 23:28
@nate-double-u
Copy link
Copy Markdown
Member Author

There's a live testable version of this here:
https://github.com/nate-double-u/mentoring

I opened a PR as it's easier to see what's going on this way.

Comment thread programs/lfx-mentorship/automation/ADMIN_GUIDE.md Outdated
Comment thread programs/lfx-mentorship/automation/ADMIN_GUIDE.md Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)**
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll update this when we move from draft to ready for review. For now I want to use dev env links.

Comment thread CONTRIBUTING.md
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)
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll update this when we move from draft to ready for review. For now I want to use dev env links.

Comment on lines +392 to +396
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';
Comment thread mentors/README.md
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.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll update this when we move from draft to ready for review. For now I want to use dev env links.

Comment on lines +30 to +46
// ── 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
Comment on lines +14 to +18
# <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
Comment on lines +207 to +209
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, '-');
Comment on lines +170 to +173
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>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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\n line endings. The substring check body.includes(### Term\n\n${term}) uses bare \n and 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\n with \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\n versus \n mismatch 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 existing parseIssueForm helper 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 key opentelemetry, but approvers.yml and quotas.yml use open-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 from projects.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, '-') produces opentelemetry for the dropdown value "OpenTelemetry", which will not match the open-telemetry override in quotas.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 (the OWNERS column 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 (or csv-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 monthMap only covers Mar/May/Jun/Aug/Sep/Nov. If a future term in terms.yml uses 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. Prefer tree/<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 Exported label 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 labeled Exported and the proposers told it has been "included in the export". Consider deferring labelling/notification until the export PR actually merges (the lfx-export-merge-notify.yml already 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 Approval and Awaiting Mentor Confirmation. That's correct on the first pass, but subsequent edits after one of the two /approve / /confirm steps already happened look fine because of the !currentLabels.includes(...Approved) guard. However, edits after Validation Passed was previously set still bypass the awaiting check by reading currentLabels from 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 trusting context.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] || defaultQ falls through to defaultQ when the override value is 0, which is a valid configuration meaning "do not allow proposals for this project this term." Use a hasOwnProperty check (or nullish coalescing ??) to honour a 0 override.
                const quota = overrides[projectKey] || defaultQ;

.github/workflows/lfx-proposal-approvals.yml:160

  • projects.yml is read by the approvals and export workflows but is not part of this PR (it's only generated by landscape-projects-sync.yml). Until the sync runs successfully and a PR with the file is merged, every /approve invocation will hit the try/catch at line 157, log "projects.yml org lookup failed", and fall through to the CSV/approvers checks (still functional, but the .project tier is silently disabled). Consider committing an initial projects.yml so 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), the try/catch swallows the error and authorized remains false. The legitimate maintainer's /approve is 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;
              }

Comment on lines +35 to +42
// ── 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; }
}
Comment on lines +8 to +14
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)"
Comment on lines +76 to +96
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
Comment on lines +360 to +365
- name: Sync board status
if: always()
uses: actions/github-script@v7
with:
github-token: ${{ secrets.PROJECT_TOKEN }}
script: |
Comment on lines +3 to +16
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+$/;
Comment on lines +82 to +98
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;
}
Comment on lines +20 to +37
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
Comment thread .github/settings.yml
Comment on lines +85 to +115
- 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"]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Development

Successfully merging this pull request may close these issues.

Proposal: streamline LFX Mentorship program intake

2 participants