diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..fdcb90d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: true +contact_links: + - name: CNCF Sandbox onboarding process + url: https://github.com/cncf/sandbox/issues/487 + about: Reference onboarding checklist this repo is based on. diff --git a/.github/ISSUE_TEMPLATE/onboarding-item.md b/.github/ISSUE_TEMPLATE/onboarding-item.md new file mode 100644 index 0000000..275ddfc --- /dev/null +++ b/.github/ISSUE_TEMPLATE/onboarding-item.md @@ -0,0 +1,33 @@ +--- +name: Onboarding item +description: Track one item of the CNCF sandbox onboarding checklist +title: "[Onboarding] " +labels: + - onboarding-item +body: + - type: markdown + attributes: + value: | + Use this template for a custom onboarding item. For standard items, run + `./scripts/bootstrap-issues.sh` instead — it creates all predefined items automatically. + + **Workflow** + 1. Open the matching section in `ONBOARDING.md` (instructions and issue link are on the checklist line). + 2. Add evidence and open a pull request that only changes `ONBOARDING.md`. + 3. Include `Closes #ISSUE_NUMBER` in the PR description (replace with this issue's number). + 4. When the PR opens, the box is checked and linked automatically. When it merges, this issue closes. + + - type: textarea + attributes: + label: Work to complete + description: Describe what needs to be done for this onboarding item. + validations: + required: true + + - type: input + attributes: + label: ONBOARDING.md section + description: Which section (slug) in ONBOARDING.md should be updated? + placeholder: maintainer-list-pr + validations: + required: false diff --git a/.github/onboarding-items.json b/.github/onboarding-items.json new file mode 100644 index 0000000..a5c7b89 --- /dev/null +++ b/.github/onboarding-items.json @@ -0,0 +1,277 @@ +{ + "source_issue": "https://github.com/cncf/sandbox/issues/487", + "categories": [ + { + "id": "required", + "heading": "Required before CNCF staff can proceed", + "intro": "A signed Project Contribution Agreement and trademark transfer are required **before** CNCF staff can complete the rest of onboarding. Other items can be worked on in parallel.", + "items": [ + { + "slug": "ip-policy-review", + "title": "Review and understand the CNCF IP Policy", + "critical": true, + "guide": "Review the [CNCF IP Policy](https://github.com/cncf/foundation/blob/main/charter.md#11-ip-policy). Ensure the project uses a CNCF-compatible license — inbound projects must use **Apache 2.0**. (Dependency licenses are tracked separately in `license-scan-import`.)", + "evidence_hint": "_Confirm license is Apache 2.0 and link to the LICENSE file._" + }, + { + "slug": "license-policy-review", + "title": "Review and understand the CNCF Third Party License Policy", + "critical": true, + "guide": "Review the [CNCF Allowlist License Policy](https://github.com/cncf/foundation/blob/main/policies-guidance/allowed-third-party-license-policy.md). This governs licenses used by third-party dependencies. CNCF FOSSA or CNCF Snyk can check compliance — pick one (tracked in `license-scan-import`).", + "evidence_hint": "_Note which scanning service you'll use (FOSSA or Snyk)._" + }, + { + "slug": "trademark-guidelines-review", + "title": "Review and understand the LF trademark guidelines", + "critical": true, + "guide": "Review the [Linux Foundation trademark guidelines](https://www.linuxfoundation.org/trademark-usage/). Let the TOC know if you plan to change your project name.", + "evidence_hint": "_Confirm review complete; note any planned name change (or N/A)._" + }, + { + "slug": "trademark-transfer", + "title": "Transfer trademark and logo assets to the Linux Foundation", + "critical": true, + "guide": "Transfer any existing trademark and logo assets to the Linux Foundation via the Project Contribution Agreement. CNCF staff sends this document to the contact emails listed in your Sandbox application.", + "evidence_hint": "_Date Contribution Agreement was signed, or status if pending._" + } + ] + }, + { + "id": "review-documents", + "heading": "Review and understand other documents", + "intro": "", + "items": [ + { + "slug": "technical-leadership-principles", + "title": "Review the Technical Leadership Principles", + "guide": "Read the [Technical Leadership Principles](https://contribute.cncf.io/maintainers/community/leadership-principles/), which outline expected behavior for maintainers in leadership roles.", + "evidence_hint": "_Confirm review complete._" + }, + { + "slug": "project-proposal-process", + "title": "Review the project proposal process and requirements", + "guide": "Read the [CNCF project proposal process and requirements](https://github.com/cncf/toc/blob/main/process/README.md).", + "evidence_hint": "_Confirm review complete._" + }, + { + "slug": "cncf-services-review", + "title": "Review services available for your project at the CNCF", + "guide": "Read about [services available to CNCF projects](https://contribute.cncf.io/resources/project-services/).", + "evidence_hint": "_Confirm review complete._" + }, + { + "slug": "online-program-guidelines", + "title": "Review the online program guidelines", + "guide": "Read the CNCF online program guidelines (webinars, meetups, events) provided by CNCF staff.", + "evidence_hint": "_Confirm review complete._" + }, + { + "slug": "telemetry-policy-review", + "title": "Review the telemetry data collection and usage policy", + "guide": "Read the CNCF telemetry data collection and usage policy.", + "evidence_hint": "_Confirm review complete._" + }, + { + "slug": "cncf-staff-office-hours", + "title": "Optional: book time with CNCF staff", + "optional": true, + "guide": "Optional: book time with CNCF staff to walk through available resources, work through onboarding tasks together, or ask questions.", + "evidence_hint": "_Meeting date, or N/A if not needed._" + } + ] + }, + { + "id": "contribute-transfer", + "heading": "Contribute and transfer other materials", + "intro": "", + "items": [ + { + "slug": "neutral-github-org", + "title": "Move the project to its own separate neutral GitHub organization", + "guide": "This makes the project transferable to the CNCF's GitHub Enterprise account. If it's already in another GitHub Enterprise account, remove it from there first.", + "evidence_hint": "_Neutral GitHub org name/URL._" + }, + { + "slug": "ghe-invite-accepted", + "title": "Accept the invite to join the CNCF GitHub Enterprise account", + "guide": "CNCF will add `thelinuxfoundation` as an organization owner to ensure neutral hosting.", + "evidence_hint": "_Date invite accepted._" + }, + { + "slug": "slack-migration", + "title": "Migrate Slack channels to the Kubernetes or CNCF Slack workspace", + "guide": "CNCF staff can help. This improves discoverability, lets CNCF enforce its Code of Conduct, and enables unlimited message retention. If you already have a large Slack/Discord community, CNCF may instead link to it from a pointer channel in the CNCF Slack workspace.", + "evidence_hint": "_Migration status or link to pointer channel._" + }, + { + "slug": "maintainers-circle-slack", + "title": "Join the #maintainers-circle Slack channel", + "guide": "Join `#maintainers-circle` on CNCF Slack to find and share knowledge with other project teams.", + "evidence_hint": "_Confirm joined._" + }, + { + "slug": "domain-transfer", + "title": "Transfer project domain(s) to the CNCF", + "guide": "If your project has its own domain(s), transfer them to the CNCF. LF Contact: `projects@cncf.io`. Project Selection: CNCF.", + "evidence_hint": "_Domain(s) transferred, or N/A if none._" + }, + { + "slug": "artwork-pr", + "title": "Submit a pull request with your artwork", + "guide": "Submit project artwork to [cncf/artwork](https://github.com/cncf/artwork). If you don't have artwork, CNCF can help design some.", + "evidence_hint": "_Link to the artwork PR._" + }, + { + "slug": "analytics-transfer", + "title": "Transfer website analytics to the CNCF", + "guide": "If you use Google Analytics, add `projects@cncf.io` as an admin of your existing account so it can be moved to a CNCF-managed account. If you don't have GA, note your current analytics setup instead.", + "evidence_hint": "_Analytics status, or N/A._" + } + ] + }, + { + "id": "project-details", + "heading": "Update and document project details", + "intro": "", + "items": [ + { + "slug": "maintainer-list-pr", + "title": "Create a maintainer list and add it to the aggregated CNCF maintainer list", + "guide": "Create/verify `MAINTAINERS.md` and open a PR against the aggregated CNCF maintainer list.", + "evidence_hint": "_Link to MAINTAINERS.md and the PR against the aggregated list._" + }, + { + "slug": "maintainer-emails", + "title": "Provide maintainer emails for mailing list and Service Desk access", + "guide": "Email maintainer addresses to `project-onboarding@cncf.io` (not shared publicly). Also link each maintainer's GitHub ID with their LF profile.", + "evidence_hint": "_Date emailed; confirm LF profile linking._" + }, + { + "slug": "dco-enabled", + "title": "Ensure the DCO app is enabled for all project GitHub repositories", + "guide": "Enable the [DCO GitHub App](https://github.com/apps/dco) on every repo in scope. A CLA may be used in addition to the DCO, not instead of it.", + "evidence_hint": "_List repos with DCO enabled._" + }, + { + "slug": "coc-in-readme", + "title": "Reference the CNCF Code of Conduct in README.md", + "guide": "Explicitly reference the CNCF Code of Conduct (or your adopted version) in the project's `README.md` on GitHub.", + "evidence_hint": "_Link to the README section._" + }, + { + "slug": "lf-footer", + "title": "Add the LF footer to your website (or README if no website)", + "guide": "Add the Linux Foundation footer to your website per [LF branding guidelines](https://github.com/cncf/foundation/blob/main/website-guidelines.md). If you don't have a dedicated website, adopt these guidelines for `README.md` instead.", + "evidence_hint": "_Link to footer on the site or README section._" + }, + { + "slug": "governance-doc", + "title": "Start a GOVERNANCE.md documenting open governance", + "guide": "Document written, open governance in a `GOVERNANCE.md` file at the root of your repo.", + "evidence_hint": "_Link to GOVERNANCE.md._" + }, + { + "slug": "security-doc", + "title": "Start a SECURITY.md security policy", + "guide": "Document a security policy in a `SECURITY.md` file at the root of your repo. See [CNCF security guidelines](https://contribute.cncf.io/maintainers/security/security-guidelines/#3-securitymd).", + "evidence_hint": "_Link to SECURITY.md._" + }, + { + "slug": "openssf-badge", + "title": "Start an OpenSSF Best Practices Badge", + "guide": "Register and begin working toward an [OpenSSF Best Practices Badge](https://www.bestpractices.dev/).", + "evidence_hint": "_Link to the badge page/status._" + }, + { + "slug": "license-scan-import", + "title": "Import all project repos into a license scanning service", + "guide": "Import all repos in scope into CNCF FOSSA or CNCF Snyk (see `license-policy-review`).", + "evidence_hint": "_Service used and confirmation all repos are imported._" + } + ] + }, + { + "id": "cncf-staff", + "heading": "CNCF staff tasks (track and follow up)", + "intro": "These are completed by CNCF staff, not the project. Track them here so you know what to follow up on; check the box once staff confirms completion.", + "items": [ + { + "slug": "devstats", + "title": "CNCF staff: add the project to DevStats", + "staff": true, + "guide": "CNCF staff adds the project to [DevStats](https://all.devstats.cncf.io/).", + "evidence_hint": "_Link to the project's DevStats dashboard once available._" + }, + { + "slug": "clomonitor", + "title": "CNCF staff: add the project to CLOMonitor", + "staff": true, + "guide": "CNCF staff adds the project to [CLOMonitor](https://clomonitor.io/).", + "evidence_hint": "_Link to the project's CLOMonitor report card._" + }, + { + "slug": "lfx-insights-onboarding", + "title": "CNCF staff: add the project to LFX Insights", + "staff": true, + "guide": "CNCF staff adds the project to [LFX Insights](https://insights.linuxfoundation.org/).", + "evidence_hint": "_Link to LFX Insights for the project._" + }, + { + "slug": "lfx-pcc-activation", + "title": "CNCF staff: activate the project in the LFX Project Control Center", + "staff": true, + "guide": "CNCF staff activates the project in the LFX Project Control Center (PCC).", + "evidence_hint": "_Confirmation date once activated._" + }, + { + "slug": "landscape-listing-onboarding", + "title": "CNCF staff: add the project to the Cloud Native Landscape", + "staff": true, + "guide": "After PCC activation, CNCF staff adds the project to the [Cloud Native Landscape](https://landscape.cncf.io/), including the `lfx_slug` in the landscape configuration file.", + "evidence_hint": "_Link to the landscape PR/entry._" + }, + { + "slug": "license-scanner-team", + "title": "CNCF staff: add the maintainers team to the license scanner", + "staff": true, + "guide": "CNCF staff adds the maintainers team to the chosen license scanner service (FOSSA or Snyk).", + "evidence_hint": "_Confirmation maintainers have scanner access._" + }, + { + "slug": "groupsio-list", + "title": "CNCF staff: create a groups.io project maintainer list in PCC", + "staff": true, + "guide": "CNCF staff creates a groups.io maintainer list for the project in the LFX Project Control Center.", + "evidence_hint": "_Groups.io list name/link._" + }, + { + "slug": "groupsio-maintainers-list", + "title": "CNCF staff: add the project's groups.io list to maintainers@cncf.io", + "staff": true, + "guide": "CNCF staff adds the project's groups.io maintainer list to `maintainers@cncf.io`.", + "evidence_hint": "_Confirmation complete._" + }, + { + "slug": "welcome-email", + "title": "CNCF staff: send a welcome email confirming maintainer list access", + "staff": true, + "guide": "CNCF staff sends a welcome email confirming maintainer mailing list and Service Desk access.", + "evidence_hint": "_Date welcome email received._" + } + ] + }, + { + "id": "final", + "heading": "Final review", + "intro": "", + "items": [ + { + "slug": "onboarding-complete", + "title": "Final review: confirm all items complete and request sign-off", + "guide": "When every item above is complete (or intentionally N/A), post a status update on your CNCF onboarding issue and ask CNCF staff to confirm and close it. Run `./scripts/generate-status-report.sh` to produce a paste-ready update, or `./scripts/generate-status-report.sh --post` to post it directly as a comment.", + "evidence_hint": "_Link to the comment confirming onboarding complete, and the closed onboarding issue._" + } + ] + } + ] +} diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..ebd7f56 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,17 @@ +## Summary + + + +## Checklist items + + + +Closes # + +## ONBOARDING.md sections updated + + + +- `section-slug` diff --git a/.github/workflows/sync-checklist-pr.yml b/.github/workflows/sync-checklist-pr.yml new file mode 100644 index 0000000..a4db2dd --- /dev/null +++ b/.github/workflows/sync-checklist-pr.yml @@ -0,0 +1,67 @@ +name: Sync checklist PR links + +on: + pull_request: + types: [opened, edited, synchronize, reopened] + +permissions: + contents: write + pull-requests: read + +jobs: + link-pr-on-checklist: + runs-on: ubuntu-latest + # Skip bot PRs and re-entry after this workflow pushes to the PR branch. + if: > + github.event.pull_request.user.login != 'github-actions[bot]' && + github.actor != 'github-actions[bot]' + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.ref }} + repository: ${{ github.event.pull_request.head.repo.full_name }} + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Update Issue links to PR links and check boxes + id: link + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + PR_BODY: ${{ github.event.pull_request.body }} + REPO: ${{ github.repository }} + run: | + set -euo pipefail + issues="$(python3 -c " + import os, re + body = os.environ.get('PR_BODY') or '' + pat = re.compile(r'\\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\\s+#(\\d+)', re.I) + nums = sorted(set(pat.findall(body))) + print(' '.join(nums)) + ")" + + if [ -z "${issues}" ]; then + echo "No closing keywords found in PR body; skipping." + echo "changed=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + python3 scripts/checklist_tracking.py link-pr \ + --repo "${REPO}" \ + --pr-number "${PR_NUMBER}" \ + --issue-numbers ${issues} + + if git diff --quiet ONBOARDING.md README.md; then + echo "No checklist tracking changes." + echo "changed=false" >> "$GITHUB_OUTPUT" + else + echo "changed=true" >> "$GITHUB_OUTPUT" + fi + + - name: Commit checklist PR link updates + if: steps.link.outputs.changed == 'true' + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add ONBOARDING.md README.md + git commit -m "docs: link checklist items to PR #${{ github.event.pull_request.number }} [skip ci]" + git push diff --git a/.github/workflows/sync-checklist.yml b/.github/workflows/sync-checklist.yml new file mode 100644 index 0000000..9bf4127 --- /dev/null +++ b/.github/workflows/sync-checklist.yml @@ -0,0 +1,133 @@ +name: Sync checklist on issue close + +on: + issues: + types: [closed] + +permissions: + contents: write + +# Multiple issues can close at once (bulk-close in the UI, a loop of +# `gh issue close`, a board automation, etc.), each firing its own run of +# this workflow. Without serializing, concurrent runs can race pushing to +# main and silently drop a checkbox update. Queue runs instead of canceling +# them so every closed issue's update (and CNCF comment) lands. +concurrency: + group: sync-checklist-${{ github.repository }} + cancel-in-progress: false + +jobs: + sync-checklist: + runs-on: ubuntu-latest + # Fallback path for issues closed without a linked PR (or if the PR-link + # step didn't run). Ignore bot-authored issues to avoid loops. + if: > + contains(github.event.issue.labels.*.name, 'onboarding-item') && + github.event.issue.user.type != 'Bot' + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: main + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Find checklist slug from labels + id: slug + run: | + slug=$(echo '${{ toJson(github.event.issue.labels.*.name) }}' | jq -r '.[] | select(startswith("checklist:")) | sub("checklist:"; "")') + if [ -z "$slug" ]; then + echo "No checklist slug label found; skipping." + echo "found=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + echo "slug=$slug" >> "$GITHUB_OUTPUT" + echo "found=true" >> "$GITHUB_OUTPUT" + echo "Syncing checklist item: $slug (issue #${{ github.event.issue.number }})" + + - name: Mark checkbox complete and update progress + if: steps.slug.outputs.found == 'true' + id: update + env: + CHECKLIST_SLUG: ${{ steps.slug.outputs.slug }} + run: | + python3 scripts/checklist_tracking.py complete-checkbox --slug "${CHECKLIST_SLUG}" + + if git diff --quiet ONBOARDING.md README.md; then + echo "changed=false" >> "$GITHUB_OUTPUT" + else + echo "changed=true" >> "$GITHUB_OUTPUT" + fi + + # Commit directly to main. Do not open a follow-up bot PR — that + # re-triggers pull_request workflows and produces approval/noise + # cascades. Commits made with GITHUB_TOKEN do not start new workflow runs. + - name: Commit checklist update to main + if: steps.update.outputs.changed == 'true' + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add ONBOARDING.md README.md + git commit -m "docs: mark onboarding item ${{ steps.slug.outputs.slug }} complete" + # Belt-and-suspenders alongside the concurrency group above: rebase + # onto main before pushing in case anything else advanced it. + for attempt in 1 2 3; do + git pull --rebase origin main && git push origin HEAD:main && break + echo "Push attempt ${attempt} failed; retrying..." + sleep $((attempt * 3)) + done + + # Post per-item progress to the external CNCF onboarding issue as items + # complete, rather than only at the very end. Requires: + # 1. .github/project-metadata.json to have onboarding_issue_url set + # (written by ./scripts/bootstrap-issues.sh) + # 2. A CNCF_SANDBOX_TOKEN repo secret (a GitHub PAT with `public_repo` + # scope is enough since cncf/sandbox is public) — GITHUB_TOKEN + # cannot write to a different repository. + - name: Determine CNCF onboarding issue + if: steps.slug.outputs.found == 'true' + id: cncf_issue + run: | + if [ ! -f .github/project-metadata.json ]; then + echo "No .github/project-metadata.json; skipping CNCF comment." + echo "found=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + url="$(jq -r '.onboarding_issue_url // ""' .github/project-metadata.json)" + if [ -z "${url}" ]; then + echo "onboarding_issue_url not set; skipping CNCF comment." + echo "found=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + repo="$(echo "${url}" | sed -E 's#https://github.com/([^/]+/[^/]+)/issues/([0-9]+).*#\1#')" + number="$(echo "${url}" | sed -E 's#https://github.com/([^/]+/[^/]+)/issues/([0-9]+).*#\2#')" + if [ -z "${repo}" ] || [ -z "${number}" ]; then + echo "Could not parse onboarding_issue_url: ${url}" + echo "found=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + echo "repo=${repo}" >> "$GITHUB_OUTPUT" + echo "number=${number}" >> "$GITHUB_OUTPUT" + echo "found=true" >> "$GITHUB_OUTPUT" + + - name: Generate item comment + if: steps.slug.outputs.found == 'true' && steps.cncf_issue.outputs.found == 'true' + run: | + python3 scripts/generate_item_comment.py \ + --slug "${{ steps.slug.outputs.slug }}" \ + --local-repo "${{ github.repository }}" \ + --local-issue-number "${{ github.event.issue.number }}" \ + --output /tmp/item-comment.md + + - name: Post progress comment to CNCF onboarding issue + if: steps.slug.outputs.found == 'true' && steps.cncf_issue.outputs.found == 'true' + env: + GH_TOKEN: ${{ secrets.CNCF_SANDBOX_TOKEN }} + run: | + if [ -z "${GH_TOKEN}" ]; then + echo "CNCF_SANDBOX_TOKEN secret not set; skipping post. See CONTRIBUTING.md." + exit 0 + fi + gh issue comment "${{ steps.cncf_issue.outputs.number }}" \ + -R "${{ steps.cncf_issue.outputs.repo }}" \ + --body-file /tmp/item-comment.md + echo "Posted progress update to ${{ steps.cncf_issue.outputs.repo }}#${{ steps.cncf_issue.outputs.number }}" diff --git a/.github/workflows/validate-pr-closes-issues.yml b/.github/workflows/validate-pr-closes-issues.yml new file mode 100644 index 0000000..321f3e6 --- /dev/null +++ b/.github/workflows/validate-pr-closes-issues.yml @@ -0,0 +1,58 @@ +name: Validate PR closes checklist issues + +on: + pull_request: + types: [opened, edited, reopened] + +permissions: + issues: write + pull-requests: read + +jobs: + comment-missing-closes: + runs-on: ubuntu-latest + if: github.event.pull_request.user.login != 'github-actions[bot]' + steps: + - name: Check for closing keywords + uses: actions/github-script@v7 + with: + script: | + const body = context.payload.pull_request.body || ''; + const closesPattern = /\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+#(\d+)/gi; + const matches = [...body.matchAll(closesPattern)]; + + if (matches.length > 0) { + core.info(`PR references issues: ${matches.map(m => '#' + m[1]).join(', ')}`); + return; + } + + const marker = ''; + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + }); + if (comments.some(c => c.body && c.body.includes(marker))) { + core.info('Reminder comment already present; skipping.'); + return; + } + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + body: [ + marker, + '### Checklist reminder', + '', + 'This PR does not reference any checklist issues with closing keywords.', + '', + 'To automatically check the box and link this PR, add one of the following to the PR description:', + '', + '```', + 'Closes #123', + '```', + '', + 'See [CONTRIBUTING.md](CONTRIBUTING.md) for the full workflow.' + ].join('\n') + }); diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..de2611d --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +*.tmp +.env +.env.* +.vscode/ +ONBOARDING-STATUS.md +__pycache__/ +scripts/__pycache__/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..5ad7753 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,114 @@ +# Contributing to Onboarding + +This repository tracks [CNCF Sandbox onboarding](https://github.com/cncf/sandbox/issues/487) after your project's TOC acceptance vote. + +## Workflow + +### 1. Bootstrap checklist issues + +After creating your repository from this template, run: + +```bash +./scripts/bootstrap-issues.sh +``` + +This prompts for basic info (project name, your CNCF onboarding issue URL, acceptance date), creates one GitHub issue per checklist item, and writes issue links onto the matching checklist lines in **[ONBOARDING.md](ONBOARDING.md)**. + +### 2. Work on checklist items (ONBOARDING.md only) + +Open **[ONBOARDING.md](ONBOARDING.md)** and stay in that file for all onboarding work. + +Each section includes: + +- Instructions (guide) for what to complete, taken from the CNCF onboarding checklist +- A checklist line with `(Issue: [#N](…))` (after bootstrap) +- An **Evidence** area for links/notes proving completion + +Pick an issue, edit the relevant section(s) in `ONBOARDING.md`, and open a pull request with `Closes #N`. Do **not** manually check the checklist box — automation marks it `[x]` and updates `(PR: …)` in **both** `ONBOARDING.md` and [README.md](README.md). **Do not edit README** for evidence or tracking. + +Progress and the checklist dashboard live in README only; they update automatically. + +### 3. Close issues via pull requests + +Reference the checklist issue in your PR description: + +```markdown +Closes #12 +``` + +Supported keywords: `Closes`, `Fixes`, `Resolves` (case insensitive). + +### 4. Checklist sync + +The [sync-checklist-pr workflow](.github/workflows/sync-checklist-pr.yml) runs when you open or update a PR with `Closes #N`. On your PR branch it: + +1. Replaces `(Issue: …)` with `(PR: …)` +2. Checks the box (`[x]`) in `ONBOARDING.md` +3. Mirrors those lines and refreshes progress in README + +The [sync-checklist workflow](.github/workflows/sync-checklist.yml) runs when the local issue closes (i.e. the PR merged, or the issue was closed directly for a staff-tracked item). It: + +1. Checks the box on `main` directly if it wasn't already (no follow-up bot PR) +2. **Posts a comment on your CNCF onboarding issue** with just that item — checkbox, evidence, and a link back to the tracking issue/PR + +This gives CNCF live, item-by-item visibility instead of one report at the end. + +### 5. CNCF staff items + +Items labeled `[CNCF Staff]` are completed by CNCF, not by your project (DevStats, CLOMonitor, LFX Insights, PCC activation, landscape listing, etc.). Track them here so you know what to follow up on, and check the box once CNCF confirms — closing that issue also posts a progress comment. + +### 6. One-time setup for automatic CNCF comments + +The per-item comment step needs two things, both set once: + +1. **`onboarding_issue_url` in `.github/project-metadata.json`** — set automatically by `./scripts/bootstrap-issues.sh` (or edit the file directly). +2. **A `CNCF_SANDBOX_TOKEN` repository secret** — a GitHub personal access token with `public_repo` scope is enough, since `cncf/sandbox` is public. `GITHUB_TOKEN` cannot post to a different repository, so this PAT is required. Add it under **Settings → Secrets and variables → Actions**. + +If either is missing, the workflow logs a message and skips the comment — it does not fail the run. + +### 7. Full status report (optional) + +For an initial full update, or to re-sync if some automatic comments failed, generate a report matching the original CNCF checklist structure: + +```bash +# Write ONBOARDING-STATUS.md (gitignored, generated) +./scripts/generate-status-report.sh + +# Or post it directly as a comment on your CNCF onboarding issue +./scripts/generate-status-report.sh --post +``` + +## Branch naming + +Use descriptive branch names: + +- `onboarding/maintainers-file` +- `onboarding/security-policy` +- `onboarding/artwork-pr` + +## Pull request template + +Pull requests should include: + +- `Closes #N` for each completed item +- Which section(s) in `ONBOARDING.md` were updated +- A brief summary of changes + +## Labels + +Issues created by the bootstrap script use these labels: + +| Label | Meaning | +| --- | --- | +| `onboarding-item` | Part of the onboarding checklist | +| `checklist:` | Maps to a specific checkbox in ONBOARDING.md | +| `critical` | Required before CNCF staff can proceed | +| `optional` | Optional item | +| `cncf-staff` | Completed by CNCF staff; track for follow-up | +| `category:*` | Onboarding section grouping | + +## References + +- [CNCF Sandbox onboarding issue this template is based on](https://github.com/cncf/sandbox/issues/487) +- [CNCF Sandbox repository](https://github.com/cncf/sandbox) +- [CNCF project services](https://contribute.cncf.io/resources/project-services/) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ec30483 --- /dev/null +++ b/LICENSE @@ -0,0 +1,190 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright 2026 The cncf-sandbox-application-template authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/ONBOARDING.md b/ONBOARDING.md new file mode 100644 index 0000000..a7ca482 --- /dev/null +++ b/ONBOARDING.md @@ -0,0 +1,457 @@ +# CNCF Sandbox Onboarding + +Single source of truth for completing [CNCF Sandbox onboarding](https://github.com/cncf/sandbox/issues/487) after your project's TOC acceptance vote. + +**Work here only:** each item shows `(Issue: …)` until you open a PR with `Closes #N`; automation then checks the box and shows `(PR: …)` here and in [README.md](README.md). Edit evidence below; do not edit README or toggle boxes yourself. + +Onboarding should be completed within **one month** of acceptance. When everything is done (or intentionally N/A), run `./scripts/generate-status-report.sh` to produce an update for your CNCF onboarding issue. + +--- + +## Required before CNCF staff can proceed + +A signed Project Contribution Agreement and trademark transfer are required **before** CNCF staff can complete the rest of onboarding. Other items can be worked on in parallel. + +### ip-policy-review + + +Review the [CNCF IP Policy](https://github.com/cncf/foundation/blob/main/charter.md#11-ip-policy). Ensure the project uses a CNCF-compatible license — inbound projects must use **Apache 2.0**. (Dependency licenses are tracked separately in `license-scan-import`.) + + +- [ ] Review and understand the CNCF IP Policy (Issue: #ISSUE_IP_POLICY_REVIEW) + +**Evidence:** + +_Confirm license is Apache 2.0 and link to the LICENSE file._ + +### license-policy-review + + +Review the [CNCF Allowlist License Policy](https://github.com/cncf/foundation/blob/main/policies-guidance/allowed-third-party-license-policy.md). This governs licenses used by third-party dependencies. CNCF FOSSA or CNCF Snyk can check compliance — pick one (tracked in `license-scan-import`). + + +- [ ] Review and understand the CNCF Third Party License Policy (Issue: #ISSUE_LICENSE_POLICY_REVIEW) + +**Evidence:** + +_Note which scanning service you'll use (FOSSA or Snyk)._ + +### trademark-guidelines-review + + +Review the [Linux Foundation trademark guidelines](https://www.linuxfoundation.org/trademark-usage/). Let the TOC know if you plan to change your project name. + + +- [ ] Review and understand the LF trademark guidelines (Issue: #ISSUE_TRADEMARK_GUIDELINES_REVIEW) + +**Evidence:** + +_Confirm review complete; note any planned name change (or N/A)._ + +### trademark-transfer + + +Transfer any existing trademark and logo assets to the Linux Foundation via the Project Contribution Agreement. CNCF staff sends this document to the contact emails listed in your Sandbox application. + + +- [ ] Transfer trademark and logo assets to the Linux Foundation (Issue: #ISSUE_TRADEMARK_TRANSFER) + +**Evidence:** + +_Date Contribution Agreement was signed, or status if pending._ + +## Review and understand other documents + +### technical-leadership-principles + + +Read the [Technical Leadership Principles](https://contribute.cncf.io/maintainers/community/leadership-principles/), which outline expected behavior for maintainers in leadership roles. + + +- [ ] Review the Technical Leadership Principles (Issue: #ISSUE_TECHNICAL_LEADERSHIP_PRINCIPLES) + +**Evidence:** + +_Confirm review complete._ + +### project-proposal-process + + +Read the [CNCF project proposal process and requirements](https://github.com/cncf/toc/blob/main/process/README.md). + + +- [ ] Review the project proposal process and requirements (Issue: #ISSUE_PROJECT_PROPOSAL_PROCESS) + +**Evidence:** + +_Confirm review complete._ + +### cncf-services-review + + +Read about [services available to CNCF projects](https://contribute.cncf.io/resources/project-services/). + + +- [ ] Review services available for your project at the CNCF (Issue: #ISSUE_CNCF_SERVICES_REVIEW) + +**Evidence:** + +_Confirm review complete._ + +### online-program-guidelines + + +Read the CNCF online program guidelines (webinars, meetups, events) provided by CNCF staff. + + +- [ ] Review the online program guidelines (Issue: #ISSUE_ONLINE_PROGRAM_GUIDELINES) + +**Evidence:** + +_Confirm review complete._ + +### telemetry-policy-review + + +Read the CNCF telemetry data collection and usage policy. + + +- [ ] Review the telemetry data collection and usage policy (Issue: #ISSUE_TELEMETRY_POLICY_REVIEW) + +**Evidence:** + +_Confirm review complete._ + +### cncf-staff-office-hours + + +Optional: book time with CNCF staff to walk through available resources, work through onboarding tasks together, or ask questions. + + +- [ ] Optional: book time with CNCF staff (Issue: #ISSUE_CNCF_STAFF_OFFICE_HOURS) + +**Evidence:** + +_Meeting date, or N/A if not needed._ + +## Contribute and transfer other materials + +### neutral-github-org + + +This makes the project transferable to the CNCF's GitHub Enterprise account. If it's already in another GitHub Enterprise account, remove it from there first. + + +- [ ] Move the project to its own separate neutral GitHub organization (Issue: #ISSUE_NEUTRAL_GITHUB_ORG) + +**Evidence:** + +_Neutral GitHub org name/URL._ + +### ghe-invite-accepted + + +CNCF will add `thelinuxfoundation` as an organization owner to ensure neutral hosting. + + +- [ ] Accept the invite to join the CNCF GitHub Enterprise account (Issue: #ISSUE_GHE_INVITE_ACCEPTED) + +**Evidence:** + +_Date invite accepted._ + +### slack-migration + + +CNCF staff can help. This improves discoverability, lets CNCF enforce its Code of Conduct, and enables unlimited message retention. If you already have a large Slack/Discord community, CNCF may instead link to it from a pointer channel in the CNCF Slack workspace. + + +- [ ] Migrate Slack channels to the Kubernetes or CNCF Slack workspace (Issue: #ISSUE_SLACK_MIGRATION) + +**Evidence:** + +_Migration status or link to pointer channel._ + +### maintainers-circle-slack + + +Join `#maintainers-circle` on CNCF Slack to find and share knowledge with other project teams. + + +- [ ] Join the #maintainers-circle Slack channel (Issue: #ISSUE_MAINTAINERS_CIRCLE_SLACK) + +**Evidence:** + +_Confirm joined._ + +### domain-transfer + + +If your project has its own domain(s), transfer them to the CNCF. LF Contact: `projects@cncf.io`. Project Selection: CNCF. + + +- [ ] Transfer project domain(s) to the CNCF (Issue: #ISSUE_DOMAIN_TRANSFER) + +**Evidence:** + +_Domain(s) transferred, or N/A if none._ + +### artwork-pr + + +Submit project artwork to [cncf/artwork](https://github.com/cncf/artwork). If you don't have artwork, CNCF can help design some. + + +- [ ] Submit a pull request with your artwork (Issue: #ISSUE_ARTWORK_PR) + +**Evidence:** + +_Link to the artwork PR._ + +### analytics-transfer + + +If you use Google Analytics, add `projects@cncf.io` as an admin of your existing account so it can be moved to a CNCF-managed account. If you don't have GA, note your current analytics setup instead. + + +- [ ] Transfer website analytics to the CNCF (Issue: #ISSUE_ANALYTICS_TRANSFER) + +**Evidence:** + +_Analytics status, or N/A._ + +## Update and document project details + +### maintainer-list-pr + + +Create/verify `MAINTAINERS.md` and open a PR against the aggregated CNCF maintainer list. + + +- [ ] Create a maintainer list and add it to the aggregated CNCF maintainer list (Issue: #ISSUE_MAINTAINER_LIST_PR) + +**Evidence:** + +_Link to MAINTAINERS.md and the PR against the aggregated list._ + +### maintainer-emails + + +Email maintainer addresses to `project-onboarding@cncf.io` (not shared publicly). Also link each maintainer's GitHub ID with their LF profile. + + +- [ ] Provide maintainer emails for mailing list and Service Desk access (Issue: #ISSUE_MAINTAINER_EMAILS) + +**Evidence:** + +_Date emailed; confirm LF profile linking._ + +### dco-enabled + + +Enable the [DCO GitHub App](https://github.com/apps/dco) on every repo in scope. A CLA may be used in addition to the DCO, not instead of it. + + +- [ ] Ensure the DCO app is enabled for all project GitHub repositories (Issue: #ISSUE_DCO_ENABLED) + +**Evidence:** + +_List repos with DCO enabled._ + +### coc-in-readme + + +Explicitly reference the CNCF Code of Conduct (or your adopted version) in the project's `README.md` on GitHub. + + +- [ ] Reference the CNCF Code of Conduct in README.md (Issue: #ISSUE_COC_IN_README) + +**Evidence:** + +_Link to the README section._ + +### lf-footer + + +Add the Linux Foundation footer to your website per [LF branding guidelines](https://github.com/cncf/foundation/blob/main/website-guidelines.md). If you don't have a dedicated website, adopt these guidelines for `README.md` instead. + + +- [ ] Add the LF footer to your website (or README if no website) (Issue: #ISSUE_LF_FOOTER) + +**Evidence:** + +_Link to footer on the site or README section._ + +### governance-doc + + +Document written, open governance in a `GOVERNANCE.md` file at the root of your repo. + + +- [ ] Start a GOVERNANCE.md documenting open governance (Issue: #ISSUE_GOVERNANCE_DOC) + +**Evidence:** + +_Link to GOVERNANCE.md._ + +### security-doc + + +Document a security policy in a `SECURITY.md` file at the root of your repo. See [CNCF security guidelines](https://contribute.cncf.io/maintainers/security/security-guidelines/#3-securitymd). + + +- [ ] Start a SECURITY.md security policy (Issue: #ISSUE_SECURITY_DOC) + +**Evidence:** + +_Link to SECURITY.md._ + +### openssf-badge + + +Register and begin working toward an [OpenSSF Best Practices Badge](https://www.bestpractices.dev/). + + +- [ ] Start an OpenSSF Best Practices Badge (Issue: #ISSUE_OPENSSF_BADGE) + +**Evidence:** + +_Link to the badge page/status._ + +### license-scan-import + + +Import all repos in scope into CNCF FOSSA or CNCF Snyk (see `license-policy-review`). + + +- [ ] Import all project repos into a license scanning service (Issue: #ISSUE_LICENSE_SCAN_IMPORT) + +**Evidence:** + +_Service used and confirmation all repos are imported._ + +## CNCF staff tasks (track and follow up) + +These are completed by CNCF staff, not the project. Track them here so you know what to follow up on; check the box once staff confirms completion. + +### devstats + + +CNCF staff adds the project to [DevStats](https://all.devstats.cncf.io/). + + +- [ ] CNCF staff: add the project to DevStats (Issue: #ISSUE_DEVSTATS) + +**Evidence:** + +_Link to the project's DevStats dashboard once available._ + +### clomonitor + + +CNCF staff adds the project to [CLOMonitor](https://clomonitor.io/). + + +- [ ] CNCF staff: add the project to CLOMonitor (Issue: #ISSUE_CLOMONITOR) + +**Evidence:** + +_Link to the project's CLOMonitor report card._ + +### lfx-insights-onboarding + + +CNCF staff adds the project to [LFX Insights](https://insights.linuxfoundation.org/). + + +- [ ] CNCF staff: add the project to LFX Insights (Issue: #ISSUE_LFX_INSIGHTS_ONBOARDING) + +**Evidence:** + +_Link to LFX Insights for the project._ + +### lfx-pcc-activation + + +CNCF staff activates the project in the LFX Project Control Center (PCC). + + +- [ ] CNCF staff: activate the project in the LFX Project Control Center (Issue: #ISSUE_LFX_PCC_ACTIVATION) + +**Evidence:** + +_Confirmation date once activated._ + +### landscape-listing-onboarding + + +After PCC activation, CNCF staff adds the project to the [Cloud Native Landscape](https://landscape.cncf.io/), including the `lfx_slug` in the landscape configuration file. + + +- [ ] CNCF staff: add the project to the Cloud Native Landscape (Issue: #ISSUE_LANDSCAPE_LISTING_ONBOARDING) + +**Evidence:** + +_Link to the landscape PR/entry._ + +### license-scanner-team + + +CNCF staff adds the maintainers team to the chosen license scanner service (FOSSA or Snyk). + + +- [ ] CNCF staff: add the maintainers team to the license scanner (Issue: #ISSUE_LICENSE_SCANNER_TEAM) + +**Evidence:** + +_Confirmation maintainers have scanner access._ + +### groupsio-list + + +CNCF staff creates a groups.io maintainer list for the project in the LFX Project Control Center. + + +- [ ] CNCF staff: create a groups.io project maintainer list in PCC (Issue: #ISSUE_GROUPSIO_LIST) + +**Evidence:** + +_Groups.io list name/link._ + +### groupsio-maintainers-list + + +CNCF staff adds the project's groups.io maintainer list to `maintainers@cncf.io`. + + +- [ ] CNCF staff: add the project's groups.io list to maintainers@cncf.io (Issue: #ISSUE_GROUPSIO_MAINTAINERS_LIST) + +**Evidence:** + +_Confirmation complete._ + +### welcome-email + + +CNCF staff sends a welcome email confirming maintainer mailing list and Service Desk access. + + +- [ ] CNCF staff: send a welcome email confirming maintainer list access (Issue: #ISSUE_WELCOME_EMAIL) + +**Evidence:** + +_Date welcome email received._ + +## Final review + +### onboarding-complete + + +When every item above is complete (or intentionally N/A), post a status update on your CNCF onboarding issue and ask CNCF staff to confirm and close it. Run `./scripts/generate-status-report.sh` to produce a paste-ready update, or `./scripts/generate-status-report.sh --post` to post it directly as a comment. + + +- [ ] Final review: confirm all items complete and request sign-off (Issue: #ISSUE_ONBOARDING_COMPLETE) + +**Evidence:** + +_Link to the comment confirming onboarding complete, and the closed onboarding issue._ diff --git a/README.md b/README.md index 91d12dc..907bd33 100644 --- a/README.md +++ b/README.md @@ -74,26 +74,23 @@ Each row links to the matching section in [ONBOARDING.md](ONBOARDING.md). A PR w ./scripts/bootstrap-issues.sh ``` -3. Commit bootstrap changes and push. -4. Work in **[ONBOARDING.md](ONBOARDING.md)** only: add evidence, open a PR with `Closes #N` (issue link is on the checklist line). -5. When everything is complete (or intentionally N/A), generate an update for your CNCF onboarding issue: - - ```bash - ./scripts/generate-status-report.sh - ./scripts/generate-status-report.sh --post # posts as a comment via gh - ``` +3. Add a `CNCF_SANDBOX_TOKEN` repo secret (a GitHub PAT with `public_repo` scope) so Actions can comment on the CNCF onboarding issue on your behalf — see [CONTRIBUTING.md](CONTRIBUTING.md). +4. Commit bootstrap changes and push. +5. Work in **[ONBOARDING.md](ONBOARDING.md)** only: add evidence, open a PR with `Closes #N` (issue link is on the checklist line). When that issue closes, a progress comment is posted to your CNCF onboarding issue automatically. See [CONTRIBUTING.md](CONTRIBUTING.md) for the full workflow. ## How tracking works -| Stage | Checklist line shows | +| Stage | What happens | | --- | --- | -| After bootstrap | `- [ ] … (Issue: [#N](…))` | -| After you open a PR with `Closes #N` | `- [x] … (PR: [#M](…))` in ONBOARDING.md and this README | -| After the PR merges | Issue closes; state on `main` already matches | +| After bootstrap | Checklist line shows `- [ ] … (Issue: [#N](…))` | +| After you open a PR with `Closes #N` | Line becomes `- [x] … (PR: [#M](…))` in ONBOARDING.md and this README | +| After the PR merges (issue closes) | Actions posts a comment with that item on your **CNCF onboarding issue** — progress becomes visible to CNCF as you go, not just at the end | + +Some items are labeled `[CNCF Staff]` — these are completed by CNCF staff, not you. Track them for follow-up and check the box once staff confirms; closing that issue also posts a comment. -Some items are labeled `[CNCF Staff]` — these are completed by CNCF staff, not you. Track them for follow-up and check the box once staff confirms. +Need a one-time full summary instead (e.g. the first update, or to catch up if a comment failed)? Run `./scripts/generate-status-report.sh --post`. ## References diff --git a/scripts/apply_onboarding_metadata.py b/scripts/apply_onboarding_metadata.py new file mode 100644 index 0000000..12f2ca7 --- /dev/null +++ b/scripts/apply_onboarding_metadata.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +"""Apply bootstrap metadata (project name, onboarding issue, dates) to README.md. + +Deliberately simple string replacement only — no section parsing. ONBOARDING.md +items don't carry project metadata, so there's no risk of the "silently drops +adjacent checklist lines" class of bug that section-rewriting scripts have. +""" + +import json +import os +from datetime import datetime, timedelta +from pathlib import Path + + +def update_readme(content: str, metadata: dict) -> str: + project_name = metadata["project_name"] + onboarding_issue_url = metadata.get("onboarding_issue_url", "") + accepted_date = metadata.get("accepted_date", "") + target_date = metadata.get("target_completion_date", "") + + content = content.replace( + "> **Project name:** _Replace with your project name_", + f"> **Project name:** {project_name}", + ) + content = content.replace( + "> **CNCF onboarding issue:** _Link to your project's onboarding issue in cncf/sandbox_", + f"> **CNCF onboarding issue:** {onboarding_issue_url}" if onboarding_issue_url else "> **CNCF onboarding issue:** _Link to your project's onboarding issue in cncf/sandbox_", + ) + content = content.replace( + "> **Accepted (TOC vote):** _YYYY-MM-DD_", + f"> **Accepted (TOC vote):** {accepted_date}" if accepted_date else "> **Accepted (TOC vote):** _YYYY-MM-DD_", + ) + content = content.replace( + "> **Target completion (accepted + 30 days):** _YYYY-MM-DD_", + f"> **Target completion (accepted + 30 days):** {target_date}" if target_date else "> **Target completion (accepted + 30 days):** _YYYY-MM-DD_", + ) + content = content.replace( + "# CNCF Sandbox Onboarding", + f"# {project_name} — CNCF Sandbox Onboarding", + 1, + ) + return content + + +def compute_target_date(accepted_date: str) -> str: + if not accepted_date: + return "" + try: + parsed = datetime.strptime(accepted_date, "%Y-%m-%d") + except ValueError: + return "" + return (parsed + timedelta(days=30)).strftime("%Y-%m-%d") + + +def main() -> int: + root = Path(os.environ["ROOT_DIR"]) + metadata = json.loads(os.environ["PROJECT_METADATA_JSON"]) + + if metadata.get("accepted_date") and not metadata.get("target_completion_date"): + metadata["target_completion_date"] = compute_target_date(metadata["accepted_date"]) + + readme_path = root / "README.md" + readme_path.write_text(update_readme(readme_path.read_text(), metadata), encoding="utf-8") + + metadata_path = root / ".github" / "project-metadata.json" + metadata_path.write_text(json.dumps(metadata, indent=2) + "\n", encoding="utf-8") + + print(f"Updated {readme_path.name} and {metadata_path}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/bootstrap-issues.sh b/scripts/bootstrap-issues.sh new file mode 100755 index 0000000..e86102d --- /dev/null +++ b/scripts/bootstrap-issues.sh @@ -0,0 +1,353 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Creates one GitHub issue per CNCF sandbox onboarding item (from +# .github/onboarding-items.json) and writes issue links into ONBOARDING.md. +# +# Prerequisites: +# - GitHub CLI (gh) installed and authenticated +# - jq installed +# - Run from the repository root after creating a repo from this template +# +# Usage: +# ./scripts/bootstrap-issues.sh [--dry-run] +# ./scripts/bootstrap-issues.sh --project-name "MyProject" [--non-interactive] +# +# Options: +# --project-name NAME Project name (prompted if omitted) +# --onboarding-issue-url URL Link to your project's cncf/sandbox onboarding issue +# --accepted-date YYYY-MM-DD Date of the TOC acceptance vote +# --non-interactive Fail if required values are missing instead of prompting + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +ITEMS_FILE="${ROOT_DIR}/.github/onboarding-items.json" +ONBOARDING_FILE="${ROOT_DIR}/ONBOARDING.md" +REGISTRY_FILE="${ROOT_DIR}/.github/issue-registry.json" +METADATA_FILE="${ROOT_DIR}/.github/project-metadata.json" +APPLY_METADATA="${ROOT_DIR}/scripts/apply_onboarding_metadata.py" +TRACKING="${ROOT_DIR}/scripts/checklist_tracking.py" +DRY_RUN=false +NON_INTERACTIVE=false +PROJECT_NAME="" +ONBOARDING_ISSUE_URL="" +ACCEPTED_DATE="" + +SLUGS_FILE="$(mktemp)" +SLUGS_ORDERED_FILE="$(mktemp)" +REGISTRY_TMP="$(mktemp)" +LABELS_FILE="$(mktemp)" +EXISTING_LABELS_FILE="$(mktemp)" +ITEMS_FLAT_FILE="$(mktemp)" + +cleanup() { + rm -f "${SLUGS_FILE}" "${SLUGS_ORDERED_FILE}" "${REGISTRY_TMP}" \ + "${LABELS_FILE}" "${EXISTING_LABELS_FILE}" "${ITEMS_FLAT_FILE}" \ + "${ONBOARDING_FILE}.tmp" +} +trap cleanup EXIT + +while [[ $# -gt 0 ]]; do + case "${1}" in + --dry-run) DRY_RUN=true ;; + --non-interactive) NON_INTERACTIVE=true ;; + --project-name) PROJECT_NAME="${2:-}"; shift ;; + --onboarding-issue-url) ONBOARDING_ISSUE_URL="${2:-}"; shift ;; + --accepted-date) ACCEPTED_DATE="${2:-}"; shift ;; + -h|--help) + sed -n '2,20p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//' + exit 0 + ;; + *) + echo "Unknown option: ${1}" >&2 + exit 1 + ;; + esac + shift +done + +for cmd in gh jq python3; do + if ! command -v "${cmd}" >/dev/null 2>&1; then + echo "Error: ${cmd} is required." >&2 + exit 1 + fi +done + +if ! gh api user -q .login >/dev/null 2>&1; then + echo "Error: gh is not authenticated. Run: gh auth login" >&2 + exit 1 +fi + +if [[ ! -f "${ITEMS_FILE}" || ! -f "${ONBOARDING_FILE}" ]]; then + echo "Error: required files missing. Run from repository root." >&2 + exit 1 +fi + +REPO="$(gh repo view --json nameWithOwner -q .nameWithOwner 2>/dev/null || true)" +if [[ -z "${REPO}" ]]; then + echo "Error: could not determine repository. Run inside a GitHub repo, or run 'gh repo set-default'." >&2 + exit 1 +fi +DEFAULT_REPO_URL="https://github.com/${REPO}" + +prompt_value() { + local var_name="${1}" prompt_text="${2}" default_value="${3}" + local current_value="${!var_name}" + + if [[ -n "${current_value}" ]]; then + return 0 + fi + if [[ "${NON_INTERACTIVE}" == true ]]; then + echo "Error: --${var_name//_/-} is required in non-interactive mode." >&2 + exit 1 + fi + + local input + if [[ -n "${default_value}" ]]; then + read -r -p "${prompt_text} [${default_value}]: " input + input="${input:-${default_value}}" + else + read -r -p "${prompt_text}: " input + fi + printf -v "${var_name}" '%s' "${input}" +} + +collect_project_info() { + if [[ "${DRY_RUN}" == true ]]; then + echo "Project setup (skipped in dry run):" + echo " Project name: ${PROJECT_NAME:-}" + echo " Onboarding issue URL: ${ONBOARDING_ISSUE_URL:-}" + echo " Accepted date: ${ACCEPTED_DATE:-}" + echo + return 0 + fi + + echo "Project setup" + echo "Press Enter to accept [default] values." + echo + prompt_value PROJECT_NAME "Project name" "" + prompt_value ONBOARDING_ISSUE_URL "Your CNCF onboarding issue URL (e.g. https://github.com/cncf/sandbox/issues/487)" "" + prompt_value ACCEPTED_DATE "TOC acceptance date (YYYY-MM-DD)" "" + + local metadata + metadata="$(jq -n \ + --arg project_name "${PROJECT_NAME}" \ + --arg onboarding_issue_url "${ONBOARDING_ISSUE_URL}" \ + --arg accepted_date "${ACCEPTED_DATE}" \ + --arg repository "${REPO}" \ + --arg bootstrapped_at "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \ + '{project_name: $project_name, onboarding_issue_url: $onboarding_issue_url, accepted_date: $accepted_date, repository: $repository, bootstrapped_at: $bootstrapped_at}')" + + export ROOT_DIR PROJECT_METADATA_JSON="${metadata}" + python3 "${APPLY_METADATA}" + echo +} + +label_color() { + case "${1}" in + onboarding-item) echo "0E8A16" ;; + critical) echo "B60205" ;; + optional) echo "FBCA04" ;; + cncf-staff) echo "5319E7" ;; + category:*) echo "1D76DB" ;; + checklist:*) echo "1D76DB" ;; + *) echo "BFD4F2" ;; + esac +} + +label_description() { + case "${1}" in + onboarding-item) echo "Part of the CNCF sandbox onboarding checklist" ;; + critical) echo "Required before CNCF staff can proceed" ;; + optional) echo "Optional item" ;; + cncf-staff) echo "Completed by CNCF staff; track for follow-up" ;; + category:*) echo "Onboarding category: ${1#category:}" ;; + checklist:*) echo "Checklist item: ${1#checklist:}" ;; + *) echo "" ;; + esac +} + +reverse_lines() { + if command -v tac >/dev/null 2>&1; then tac "$1"; else tail -r "$1"; fi +} + +# Flatten onboarding-items.json into one JSON object per line: slug, title, +# category id, critical/optional/staff flags, guide, evidence hint. +flatten_items() { + jq -c '.categories[] as $c | $c.items[] | . + {category: $c.id}' "${ITEMS_FILE}" > "${ITEMS_FLAT_FILE}" +} + +build_checklist_order() { + # Order comes from ONBOARDING.md (### slug headings), not the JSON file, so + # the created-issue order always matches what's rendered in the doc. + grep -oE '' "${ONBOARDING_FILE}" \ + | sed -E 's//\1/' \ + | awk '!seen[$0]++' > "${SLUGS_ORDERED_FILE}" +} + +validate_checklist_order() { + local items_file doc_file only_items only_doc + items_file="$(mktemp)" + doc_file="$(mktemp)" + jq -r '[.categories[].items[].slug] | .[]' "${ITEMS_FILE}" | LC_ALL=C sort > "${items_file}" + LC_ALL=C sort "${SLUGS_ORDERED_FILE}" > "${doc_file}" + + if cmp -s "${items_file}" "${doc_file}"; then + rm -f "${items_file}" "${doc_file}" + return 0 + fi + + local only_map only_app + only_map="$(comm -23 "${items_file}" "${doc_file}" || true)" + only_app="$(comm -13 "${items_file}" "${doc_file}" || true)" + rm -f "${items_file}" "${doc_file}" + + echo "Error: checklist slugs in ONBOARDING.md do not match .github/onboarding-items.json" >&2 + if [[ -n "${only_map}" ]]; then + echo " Present in onboarding-items.json but missing from ONBOARDING.md:" >&2 + sed 's/^/ - /' <<< "${only_map}" >&2 + fi + if [[ -n "${only_app}" ]]; then + echo " Present in ONBOARDING.md but missing from onboarding-items.json:" >&2 + sed 's/^/ - /' <<< "${only_app}" >&2 + fi + echo " Fix: python3 scripts/render_onboarding_template.py (only if you haven't started tracking real issues yet)." >&2 + exit 1 +} + +ensure_labels() { + local label color description + echo "Fetching existing repository labels..." + gh label list --limit 500 --json name -q '.[].name' | sort > "${EXISTING_LABELS_FILE}" + + { + echo "onboarding-item" + echo "critical" + echo "optional" + echo "cncf-staff" + jq -r '.categories[].id' "${ITEMS_FILE}" | sed 's/^/category:/' + jq -r '.categories[].items[].slug' "${ITEMS_FILE}" | sed 's/^/checklist:/' + } | sort -u > "${LABELS_FILE}" + + while IFS= read -r label; do + [[ -z "${label}" ]] && continue + if grep -qxF "${label}" "${EXISTING_LABELS_FILE}"; then + continue + fi + color="$(label_color "${label}")" + description="$(label_description "${label}")" + if [[ "${DRY_RUN}" == true ]]; then + echo "[dry-run] Would create label: ${label}" + continue + fi + echo "Creating label: ${label}" + gh label create "${label}" --color "${color}" --description "${description}" + done < "${LABELS_FILE}" +} + +build_issue_body() { + local slug="${1}" + jq -r --arg s "${slug}" ' + select(.slug == $s) | + (.guide // "") + "\n\n**Evidence:** update the `" + $s + "` section in ONBOARDING.md with links/notes, then open a PR with `Closes #ISSUE_NUMBER`.\n\n---\n**Checklist slug:** `" + $s + "`\n**Onboarding doc:** See [ONBOARDING.md](ONBOARDING.md#" + $s + ")" + ' "${ITEMS_FLAT_FILE}" +} + +build_issue_title() { + local slug="${1}" + jq -r --arg s "${slug}" ' + select(.slug == $s) | + (if .critical then "[Critical] " elif .staff then "[CNCF Staff] " elif .optional then "[Optional] " else "[Onboarding] " end) + .title + ' "${ITEMS_FLAT_FILE}" +} + +build_issue_labels() { + local slug="${1}" + jq -r --arg s "${slug}" ' + select(.slug == $s) | + ["onboarding-item", ("category:" + .category), ("checklist:" + $s)] + + (if .critical then ["critical"] else [] end) + + (if .optional then ["optional"] else [] end) + + (if .staff then ["cncf-staff"] else [] end) + | join(",") + ' "${ITEMS_FLAT_FILE}" +} + +collect_project_info +flatten_items +build_checklist_order +validate_checklist_order + +# GitHub's default issues view sorts by Newest first, so create the last +# checklist item first to make the first item land at the top of that view. +reverse_lines "${SLUGS_ORDERED_FILE}" > "${SLUGS_FILE}" +total="$(wc -l < "${SLUGS_ORDERED_FILE}" | tr -d ' ')" + +echo "Bootstrapping ${total} onboarding issues for ${REPO}..." +echo "Completion order (top to bottom in the issues list):" +while IFS= read -r slug; do + echo " - $(build_issue_title "${slug}")" +done < "${SLUGS_ORDERED_FILE}" +echo + +ensure_labels +echo + +echo "{" > "${REGISTRY_TMP}" +echo " \"repository\": \"${REPO}\"," >> "${REGISTRY_TMP}" +echo " \"created_at\": \"$(date -u +"%Y-%m-%dT%H:%M:%SZ")\"," >> "${REGISTRY_TMP}" +echo " \"issues\": {" >> "${REGISTRY_TMP}" + +first_entry=true +while IFS= read -r slug; do + title="$(build_issue_title "${slug}")" + labels="$(build_issue_labels "${slug}")" + body="$(build_issue_body "${slug}")" + placeholder="#ISSUE_$(echo "${slug}" | tr '[:lower:]-' '[:upper:]_')" + body="${body//\#ISSUE_NUMBER/${placeholder}}" + + if [[ "${DRY_RUN}" == true ]]; then + echo "[dry-run] Would create: ${title}" + issue_number="0" + else + existing="$(gh issue list --label "checklist:${slug}" --state all --json number -q '.[0].number' 2>/dev/null || true)" + if [[ -n "${existing}" && "${existing}" != "null" ]]; then + echo "Issue already exists for ${slug}: #${existing}" + issue_number="${existing}" + else + echo "Creating issue: ${title}" + issue_url="$(gh issue create --title "${title}" --label "${labels}" --body "${body}")" + issue_number="${issue_url##*/}" + echo " -> #${issue_number}" + fi + fi + + if [[ "${first_entry}" == true ]]; then first_entry=false; else echo "," >> "${REGISTRY_TMP}"; fi + printf ' "%s": %s' "${slug}" "${issue_number}" >> "${REGISTRY_TMP}" + + if [[ "${DRY_RUN}" == false ]]; then + python3 "${TRACKING}" --root "${ROOT_DIR}" link-issue --slug "${slug}" --number "${issue_number}" --repo "${REPO}" + fi +done < "${SLUGS_FILE}" + +echo >> "${REGISTRY_TMP}" +echo " }" >> "${REGISTRY_TMP}" +echo "}" >> "${REGISTRY_TMP}" + +if [[ "${DRY_RUN}" == true ]]; then + echo + echo "Dry run complete. No files modified." + exit 0 +fi + +mv "${REGISTRY_TMP}" "${REGISTRY_FILE}" +trap - EXIT + +python3 "${TRACKING}" --root "${ROOT_DIR}" sync-readme + +echo +echo "Updated ONBOARDING.md, README.md, and wrote ${REGISTRY_FILE}" +echo +echo "Next steps:" +echo " git add ONBOARDING.md README.md .github/project-metadata.json .github/issue-registry.json" +echo " git commit -m 'Bootstrap CNCF sandbox onboarding checklist issues'" +echo " git push" diff --git a/scripts/checklist_tracking.py b/scripts/checklist_tracking.py new file mode 100644 index 0000000..6db8de3 --- /dev/null +++ b/scripts/checklist_tracking.py @@ -0,0 +1,304 @@ +#!/usr/bin/env python3 +"""Issue/PR tracking on checklist lines in ONBOARDING.md and README.md. + +Single source of truth is ONBOARDING.md. README.md mirrors checklist lines +and holds the only progress indicator. Checklist lines look like: + + - [ ] Title (Issue: [#12](https://github.com/org/repo/issues/12)) + +After a PR with `Closes #12` is opened, the line becomes: + + - [x] Title (PR: [#34](https://github.com/org/repo/pull/34)) +""" + +import argparse +import json +import re +import sys +from pathlib import Path +from typing import Dict, List, Optional, Tuple + +ONBOARDING_FILENAME = "ONBOARDING.md" + +CHECKLIST_LINE = re.compile(r"^- \[[ xX]\].*.*$") +CHECKLIST_LINE_PARTS = re.compile( + r"^- \[([ xX])\] (.+?) \s*(\(.*\))?\s*$" +) +CHECKLIST_SLUG = re.compile(r"") +TRACKING_SUFFIX = re.compile(r" \((Issue|PR):.*\)\s*$") + +START_DASHBOARD = "" +END_DASHBOARD = "" +START_PROGRESS = "" +END_PROGRESS = "" +BAR_WIDTH = 20 + +# Item headings are H3 (`### slug`); category headings are H2 with prose text +# and never match this pattern, so they're safely ignored by the parsers below. +ITEM_HEADING = re.compile(r"^### ([a-z0-9-]+)\s*$") + + +def onboarding_path(root: Path) -> Path: + return root / ONBOARDING_FILENAME + + +def issue_token_placeholder(slug: str) -> str: + token = f"#ISSUE_{slug.upper().replace('-', '_')}" + return f"(Issue: {token})" + + +def issue_token_linked(repo: str, issue_number: int) -> str: + return f"(Issue: [#{issue_number}](https://github.com/{repo}/issues/{issue_number}))" + + +def pr_token_linked(repo: str, pr_number: int) -> str: + return f"(PR: [#{pr_number}](https://github.com/{repo}/pull/{pr_number}))" + + +def strip_tracking_suffix(line: str) -> str: + return TRACKING_SUFFIX.sub("", line.rstrip()) + + +def set_checklist_line_tracking(line: str, slug: str, tracking: str) -> str: + marker = f"" + if marker not in line or not CHECKLIST_LINE.match(line): + return line + base = strip_tracking_suffix(line) + return f"{base} {tracking}" + + +def apply_issue_link(content: str, slug: str, issue_number: int, repo: str) -> str: + placeholder = issue_token_placeholder(slug) + linked = issue_token_linked(repo, issue_number) + lines = [] + for line in content.splitlines(): + if f"" in line and placeholder in line: + lines.append(line.replace(placeholder, linked)) + else: + lines.append(line) + return "\n".join(lines) + ("\n" if content.endswith("\n") else "") + + +def apply_pr_link(content: str, slug: str, pr_number: int, repo: str) -> str: + tracking = pr_token_linked(repo, pr_number) + lines = [ + set_checklist_line_tracking(line, slug, tracking) + if f"" in line + else line + for line in content.splitlines() + ] + return "\n".join(lines) + ("\n" if content.endswith("\n") else "") + + +def mark_checkbox_complete(content: str, slug: str) -> str: + marker = f"" + pattern = re.compile(rf"^- \[ \] (.+?{re.escape(marker)})( .*)?$", re.MULTILINE) + + def repl(match: "re.Match[str]") -> str: + suffix = match.group(2) or "" + return f"- [x] {match.group(1)}{suffix}" + + return pattern.sub(repl, content, count=1) + + +def slug_for_issue(registry: Dict[str, int], issue_number: int) -> Optional[str]: + for slug, num in registry.items(): + if int(num) == int(issue_number): + return slug + return None + + +def load_issue_registry(path: Path) -> Dict[str, int]: + if not path.is_file(): + return {} + data = json.loads(path.read_text(encoding="utf-8")) + return {k: int(v) for k, v in data.get("issues", {}).items()} + + +def parse_closing_issues(text: str) -> List[int]: + pattern = re.compile(r"\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+#(\d+)", re.IGNORECASE) + return sorted({int(m.group(1)) for m in pattern.finditer(text or "")}) + + +def slug_complete(content: str, slug: str) -> bool: + marker = f"" + for line in content.splitlines(): + if marker in line and re.match(r"^- \[[xX]\]", line): + return True + return False + + +def compute_progress(content: str) -> Tuple[int, int]: + slugs = sorted(set(CHECKLIST_SLUG.findall(content))) + completed = sum(1 for slug in slugs if slug_complete(content, slug)) + return completed, len(slugs) + + +def render_progress_block(completed: int, total: int) -> str: + percent = round((completed / total) * 100) if total else 0 + filled = round((completed / total) * BAR_WIDTH) if total else 0 + bar = ("█" * filled) + ("░" * (BAR_WIDTH - filled)) + return ( + f"> **Onboarding progress:** **{completed} / {total}** items complete ({percent}%) \n" + f"> `{bar}` {percent}%" + ) + + +def update_marked_block(path: Path, start: str, end: str, block: str) -> bool: + if not path.is_file(): + return False + content = path.read_text(encoding="utf-8") + if start not in content or end not in content: + return False + pattern = re.compile(rf"{re.escape(start)}.*?{re.escape(end)}", re.DOTALL) + replacement = f"{start}\n{block}\n{end}" if block else f"{start}{end}" + new_content = pattern.sub(replacement, content, count=1) + if new_content == content: + return False + path.write_text(new_content, encoding="utf-8") + return True + + +def build_readme_dashboard(onboarding: str) -> str: + lines: List[str] = [] + current_slug: Optional[str] = None + checklist_row: Optional[str] = None + + def flush() -> None: + nonlocal current_slug, checklist_row + if current_slug and checklist_row: + lines.append(f"- {checklist_row}") + current_slug, checklist_row = None, None + + in_guide = False + for raw in onboarding.splitlines(): + heading = ITEM_HEADING.match(raw) + if heading: + flush() + current_slug = heading.group(1) + in_guide = False + continue + if current_slug is None: + continue + stripped = raw.strip() + if stripped == "": + in_guide = True + continue + if stripped == "": + in_guide = False + continue + if in_guide: + continue + parts = CHECKLIST_LINE_PARTS.match(raw.strip()) + if parts: + checked = parts.group(1).lower() == "x" + title = parts.group(2).strip() + tracking = (parts.group(3) or "").strip() + emoji = "✅" if checked else "⬜" + checklist_row = f"{emoji} [{title}]({ONBOARDING_FILENAME}#{current_slug}) {tracking}".rstrip() + + flush() + return "\n".join(lines).rstrip() + "\n" + + +def sync_readme(root: Path) -> bool: + readme = root / "README.md" + onboarding = onboarding_path(root) + if not readme.is_file() or not onboarding.is_file(): + return False + + onboarding_text = onboarding.read_text(encoding="utf-8") + completed, total = compute_progress(onboarding_text) + progress_block = render_progress_block(completed, total) + dashboard_block = build_readme_dashboard(onboarding_text) + + changed_progress = update_marked_block(readme, START_PROGRESS, END_PROGRESS, progress_block) + changed_dashboard = update_marked_block(readme, START_DASHBOARD, END_DASHBOARD, dashboard_block) + return changed_progress or changed_dashboard + + +def update_files_for_pr(root: Path, repo: str, pr_number: int, issue_numbers: List[int]) -> Tuple[bool, List[str]]: + registry_path = root / ".github" / "issue-registry.json" + registry = load_issue_registry(registry_path) + path = onboarding_path(root) + before = path.read_text(encoding="utf-8") + content = before + changed_slugs: List[str] = [] + + for issue_number in issue_numbers: + slug = slug_for_issue(registry, issue_number) + if not slug: + continue + # Closes #N is enough: swap Issue -> PR and check the box in one step. + content = apply_pr_link(content, slug, pr_number, repo) + content = mark_checkbox_complete(content, slug) + changed_slugs.append(slug) + + if not changed_slugs or content == before: + return False, [] + + path.write_text(content, encoding="utf-8") + sync_readme(root) + return True, changed_slugs + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--root", type=Path, default=Path(__file__).resolve().parent.parent) + sub = parser.add_subparsers(dest="command", required=True) + + link_issue = sub.add_parser("link-issue") + link_issue.add_argument("--slug", required=True) + link_issue.add_argument("--number", type=int, required=True) + link_issue.add_argument("--repo", required=True) + + link_pr = sub.add_parser("link-pr") + link_pr.add_argument("--repo", required=True) + link_pr.add_argument("--pr-number", type=int, required=True) + link_pr.add_argument("--issue-numbers", nargs="+", type=int, required=True) + + complete = sub.add_parser("complete-checkbox") + complete.add_argument("--slug", required=True) + + sub.add_parser("sync-readme") + sub.add_parser("progress") + + args = parser.parse_args() + root = args.root + path = onboarding_path(root) + + if args.command == "link-issue": + text = apply_issue_link(path.read_text(encoding="utf-8"), args.slug, args.number, args.repo) + path.write_text(text, encoding="utf-8") + sync_readme(root) + return 0 + + if args.command == "link-pr": + changed, slugs = update_files_for_pr(root, args.repo, args.pr_number, args.issue_numbers) + if not changed: + print("No checklist lines updated for PR", file=sys.stderr) + return 0 + print(f"Updated PR links and checked boxes for: {', '.join(slugs)}") + return 0 + + if args.command == "complete-checkbox": + text = mark_checkbox_complete(path.read_text(encoding="utf-8"), args.slug) + path.write_text(text, encoding="utf-8") + sync_readme(root) + return 0 + + if args.command == "sync-readme": + sync_readme(root) + return 0 + + if args.command == "progress": + completed, total = compute_progress(path.read_text(encoding="utf-8")) + percent = round((completed / total) * 100) if total else 0 + print(f"Onboarding progress: {completed}/{total} ({percent}%)") + return 0 + + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/generate-status-report.sh b/scripts/generate-status-report.sh new file mode 100755 index 0000000..a8e0a47 --- /dev/null +++ b/scripts/generate-status-report.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Generates ONBOARDING-STATUS.md from ONBOARDING.md — a paste-ready update for +# your CNCF onboarding issue's "Quote reply" checklist. +# +# Usage: +# ./scripts/generate-status-report.sh # write ONBOARDING-STATUS.md +# ./scripts/generate-status-report.sh --post # also post as a comment +# ./scripts/generate-status-report.sh --post --onboarding-issue-url URL + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +GENERATOR="${ROOT_DIR}/scripts/generate_status_report.py" +METADATA_FILE="${ROOT_DIR}/.github/project-metadata.json" +OUTPUT_FILE="${ROOT_DIR}/ONBOARDING-STATUS.md" +POST=false +ISSUE_URL="" + +while [[ $# -gt 0 ]]; do + case "${1}" in + --post) POST=true ;; + --onboarding-issue-url) ISSUE_URL="${2:-}"; shift ;; + -h|--help) + sed -n '2,10p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//' + exit 0 + ;; + *) + echo "Unknown option: ${1}" >&2 + exit 1 + ;; + esac + shift +done + +for cmd in python3 jq; do + if ! command -v "${cmd}" >/dev/null 2>&1; then + echo "Error: ${cmd} is required." >&2 + exit 1 + fi +done + +python3 "${GENERATOR}" + +if [[ "${POST}" == true ]]; then + if ! command -v gh >/dev/null 2>&1; then + echo "Error: gh is required for --post." >&2 + exit 1 + fi + if ! gh api user -q .login >/dev/null 2>&1; then + echo "Error: gh is not authenticated. Run: gh auth login" >&2 + exit 1 + fi + if [[ -z "${ISSUE_URL}" && -f "${METADATA_FILE}" ]]; then + ISSUE_URL="$(jq -r '.onboarding_issue_url // ""' "${METADATA_FILE}")" + fi + if [[ -z "${ISSUE_URL}" ]]; then + echo "Error: --onboarding-issue-url is required with --post (or run bootstrap-issues.sh first)." >&2 + exit 1 + fi + + # Expect a URL like https://github.com/cncf/sandbox/issues/487 + repo_and_number="$(echo "${ISSUE_URL}" | sed -E 's#https://github.com/([^/]+/[^/]+)/issues/([0-9]+).*#\1 \2#')" + repo="$(echo "${repo_and_number}" | cut -d' ' -f1)" + number="$(echo "${repo_and_number}" | cut -d' ' -f2)" + if [[ -z "${repo}" || -z "${number}" ]]; then + echo "Error: could not parse repo/issue number from: ${ISSUE_URL}" >&2 + exit 1 + fi + + echo "Posting status update to ${repo}#${number}..." + gh issue comment "${number}" -R "${repo}" --body-file "${OUTPUT_FILE}" + echo "Posted." +fi diff --git a/scripts/generate_item_comment.py b/scripts/generate_item_comment.py new file mode 100644 index 0000000..5100d25 --- /dev/null +++ b/scripts/generate_item_comment.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python3 +"""Render a single checklist item's section as a comment for the external +CNCF onboarding issue. + +Used by .github/workflows/sync-checklist.yml right after a checklist item's +local issue closes (i.e. its PR merged), so CNCF can see progress item by +item instead of only at the very end. +""" + +import argparse +import json +import re +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +ONBOARDING_FILE = ROOT / "ONBOARDING.md" +ITEMS_FILE = ROOT / ".github" / "onboarding-items.json" +METADATA_FILE = ROOT / ".github" / "project-metadata.json" + +ITEM_HEADING = re.compile(r"^### ([a-z0-9-]+)\s*$") +CATEGORY_HEADING = re.compile(r"^## (.+?)\s*$") +CHECKLIST_LINE = re.compile(r"^- \[([ xX])\].*(.*)$") +EVIDENCE_HEADING = re.compile(r"^\*\*Evidence:\*\*\s*$") +ANY_HEADING = re.compile(r"^#{1,6}\s") + + +def extract_item(text: str, slug: str) -> dict: + """Pull {title, checked, tracking, evidence, category} for one slug.""" + current_slug = None + current_category = "" + collecting_evidence = False + evidence_lines: list[str] = [] + result = {"title": "", "checked": False, "tracking": "", "evidence": "", "category": ""} + found = False + + for line in text.splitlines(): + cat = CATEGORY_HEADING.match(line) + if cat and not ITEM_HEADING.match(line): + current_category = cat.group(1).strip() + + heading = ITEM_HEADING.match(line) + if heading: + if found: + break # left the target item's section + current_slug = heading.group(1) + collecting_evidence = False + evidence_lines = [] + if current_slug == slug: + found = True + result["category"] = current_category + continue + + if not found: + continue + + if ANY_HEADING.match(line) and not heading: + collecting_evidence = False + continue + + match = CHECKLIST_LINE.match(line) + if match and match.group(2) == slug: + result["checked"] = match.group(1).lower() == "x" + result["tracking"] = match.group(3).strip() + title = re.sub(r"^- \[[ xX]\]\s*", "", line.strip()) + title = re.sub(r"\s*.*$", "", title) + result["title"] = title.strip() + continue + + if EVIDENCE_HEADING.match(line.strip()): + collecting_evidence = True + continue + + if collecting_evidence: + stripped = line.strip() + if stripped and not (stripped.startswith("_") and stripped.endswith("_")): + evidence_lines.append(stripped) + + result["evidence"] = "\n".join(evidence_lines).strip() + return result + + +def render_comment(item: dict, slug: str, local_repo: str, local_issue_number: str) -> str: + box = "x" if item["checked"] else " " + title = item["title"] or slug + lines = [ + f"", + f"**Progress update — {item['category']}**" if item["category"] else "**Progress update**", + "", + f"- [{box}] {title}", + ] + if item["evidence"]: + for evidence_line in item["evidence"].splitlines(): + lines.append(f" {evidence_line}") + + footer_bits = [] + tracking = item["tracking"].strip() + if tracking.startswith("(") and tracking.endswith(")"): + # Strip exactly the one outer wrapping paren pair added by + # pr_token_linked/issue_token_linked; the inner markdown link has its + # own matching parens that must be preserved. + tracking = tracking[1:-1] + if tracking: + footer_bits.append(tracking) + if local_repo and local_issue_number: + footer_bits.append( + f"tracking issue [{local_repo}#{local_issue_number}](https://github.com/{local_repo}/issues/{local_issue_number})" + ) + if footer_bits: + lines.append("") + lines.append(f"{' · '.join(footer_bits)}") + + return "\n".join(lines).rstrip() + "\n" + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--slug", required=True) + parser.add_argument("--local-repo", default="") + parser.add_argument("--local-issue-number", default="") + parser.add_argument("--output", type=Path, required=True) + args = parser.parse_args() + + if not ONBOARDING_FILE.is_file(): + print("Error: ONBOARDING.md not found.", file=sys.stderr) + return 1 + + item = extract_item(ONBOARDING_FILE.read_text(encoding="utf-8"), args.slug) + if not item["title"]: + print(f"Error: slug '{args.slug}' not found in ONBOARDING.md.", file=sys.stderr) + return 1 + + comment = render_comment(item, args.slug, args.local_repo, args.local_issue_number) + args.output.write_text(comment, encoding="utf-8") + print(f"Wrote {args.output}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/generate_status_report.py b/scripts/generate_status_report.py new file mode 100644 index 0000000..ff52916 --- /dev/null +++ b/scripts/generate_status_report.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python3 +"""Generate a status-report comment from ONBOARDING.md for the CNCF onboarding issue. + +The CNCF onboarding issue asks projects to "Quote reply" the original checklist +and update it as items complete. This renders a matching checklist (grouped by +the same categories) with evidence links, ready to paste as a comment. +""" + +import json +import re +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +ITEMS_FILE = ROOT / ".github" / "onboarding-items.json" +ONBOARDING_FILE = ROOT / "ONBOARDING.md" +OUTPUT_FILE = ROOT / "ONBOARDING-STATUS.md" + +CHECKLIST_LINE = re.compile(r"^- \[([ xX])\].*(.*)$") +EVIDENCE_HEADING = re.compile(r"^\*\*Evidence:\*\*\s*$") +ITEM_HEADING = re.compile(r"^### ([a-z0-9-]+)\s*$") +ANY_HEADING = re.compile(r"^#{1,6}\s") + + +def parse_onboarding(text: str): + """Return {slug: {"checked": bool, "tracking": str, "evidence": str}}.""" + results = {} + current_slug = None + collecting_evidence = False + evidence_lines: list[str] = [] + + def flush(): + if current_slug is not None: + results[current_slug]["evidence"] = "\n".join(evidence_lines).strip() + + for line in text.splitlines(): + heading = ITEM_HEADING.match(line) + if heading: + flush() + current_slug = heading.group(1) + results[current_slug] = {"checked": False, "tracking": "", "evidence": ""} + collecting_evidence = False + evidence_lines = [] + continue + + if current_slug is None: + continue + + # A category heading (## ...) ends the current item's evidence block + # even though it isn't itself a new item heading (### ...). + if ANY_HEADING.match(line): + collecting_evidence = False + continue + + match = CHECKLIST_LINE.match(line) + if match: + results[current_slug]["checked"] = match.group(1).lower() == "x" + results[current_slug]["tracking"] = match.group(3).strip() + continue + + if EVIDENCE_HEADING.match(line.strip()): + collecting_evidence = True + continue + + if collecting_evidence: + stripped = line.strip() + if stripped and not (stripped.startswith("_") and stripped.endswith("_")): + evidence_lines.append(stripped) + + flush() + return results + + +def render_report(data: dict, statuses: dict) -> str: + lines = ["", ""] + lines.append(f"# Onboarding status update — {data.get('project_name', 'Project')}") + lines.append("") + + total = 0 + done = 0 + for category in data["categories"]: + lines.append(f"## {category['heading']}") + lines.append("") + for item in category["items"]: + slug = item["slug"] + status = statuses.get(slug, {}) + checked = status.get("checked", False) + total += 1 + done += 1 if checked else 0 + box = "x" if checked else " " + lines.append(f"- [{box}] {item['title']}") + evidence = status.get("evidence", "") + if evidence: + for evidence_line in evidence.splitlines(): + lines.append(f" {evidence_line}") + lines.append("") + + percent = round((done / total) * 100) if total else 0 + lines.insert(2, f"**Progress: {done} / {total} items complete ({percent}%)**") + lines.insert(3, "") + return "\n".join(lines).rstrip() + "\n" + + +def main() -> int: + if not ITEMS_FILE.is_file() or not ONBOARDING_FILE.is_file(): + print("Error: run from repository root; ONBOARDING.md or onboarding-items.json missing.", file=sys.stderr) + return 1 + + data = json.loads(ITEMS_FILE.read_text()) + metadata_path = ROOT / ".github" / "project-metadata.json" + if metadata_path.is_file(): + data["project_name"] = json.loads(metadata_path.read_text()).get("project_name", "Project") + + statuses = parse_onboarding(ONBOARDING_FILE.read_text()) + report = render_report(data, statuses) + OUTPUT_FILE.write_text(report, encoding="utf-8") + print(f"Wrote {OUTPUT_FILE}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/render_onboarding_template.py b/scripts/render_onboarding_template.py new file mode 100644 index 0000000..c631c21 --- /dev/null +++ b/scripts/render_onboarding_template.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +"""Render ONBOARDING.md from .github/onboarding-items.json (template maintenance). + +Run this after editing onboarding-items.json to regenerate ONBOARDING.md. +Re-running is safe before bootstrap (placeholders only); after bootstrap, +regenerating would reset Issue/PR links, so only run this when adding or +restructuring checklist items, not during normal use. +""" + +import json +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +from checklist_tracking import issue_token_placeholder # noqa: E402 + +ROOT = Path(__file__).resolve().parent.parent +ITEMS_FILE = ROOT / ".github" / "onboarding-items.json" + + +def checklist_line(slug: str, title: str) -> str: + return f"- [ ] {title} {issue_token_placeholder(slug)}" + + +def flag_prefix(item: dict) -> str: + if item.get("critical"): + return "[Critical] " + if item.get("staff"): + return "[CNCF Staff] " + if item.get("optional"): + return "[Optional] " + return "" + + +def render() -> str: + data = json.loads(ITEMS_FILE.read_text()) + source_issue = data.get("source_issue", "") + + lines = [ + "# CNCF Sandbox Onboarding", + "", + f"Single source of truth for completing [CNCF Sandbox onboarding]({source_issue}) after your project's TOC acceptance vote.", + "", + "**Work here only:** each item shows `(Issue: …)` until you open a PR with `Closes #N`; automation then checks the box and shows `(PR: …)` here and in [README.md](README.md). Edit evidence below; do not edit README or toggle boxes yourself.", + "", + "Onboarding should be completed within **one month** of acceptance. When everything is done (or intentionally N/A), run `./scripts/generate-status-report.sh` to produce an update for your CNCF onboarding issue.", + "", + "---", + "", + ] + + for category in data["categories"]: + lines.append(f"## {category['heading']}") + lines.append("") + if category.get("intro"): + lines.append(category["intro"]) + lines.append("") + + for item in category["items"]: + slug = item["slug"] + title = flag_prefix(item) + item["title"] + + lines.append(f"### {slug}") + lines.append("") + lines.append("") + lines.extend(item["guide"].splitlines()) + lines.append("") + lines.append("") + lines.append(checklist_line(slug, item["title"])) + lines.append("") + lines.append("**Evidence:**") + lines.append("") + lines.append(item.get("evidence_hint", "_Notes or links here._")) + lines.append("") + + return "\n".join(lines).rstrip() + "\n" + + +if __name__ == "__main__": + out = ROOT / "ONBOARDING.md" + out.write_text(render()) + print(f"Wrote {out}")