Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
444 changes: 444 additions & 0 deletions .github/ISSUE_TEMPLATE/lfx-program-proposal.yml

Large diffs are not rendered by default.

63 changes: 58 additions & 5 deletions .github/settings.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,6 @@ labels:
color: 3B8924
- name: outreachy
color: 7fd811
- name: season of docs
color: f4af97
- name: summer of code
color: 00ffff
- name: season of docs
Expand All @@ -56,18 +54,73 @@ labels:
color: 0052CC
- name: '2026'
color: 0052CC
- name: '2027'
color: 0052CC
- name: discussion
color: af17d7
- name: administration
color: e3ead3
- name: Awaiting Mentor Confirmation
color: E99695
- name: Mentors Confirmed
color: D93F0B
color: 0E8A16
- name: Awaiting Maintainer/Contribex Approval
color: D4C5F9
- name: Maintainer/Contribex Approved
color: 5319E7
# one "blank" line
- name: blank

#
# LFX program proposal intake labels
#

# Intake marker
- name: Proposal
color: 1D76DB
description: LFX program proposal intake

# Validation gates
- name: Needs Validation
color: FBCA04
description: Awaiting format validation
- 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
Comment on lines +85 to +115
- name: Applications Closed
color: 5319E7
description: LFX applications closed

# Soft signal
- name: Over Quota
color: E4E669
description: Project exceeds per-term proposal quota


# end of labels
226 changes: 226 additions & 0 deletions .github/workflows/landscape-projects-sync.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
name: Sync CNCF Projects from Landscape

on:
schedule:
- cron: '0 6 * * 1' # every Monday at 06:00 UTC
workflow_dispatch: # manual trigger

permissions:
contents: write
pull-requests: write

jobs:
sync-projects:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Fetch and sync projects
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
python3 << 'PYEOF'
import json, re, sys, urllib.request
from datetime import date

import yaml # PyYAML is pre-installed on ubuntu-latest
import os

gh_token = os.environ.get("GITHUB_TOKEN", "")

# ── Fetch landscape.yml ──
url = "https://raw.githubusercontent.com/cncf/landscape/master/landscape.yml"
with urllib.request.urlopen(url) as resp:
data = yaml.safe_load(resp.read())

# ── Extract CNCF projects ──
valid = {"graduated", "incubating", "sandbox"}
projects = []
for cat in data.get("landscape", []):
for sub in cat.get("subcategories", []):
for item in (sub.get("items") or []):
level = item.get("project")
if level not in valid:
continue
extra = item.get("extra", {})
slug = extra.get("lfx_slug") or item["name"]
slug = re.sub(r"[^a-z0-9-]", "-", slug.lower())
slug = re.sub(r"-+", "-", slug).strip("-")
repo_url = item.get("repo_url", "")
projects.append({
"name": item["name"],
"slug": slug,
"maturity": level,
"homepage": item.get("homepage_url", ""),
"logo": item.get("logo", ""),
"repo_url": repo_url,
})

projects.sort(key=lambda p: p["name"].lower())
print(f"Found {len(projects)} CNCF projects")

# ── Check which orgs have a .project repo ──
checked_orgs = {} # org -> bool (cache to avoid duplicate checks)
for proj in projects:
repo_url = proj.get("repo_url", "")
m = re.match(r"https://github\.com/([^/]+)/", repo_url)
if not m:
continue
org = m.group(1)
if org in checked_orgs:
if checked_orgs[org]:
proj["has_dot_project"] = True
continue
# Check if {org}/.project exists via GitHub API
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 +76 to +96

dot_count = sum(1 for o, v in checked_orgs.items() if v)
print(f"Checked {len(checked_orgs)} orgs, {dot_count} have .project repos")

# ── Write projects.yml ──
header = (
f"# Auto-generated from cncf/landscape — DO NOT EDIT MANUALLY.\n"
f"# Last updated: {date.today().isoformat()}\n"
f"# Total projects: {len(projects)}\n\n"
)
with open("programs/lfx-mentorship/automation/projects.yml", "w") as f:
f.write(header)
yaml.dump(projects, f, default_flow_style=False, sort_keys=False)

# ── Update issue form dropdown ──
form_path = ".github/ISSUE_TEMPLATE/lfx-program-proposal.yml"
with open(form_path) as f:
form_text = f.read()

new_names = [p["name"] for p in projects]

