diff --git a/.github/ISSUE_TEMPLATE/01_bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml similarity index 100% rename from .github/ISSUE_TEMPLATE/01_bug_report.yml rename to .github/ISSUE_TEMPLATE/bug_report.yml diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 68d03cd3..56fe3958 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,4 +1,12 @@ blank_issues_enabled: true + +issue_templates: + - bug_report.yml + - feature_request.yml + - planning_epic.yml + - planning_feature.yml + - planning_task.yml + contact_links: - name: Kubeflow Documentation url: https://www.kubeflow.org/ diff --git a/.github/ISSUE_TEMPLATE/02_feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml similarity index 100% rename from .github/ISSUE_TEMPLATE/02_feature_request.yml rename to .github/ISSUE_TEMPLATE/feature_request.yml diff --git a/.github/ISSUE_TEMPLATE/planning_epic.yml b/.github/ISSUE_TEMPLATE/planning_epic.yml new file mode 100644 index 00000000..7b79c0a2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/planning_epic.yml @@ -0,0 +1,27 @@ +name: "[Planning] Epic" +description: "🛑 Only intended to be used by Kubeflow project managers" +title: "[EPIC] " +labels: + - "kind/plan-epic" +assignees: [] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to create an epic! + - type: checkboxes + id: certification + attributes: + label: Certification + description: Please confirm your role + options: + - label: I certify I am an Epic Owner for Kubeflow Notebooks 2.0 and expected to create planning-related issues. + required: true + - type: textarea + id: user-story + attributes: + label: User Story + description: Describe the feature from the end user's perspective, including who they are, what they want to achieve, and why it's important. + placeholder: As a [type of user], I want [goal] so that [benefit] + validations: + required: true \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/planning_feature.yml b/.github/ISSUE_TEMPLATE/planning_feature.yml new file mode 100644 index 00000000..7751a5cc --- /dev/null +++ b/.github/ISSUE_TEMPLATE/planning_feature.yml @@ -0,0 +1,48 @@ +name: "[Planning] Feature" +description: "🛑 Only intended to be used by Kubeflow project managers" +title: "[FEATURE] " +labels: + - "kind/plan-feature" +assignees: [] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to create a feature request! + - type: checkboxes + id: certification + attributes: + label: Certification + description: Please confirm your role + options: + - label: I certify I am an Epic Owner for Kubeflow Notebooks 2.0 and expected to create planning-related issues. + required: true + - type: textarea + id: motivation + attributes: + label: Motivation + description: Explain the reasoning behind this feature. What problem does it solve or what value does it add? + placeholder: This feature will help users by... + validations: + required: true + - type: textarea + id: design + attributes: + label: High Level Design / Mock-ups + description: Provide a top-level overview of the proposed solution. Include diagrams, rough mockups, or architectural notes if helpful. + placeholder: | + [Add your design details here] + - Consider including diagrams + - Add mockups if available + - Describe the architecture + - type: textarea + id: acceptance-criteria + attributes: + label: Acceptance Criteria + description: Define what needs to be true for this feature to be considered complete. Be as clear and measurable as possible. + placeholder: | + - [ ] Criterion 1 + - [ ] Criterion 2 + - [ ] Criterion 3 + validations: + required: true \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/planning_task.yml b/.github/ISSUE_TEMPLATE/planning_task.yml new file mode 100644 index 00000000..2663cdb3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/planning_task.yml @@ -0,0 +1,38 @@ +name: "[Planning] Task" +description: "🛑 Only intended to be used by Kubeflow project managers" +title: "[TASK] " +labels: + - "kind/plan-task" +assignees: [] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to create a task! + - type: checkboxes + id: certification + attributes: + label: Certification + description: Please confirm your role + options: + - label: I certify I am an Epic Owner for Kubeflow Notebooks 2.0 and expected to create planning-related issues. + required: true + - type: textarea + id: description + attributes: + label: Description + description: Provide a brief explanation of what this task involves and why it's needed. Focus on the scope and intent of the work. + placeholder: This task will... + validations: + required: true + - type: textarea + id: acceptance-criteria + attributes: + label: Acceptance Criteria + description: List the specific, measurable conditions that must be met for the task to be considered complete. + placeholder: | + - [ ] Criterion 1 + - [ ] Criterion 2 + - [ ] Criterion 3 + validations: + required: true \ No newline at end of file diff --git a/.github/workflows/slash-commands.yaml b/.github/workflows/slash-commands.yaml new file mode 100644 index 00000000..441436e8 --- /dev/null +++ b/.github/workflows/slash-commands.yaml @@ -0,0 +1,243 @@ +name: Slash Command Handler + +on: + issue_comment: + types: [created] + +permissions: + issues: write + +jobs: + handle-slash-command: + if: | + github.event.issue.pull_request == null + && contains('["thesuperzapper", "ederign", "andyatmiami", "paulovmr", "jenny-s51", "harshad16", "thaorell", "kimwnasptd", "liavweiss"]', github.event.comment.user.login) + && ( + contains(github.event.comment.body, '/add-sub-issue') + || contains(github.event.comment.body, '/remove-sub-issue') + ) + runs-on: ubuntu-latest + + steps: + - name: Handle slash commands + id: handle-commands + uses: actions/github-script@v7 + with: + script: | + const parseIssueNumber = (input) => { + if (!input) return null; + + // Handle plain number + if (/^\d+$/.test(input)) { + return input; + } + + // Handle #number format + const hashMatch = input.match(/^#(\d+)$/); + if (hashMatch) { + return hashMatch[1]; + } + + // Handle URL format + const urlMatch = input.match(/\/issues\/(\d+)$/); + if (urlMatch) { + return urlMatch[1]; + } + + throw new Error(`Could not parse issue number from input: '${input}'`); + }; + + const getIssueNodeId = async (owner, repo, issueNumber) => { + const response = await github.graphql(` + query { + repository(owner: "${owner}", name: "${repo}") { + issue(number: ${issueNumber}) { + id + title + } + } + } + `); + return { + id: response.repository.issue.id, + title: response.repository.issue.title + }; + }; + + const performSubIssueMutation = async (action, parentIssueNodeId, childIssueNodeId) => { + const mutationField = `${action}SubIssue`; + + const mutation = ` + mutation { + ${mutationField}(input: { + issueId: "${parentIssueNodeId}", + subIssueId: "${childIssueNodeId}" + }) { + clientMutationId + issue { + id + title + } + subIssue { + id + title + } + } + } + `; + + try { + const response = await github.graphql(mutation); + return response; + } catch (error) { + throw new Error(error.message); + } + }; + + const collectSubIssueOperations = async (line, action, owner, repo) => { + const commandPrefix = `/${action}-sub-issue`; + if (!line.startsWith(commandPrefix)) return []; + + const args = line.replace(commandPrefix, '').trim().split(/\s+/); + const operations = []; + + for (const issue of args) { + const childIssueNumber = parseIssueNumber(issue); + const childIssue = await getIssueNodeId(owner, repo, childIssueNumber); + operations.push({ + action, + issueNumber: childIssueNumber, + title: childIssue.title, + nodeId: childIssue.id + }); + } + + return operations; + }; + + const formatOperationsList = (operations, action) => { + if (operations.length === 0) return []; + + return [ + `### ${action} Sub-issues:`, + ...operations.map(op => `- #${op.issueNumber}`), + '' + ]; + }; + + try { + const { owner, repo } = context.repo; + const parentIssueNumber = context.payload.issue.number; + const commentBody = context.payload.comment.body; + + // Get parent issue node ID and title + const parentIssue = await getIssueNodeId(owner, repo, parentIssueNumber); + + // Collect all operations first + const lines = commentBody.split('\n'); + const operations = []; + + for (const line of lines) { + operations.push(...await collectSubIssueOperations(line, 'add', owner, repo)); + operations.push(...await collectSubIssueOperations(line, 'remove', owner, repo)); + } + + if (operations.length === 0) { + return; // No valid operations found + } + + // Create preview comment + const previewBodyParts = [ + ':mag: **Sub-issue Operation Preview**', + '', + `The following operations will be performed on issue #${parentIssueNumber} (${parentIssue.title}) at the request of @${context.payload.comment.user.login}:`, + '' + ]; + + // Group operations by action for display + const addOperations = operations.filter(op => op.action === 'add'); + const removeOperations = operations.filter(op => op.action === 'remove'); + + previewBodyParts.push( + ...formatOperationsList(addOperations, 'Adding'), + ...formatOperationsList(removeOperations, 'Removing') + ); + + previewBodyParts.push('_This is a preview of the changes. The actual operations will be executed in the background._'); + + // Post preview comment + const previewComment = await github.rest.issues.createComment({ + owner, + repo, + issue_number: parentIssueNumber, + body: previewBodyParts.join('\n') + }); + + // Execute operations in original order + for (const op of operations) { + await performSubIssueMutation(op.action, parentIssue.id, op.nodeId); + } + + // Post success comment + await github.rest.issues.createComment({ + owner, + repo, + issue_number: parentIssueNumber, + body: [ + ':white_check_mark: **GitHub Action Succeeded**', + '', + `All [sub-issue operations](${previewComment.data.html_url}) requested by @${context.payload.comment.user.login} have been completed successfully.`, + '' + ].join('\n') + }); + + } catch (error) { + core.setOutput('error_message', error.message); + core.setFailed(error.message); + } + + - name: Post error comment if failure + if: failure() + uses: actions/github-script@v7 + with: + script: | + try { + const commentUrl = context.payload.comment.html_url; + const runId = context.runId; + const { owner, repo } = context.repo; + const errorMessage = `${{ steps.handle-commands.outputs.error_message }}`; + + const errorBodyParts = [ + ':x: **GitHub Action Failed**', + '', + `The workflow encountered an error while processing [your comment](${commentUrl}) to manage sub-issues.`, + '', + `:point_right: [View the run](https://github.com/${owner}/${repo}/actions/runs/${runId})`, + '' + ]; + + if (errorMessage && errorMessage !== '') { + errorBodyParts.push( + '
', + 'Error details', + '', + '```', + errorMessage, + '```', + '', + '
', + '' + ); + } + + errorBodyParts.push('Please check the logs and try again, or open a bug report if the issue persists.'); + + await github.rest.issues.createComment({ + owner, + repo, + issue_number: context.payload.issue.number, + body: errorBodyParts.join('\n') + }); + } catch (error) { + core.setFailed(`Failed to post error comment: ${error.message}`); + } diff --git a/.github/workflows/validate-planning-label.yml b/.github/workflows/validate-planning-label.yml new file mode 100644 index 00000000..68f6b851 --- /dev/null +++ b/.github/workflows/validate-planning-label.yml @@ -0,0 +1,126 @@ +name: Validate Planning Issue + +on: + issues: + types: [opened, labeled] + +env: + AUTHORIZED_USERS: '["thesuperzapper", "ederign", "andyatmiami", "paulovmr", "jenny-s51", "harshad16", "thaorell", "kimwnasptd", "liavweiss"]' + +permissions: + issues: write + +jobs: + validate-issue: + if: | + (github.event.action == 'labeled' && (github.event.label.name == 'kind/plan-epic' || github.event.label.name == 'kind/plan-feature' || github.event.label.name == 'kind/plan-task')) || + (github.event.action == 'opened' && (contains(github.event.issue.labels.*.name, 'kind/plan-epic') || contains(github.event.issue.labels.*.name, 'kind/plan-feature') || contains(github.event.issue.labels.*.name, 'kind/plan-task'))) + runs-on: ubuntu-latest + steps: + - name: Log trigger + uses: actions/github-script@v7 + with: + script: | + console.log(`Action triggered by: ${context.eventName} event with action: ${context.payload.action}`); + if (context.payload.action === 'labeled') { + console.log(`Label added: ${context.payload.label.name}`); + } else if (context.payload.action === 'opened') { + console.log(`Issue opened with labels: ${context.payload.issue.labels.map(l => l.name).join(', ')}`); + } + + - name: Handle labeled action + if: github.event.action == 'labeled' + uses: actions/github-script@v7 + with: + script: | + const AUTHORIZED_USERS = JSON.parse(process.env.AUTHORIZED_USERS); + const actor = context.actor; + const issueNumber = context.issue.number; + const addedLabel = context.payload.label.name; + + // First check user authorization + if (!AUTHORIZED_USERS.includes(actor)) { + // Remove the planning label + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + name: addedLabel + }); + + // Add a comment explaining why the label was removed + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + body: `@${actor} You are not authorized to add planning labels. Only authorized users can add planning labels to issues.` + }); + + core.setFailed(`User ${actor} is not authorized to add planning labels`); + } + + console.log(`User ${actor} is authorized to add planning labels`); + + // Then check planning label requirements + const { data: issue } = await github.rest.issues.get({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber + }); + + const currentLabels = issue.labels.map(label => label.name); + const planningLabels = ['kind/plan-epic', 'kind/plan-feature', 'kind/plan-task']; + const presentPlanningLabels = currentLabels.filter(label => planningLabels.includes(label)); + + if (presentPlanningLabels.length !== 1) { + // Remove the planning label + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + name: addedLabel + }); + + // Add a comment explaining why the label was removed + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + body: `@${actor} Planning labels require exactly one planning label to be present: ${planningLabels.join(', ')}. Please ensure only one planning label is applied.` + }); + + core.setFailed(`Planning labels require exactly one of: ${planningLabels.join(', ')}`); + } + + console.log(`Issue has valid planning label: ${presentPlanningLabels[0]}`); + + - name: Handle opened action + if: github.event.action == 'opened' + uses: actions/github-script@v7 + with: + script: | + const AUTHORIZED_USERS = JSON.parse(process.env.AUTHORIZED_USERS); + const actor = context.actor; + const issueNumber = context.issue.number; + + if (!AUTHORIZED_USERS.includes(actor)) { + // Add a comment explaining why the issue will be closed + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + body: `@${actor} You are not authorized to create planning issues. Only authorized users can create planning issues.` + }); + + // Close the issue + await github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + state: 'closed' + }); + + core.setFailed(`User ${actor} is not authorized to create planning issues`); + } + + console.log(`User ${actor} is authorized to create planning issues`); diff --git a/ADOPTERS.md b/ADOPTERS.md new file mode 100644 index 00000000..d383da22 --- /dev/null +++ b/ADOPTERS.md @@ -0,0 +1,9 @@ +# Adopters of Kubeflow Notebooks + +Below are the adopters of the Notebooks project. If you are using Notebooks +please add yourself into the following list by a pull request. +Please keep the list in alphabetical order. + +| Organization | Contact | Description of Use | +|--------------------------------------------------|-------------------------------------------------------------------|--------------------------------------------------------| +| [Red Hat](https://www.redhat.com/) | [@franciscojavierarceo](https://github.com/franciscojavierarceo) | Kubeflow Notebooks is part of Red Hat OpenShift AI. | diff --git a/README.md b/README.md index bb9f8c2c..c5264ce9 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,13 @@ # Kubeflow Notebooks +[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/9942/badge)](https://www.bestpractices.dev/projects/9942) [Kubeflow Notebooks](https://www.kubeflow.org/docs/components/notebooks/overview/) lets you run web-based development environments on your Kubernetes cluster by running them inside Pods. > ⚠️ __Note__ ⚠️ -> +> > We are currently moving the _Kubeflow Notebooks 1.0_ codebase from [`kubeflow/kubeflow`](https://github.com/kubeflow/kubeflow) to this repository ([`kubeflow/notebooks`](https://github.com/kubeflow/notebooks)). > Please see [`kubeflow/kubeflow#7549`](https://github.com/kubeflow/kubeflow/issues/7549) for more information. -> +> > We are currently developing _Kubeflow Notebooks 2.0_ in this repository under the [`notebooks-v2`](https://github.com/kubeflow/notebooks/tree/notebooks-v2) branch. > Please see [`kubeflow/notebooks#85`](https://github.com/kubeflow/notebooks/issues/85) for more information. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..ccc09b1e --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,64 @@ +# Security Policy + +## Supported Versions + +Kubeflow Notebooks versions are expressed as `vX.Y.Z`, where X is the major version, +Y is the minor version, and Z is the patch version, following the +[Semantic Versioning](https://semver.org/) terminology. + +The Kubeflow Notebooks project maintains release branches for the most recent two minor releases. +Applicable fixes, including security fixes, may be backported to those two release branches, +depending on severity and feasibility. + +Users are encouraged to stay updated with the latest releases to benefit from security patches and +improvements. + +## Reporting a Vulnerability + +We're extremely grateful for security researchers and users that report vulnerabilities to the +Kubeflow Open Source Community. All reports are thoroughly investigated by Kubeflow projects owners. + +You can use the following ways to report security vulnerabilities privately: + +- Using the Kubeflow Notebooks repository [GitHub Security Advisory](https://github.com/kubeflow/notebooks/security/advisories/new). +- Using our private Kubeflow Steering Committee mailing list: ksc@kubeflow.org. + +Please provide detailed information to help us understand and address the issue promptly. + +## Disclosure Process + +**Acknowledgment**: We will acknowledge receipt of your report within 10 business days. + +**Assessment**: The Kubeflow projects owners will investigate the reported issue to determine its +validity and severity. + +**Resolution**: If the issue is confirmed, we will work on a fix and prepare a release. + +**Notification**: Once a fix is available, we will notify the reporter and coordinate a public +disclosure. + +**Public Disclosure**: Details of the vulnerability and the fix will be published in the project's +release notes and communicated through appropriate channels. + +## Prevention Mechanisms + +Kubeflow Notebooks employs several measures to prevent security issues: + +**Code Reviews**: All code changes are reviewed by maintainers to ensure code quality and security. + +**Dependency Management**: Regular updates and monitoring of dependencies (e.g. Dependabot) to +address known vulnerabilities. + +**Continuous Integration**: Automated testing and security checks are integrated into the CI/CD pipeline. + +**Image Scanning**: Container images are scanned for vulnerabilities. + +## Communication Channels + +For the general questions please join the following resources: + +- Kubeflow [Slack channels](https://www.kubeflow.org/docs/about/community/#kubeflow-slack-channels). + +- Kubeflow discuss [mailing list](https://www.kubeflow.org/docs/about/community/#kubeflow-mailing-list). + +Please **do not report** security vulnerabilities through public channels.