diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index bb961996016..d8944ceeaff 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -11,4 +11,4 @@ # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* -blank_issues_enabled: true +blank_issues_enabled: false diff --git a/.github/PULL_REQUEST_TEMPLATE/bug_fix.md b/.github/PULL_REQUEST_TEMPLATE/bug_fix.md index c552764dd34..787442fd1f1 100644 --- a/.github/PULL_REQUEST_TEMPLATE/bug_fix.md +++ b/.github/PULL_REQUEST_TEMPLATE/bug_fix.md @@ -17,3 +17,5 @@ > ticket yet, create one via [this issue template](../ISSUE_TEMPLATE/new?template=bug_fix.md). closes [ISSUE-NUMBER] (bugfix ticket) + + diff --git a/.github/PULL_REQUEST_TEMPLATE/improvement.md b/.github/PULL_REQUEST_TEMPLATE/improvement.md index 9777bbb132c..e304d2eff6f 100644 --- a/.github/PULL_REQUEST_TEMPLATE/improvement.md +++ b/.github/PULL_REQUEST_TEMPLATE/improvement.md @@ -17,3 +17,5 @@ > ticket yet, create one via [this issue template](../ISSUE_TEMPLATE/new?template=improvement.md). closes [ISSUE-NUMBER] (improvement ticket) + + diff --git a/.github/scripts/check_issue_type.py b/.github/scripts/check_issue_type.py new file mode 100644 index 00000000000..ffdde8fb96a --- /dev/null +++ b/.github/scripts/check_issue_type.py @@ -0,0 +1,149 @@ +# ******************************************************************************* +# Copyright (c) 2025 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +#!/usr/bin/env python3 + +import os +import json +import requests +import sys +from dotenv import load_dotenv + + +def set_output(name, value): + print(f"::set-output name={name}::{value}") + +def set_failed(message): + print(f"::error::{message}") + sys.exit(1) + +def set_output(name, value): + print(f"::set-output name={name}::{value}") + +def set_failed(message): + print(f"::error::{message}") + sys.exit(1) + +def main(): + linked_issue_numbers_str = os.environ.get('LINKED_ISSUES') + github_token = os.environ.get('GITHUB_TOKEN') + repo_owner = os.environ.get('GITHUB_REPOSITORY_OWNER') + repo_name = os.environ.get('GITHUB_REPOSITORY').split('/')[-1] + + if not github_token: + set_failed("GITHUB_TOKEN is not set. Ensure it's passed as an environment variable.") + + # Initialize linked_issue_numbers as an empty list + linked_issue_numbers = [] + + if not linked_issue_numbers_str: + print("No issues linked in the PR description. Skipping type check.") + set_output('found_bug_issue', 'false') + set_output('found_feature_issue', 'false') + set_output('found_task_issue', 'false') + set_output('other_issue_types', '[]') + return + + + try: + parsed_data = json.loads(linked_issue_numbers_str) + if isinstance(parsed_data, int): + linked_issue_numbers = [parsed_data] + elif isinstance(parsed_data, list): + linked_issue_numbers = parsed_data + else: + set_failed(f"Unexpected format for LINKED_ISSUES: {linked_issue_numbers_str}. Expected a JSON array or single number.") + except json.JSONDecodeError: + set_failed(f"Failed to parse LINKED_ISSUES as JSON: {linked_issue_numbers_str}.") + + + print(f"Linked issue numbers: {linked_issue_numbers}") + + found_bug_issue = False + found_feature_issue = False + found_task_issue = False + other_issue_types = [] + + graphql_url = "https://api.github.com/graphql" + headers = { + "Authorization": f"Bearer {github_token}", + "Content-Type": "application/json" + } + + for issue_number in linked_issue_numbers: + try: + query = """ + query GetIssueType($owner: String!, $repo: String!, $issueNumber: Int!) { + repository(owner: $owner, name: $repo) { + issue(number: $issueNumber) { + id + title + issueType { + name + } + } + } + } + """ + + variables = { + "owner": repo_owner, + "repo": repo_name, + "issueNumber": issue_number, + } + + response = requests.post(graphql_url, headers=headers, json={'query': query, 'variables': variables}) + response.raise_for_status() + result = response.json() + + if 'errors' in result: + set_failed(f"GraphQL errors for issue #{issue_number}: {result['errors']}") + + issue_data = result.get('data', {}).get('repository', {}).get('issue') + + if not issue_data: + print(f"Warning: Issue #{issue_number} not found in this repository or no data returned.") + other_issue_types.append({'number': issue_number, 'type': 'Issue Not Found'}) + continue + + issue_type_name = issue_data.get('issueType', {}).get('name') + + if issue_type_name: + type_value = issue_type_name + print(f"Issue #{issue_number} has Built-in Issue Type: {type_value}") + + if type_value == 'Bug': + found_bug_issue = True + elif type_value == 'Feature Request': + found_feature_issue = True + elif type_value == 'Task': + found_task_issue = True + else: + other_issue_types.append({'number': issue_number, 'type': type_value}) + else: + print(f"Issue #{issue_number} has no built-in 'issueType' specified.") + other_issue_types.append({'number': issue_number, 'type': 'No Built-in Issue Type'}) + + except requests.exceptions.RequestException as e: + set_failed(f"HTTP error fetching issue #{issue_number} type: {e}") + except Exception as e: + set_failed(f"An unexpected error occurred for issue #{issue_number}: {e}") + + set_output('found_issue', str(found_bug_issue or found_feature_issue).lower()) + set_output('found_feature_issue', str(found_feature_issue).lower()) + set_output('found_task_issue', str(found_task_issue).lower()) + set_output('other_issue_types', json.dumps(other_issue_types)) + + +if __name__ == "__main__": + main() diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml new file mode 100644 index 00000000000..6920e2a175b --- /dev/null +++ b/.github/workflows/pr-validation.yml @@ -0,0 +1,79 @@ +# ******************************************************************************* +# Copyright (c) 2024 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +name: Pr Validation check for bugs +on: + pull_request: + types: [edited, synchronize, opened, reopened] + + +jobs: + + verify_linked_bug_issue: + runs-on: ubuntu-latest + name: Ensure Pull Request has a linked issue. + permissions: + pages: write + contents: write + pull-requests: write + issues: write # Allow label creation + steps: + - name: Check PR Template + id: check_template + run: | + PR_BODY="${{ github.event.pull_request.body }}" + # Check if the PR body contains the unique identifier from your bug-fix.md template + if [[ "$PR_BODY" == *""* ]]; then + echo "PR uses bug-fix.md template. Proceeding with workflow." + echo "template_match=true" >> $GITHUB_OUTPUT + else + echo "PR does NOT use bug-fix.md template. Skipping subsequent steps." + echo "template_match=false" >> $GITHUB_OUTPUT + fi + + - name: Checkout code + if: steps.check_template.outputs.template_match == 'true' + uses: actions/checkout@v4.2.2 + + - name: Set up Python + if: steps.check_template.outputs.template_match == 'true' + uses: actions/setup-python@v5 + with: + python-version: '3.12' + - name: Install dependencies + if: steps.check_template.outputs.template_match == 'true' + run: pip install requests python-dotenv + + - name: Get issues + if: steps.check_template.outputs.template_match == 'true' + id: get-issues + uses: mondeja/pr-linked-issues-action@v2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Check type of each linked issue via GraphQL (Python) + if: steps.check_template.outputs.template_match == 'true' + id: issue_type_check + run: python ./.github/scripts/check_issue_type.py + env: + LINKED_ISSUES: ${{ steps.get-issues.outputs.issues }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # These are needed by the Python script to construct the GraphQL query variables + GITHUB_REPOSITORY_OWNER: ${{ github.repository_owner }} + GITHUB_REPOSITORY: ${{ github.repository }} + + - name: Fail if no Bug or Feature Request issue found for PR + if: steps.check_template.outputs.template_match == 'true' && steps.issue_type_check.outputs.found_issue == 'false' + run: | + echo "::error::This PR uses the improvement or bug-fix template, but no linked issue of type 'Bug' or "Feature Request" was found." + exit 1 # This will cause the workflow to fail