# Rewrite the options block under id: cncf_project
lines = form_text.split("\n")
new_lines = []
in_options = False
options_inserted = False

for i, line in enumerate(lines):
# Detect the cncf_project options: line
if (not options_inserted
and re.match(r"^\s+options:\s*$", line)):
context = "\n".join(lines[max(0, i - 10):i])
if "id: cncf_project" in context:
indent = re.match(r"^(\s+)", line).group(1) + " "
new_lines.append(line)
for name in new_names:
new_lines.append(f'{indent}- "{name}"')
in_options = True
options_inserted = True
continue

# Skip old option lines
if in_options:
if re.match(r"^\s+-\s", line):
continue
else:
in_options = False

new_lines.append(line)

with open(form_path, "w") as f:
f.write("\n".join(new_lines))

# ── Sync term dropdowns from terms.yml ──
# Single source of truth: programs/lfx-mentorship/automation/terms.yml
# Targets: issue form (id: term) and export workflow (workflow_dispatch input)
terms_path = "programs/lfx-mentorship/automation/terms.yml"
with open(terms_path) as f:
terms_data = yaml.safe_load(f)
term_list = terms_data.get("terms", [])

def rewrite_options_block(file_path, context_marker):
"""Rewrite an options: block whose preceding lines contain context_marker."""
with open(file_path) as f:
text = f.read()
lines = text.split("\n")
out = []
in_opts = False
done = False
for i, line in enumerate(lines):
if (not done and re.match(r"^\s+options:\s*$", line)):
ctx = "\n".join(lines[max(0, i - 10):i])
if context_marker in ctx:
indent = re.match(r"^(\s+)", line).group(1) + " "
out.append(line)
for t in term_list:
out.append(f'{indent}- "{t}"')
in_opts = True
done = True
continue
if in_opts:
if re.match(r"^\s+-\s", line):
continue
else:
in_opts = False
out.append(line)
with open(file_path, "w") as f:
f.write("\n".join(out))

rewrite_options_block(form_path, "id: term")
rewrite_options_block(
".github/workflows/lfx-export.yml",
"term:" # the workflow_dispatch input name
)
print(f"Term dropdowns synced: {term_list}")

print("Files updated successfully")
PYEOF

- name: Check for changes
id: check
run: |
if git diff --quiet; then
echo "changed=false" >> "$GITHUB_OUTPUT"
else
echo "changed=true" >> "$GITHUB_OUTPUT"
fi

- name: Create Pull Request
if: steps.check.outputs.changed == 'true'
uses: peter-evans/create-pull-request@v7
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: |
chore: sync CNCF project list from landscape

Auto-generated by landscape-projects-sync workflow.

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Assisted-by: Copilot <223556219+Copilot@users.noreply.github.com>
branch: automation/landscape-sync
title: "chore: sync CNCF project list from landscape"
body: |
Auto-generated update to the CNCF Project dropdown in the
LFX program proposal issue form and the projects.yml config.

Source: [`cncf/landscape/landscape.yml`](https://github.com/cncf/landscape/blob/master/landscape.yml)

Please review the project list diff and merge if correct.
labels: administration
44 changes: 44 additions & 0 deletions .github/workflows/lfx-export-merge-notify.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: LFX Export Merge Notify

on:
pull_request:
types: [closed]
branches: [main]

permissions:
issues: write

jobs:
notify:
if: >-
github.event.pull_request.merged == true
&& startsWith(github.event.pull_request.head.ref, 'automation/lfx-export-')
runs-on: ubuntu-latest
steps:
- name: Notify exported issues
uses: actions/github-script@v7
with:
script: |
const { owner, repo } = context.repo;
const pr = context.payload.pull_request;
const prNumber = pr.number;
const prUrl = pr.html_url;

// Extract issue numbers from PR body (format: "- #123 — ...")
const issueNums = [...(pr.body || '').matchAll(/-\s+#(\d+)\s+—/g)]
.map(m => parseInt(m[1]));

if (issueNums.length === 0) {
console.log('No linked issues found in PR body.');
return;
}

for (const issue_number of issueNums) {
await github.rest.issues.createComment({
owner, repo, issue_number,
body: `✅ Export PR #${prNumber} has been merged.\n\n` +
`The program files are now on \`main\`. Next step: post to LFX platform.\n\n` +
`This issue will be updated when the program is live on LFX.`,
});
console.log(`Notified #${issue_number} about merge of PR #${prNumber}`);
}
Loading
Loading