Closes #796 - Add Ontology item type support #1019
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Validate PR | |
| description: "Validate pull requests for code conventions, naming conventions, linked issues, and version bumps" | |
| on: | |
| pull_request: | |
| branches: ["main"] | |
| types: [opened, edited, synchronize, ready_for_review, labeled, unlabeled] | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| issues: write | |
| statuses: write | |
| jobs: | |
| format_ruff: | |
| if: ${{ github.event.action != 'labeled' && github.event.action != 'unlabeled' }} | |
| name: Code Formatted | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v2 | |
| - name: Set up Python | |
| uses: actions/setup-python@v2 | |
| with: | |
| python-version: "3.x" | |
| - name: Install dependencies | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install ruff | |
| - name: Run ruff format | |
| run: | | |
| if ruff format; then | |
| echo "✅ ruff format passed." | |
| else | |
| echo "❌ ruff format failed." | |
| exit 1 | |
| fi | |
| lint_ruff: | |
| if: ${{ github.event.action != 'labeled' && github.event.action != 'unlabeled' }} | |
| name: Code Linted | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v2 | |
| - name: Set up Python | |
| uses: actions/setup-python@v2 | |
| with: | |
| python-version: "3.x" | |
| - name: Install dependencies | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install ruff | |
| - name: Run ruff lint | |
| run: | | |
| if ruff check; then | |
| echo "✅ ruff lint passed." | |
| else | |
| echo "❌ ruff lint failed." | |
| exit 1 | |
| fi | |
| validate-version-bump: | |
| name: Proper Version Bump | |
| runs-on: ubuntu-latest | |
| outputs: | |
| is_version_bump: ${{ steps.version_bump_check.outputs.is_version_bump }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Validate Version Bump | |
| id: version_bump_check | |
| run: | | |
| set -e | |
| PR_TITLE="${{ github.event.pull_request.title }}" | |
| VERSION_REGEX='^v([0-9]+\.[0-9]+\.[0-9]+)$' | |
| BASE_REF="origin/main" | |
| CHANGED_FILES=$(git diff --name-only $BASE_REF...${{ github.event.pull_request.head.sha }}) | |
| VERSION_CHANGED=false | |
| TITLE_IS_VERSION=false | |
| OLD_VERSION="" | |
| NEW_VERSION="" | |
| # Get VERSION from main branch | |
| OLD_VERSION=$(git show origin/main:src/fabric_cicd/constants.py | grep '^VERSION = ' | sed -E 's/VERSION = "([^"]+)"/\1/') | |
| # Get VERSION from current branch | |
| NEW_VERSION=$(grep '^VERSION = ' src/fabric_cicd/constants.py | sed -E 's/VERSION = "([^"]+)"/\1/') | |
| if [ "$OLD_VERSION" != "$NEW_VERSION" ]; then | |
| VERSION_CHANGED=true | |
| fi | |
| if [[ "$PR_TITLE" =~ $VERSION_REGEX ]]; then | |
| TITLE_IS_VERSION=true | |
| fi | |
| if [ "$TITLE_IS_VERSION" = true ] || [ "$VERSION_CHANGED" = true ]; then | |
| echo "is_version_bump=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "is_version_bump=false" >> $GITHUB_OUTPUT | |
| echo "✅ Version bump validation passed." | |
| exit 0 | |
| fi | |
| # 1. If version is updated, title must match | |
| if [ "$VERSION_CHANGED" = true ]; then | |
| EXPECTED_TITLE="v$NEW_VERSION" | |
| if [ "$PR_TITLE" != "$EXPECTED_TITLE" ]; then | |
| echo "❌ PR title must be vX.X.X when VERSION is changed. Expected title: $EXPECTED_TITLE" | |
| exit 1 | |
| fi | |
| fi | |
| # 2. If title is version format, constants.py, changelog.md, and .changes/v<version>.md must be included in changed files | |
| # Note: Additional files (e.g., removed unreleased change files) are allowed alongside the required files | |
| if [[ "$PR_TITLE" =~ $VERSION_REGEX ]]; then | |
| VERSION_NUMBER="${BASH_REMATCH[1]}" | |
| REQUIRED_FILES=("src/fabric_cicd/constants.py" "docs/changelog.md" ".changes/v${VERSION_NUMBER}.md") | |
| MISSING_FILES=() | |
| for file in "${REQUIRED_FILES[@]}"; do | |
| if ! echo "$CHANGED_FILES" | grep -Fqx "$file"; then | |
| MISSING_FILES+=("$file") | |
| fi | |
| done | |
| if [ ${#MISSING_FILES[@]} -ne 0 ]; then | |
| echo "❌ The following required files must be included in a PR titled vX.X.X: ${MISSING_FILES[*]}" | |
| exit 1 | |
| fi | |
| fi | |
| echo "✅ Version bump validation passed." | |
| check-author-permissions: | |
| name: Check Author Permissions | |
| runs-on: ubuntu-latest | |
| outputs: | |
| skip_validation: ${{ steps.check_permissions.outputs.skip_validation }} | |
| permissions: | |
| pull-requests: read | |
| steps: | |
| - name: Check PR Author Collaborator Role | |
| id: check_permissions | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const prAuthor = context.payload.pull_request.user.login; | |
| const repo = context.repo; | |
| // Skip validation for dependabot | |
| if (prAuthor === 'dependabot[bot]') { | |
| console.log('✅ Dependabot PR detected. Skipping linked issue validation.'); | |
| core.setOutput('skip_validation', 'true'); | |
| return; | |
| } | |
| console.log(`Checking collaborator role for PR author: ${prAuthor}`); | |
| try { | |
| const { data: collaborator } = await github.rest.repos.getCollaboratorPermissionLevel({ | |
| owner: repo.owner, | |
| repo: repo.repo, | |
| username: prAuthor | |
| }); | |
| const roleName = collaborator.role_name; | |
| console.log(`Collaborator role for ${prAuthor}: ${roleName}`); | |
| // Skip validation for users with admin, maintain, or write role | |
| const skipValidation = ['admin', 'maintain', 'write'].includes(roleName); | |
| core.setOutput('skip_validation', skipValidation.toString()); | |
| if (skipValidation) { | |
| console.log(`✅ User ${prAuthor} has ${roleName} role. Validation job for linked issue will be skipped.`); | |
| } else { | |
| console.log(`ℹ️ User ${prAuthor} has ${roleName} role. Validation job for linked issue will run.`); | |
| } | |
| } catch (error) { | |
| console.log(`⚠️ Could not determine collaborator role for ${prAuthor}: ${error.message}`); | |
| console.log('Defaulting to running validation job for linked issue.'); | |
| core.setOutput('skip_validation', 'false'); | |
| } | |
| validate-linked-issue: | |
| name: Issue Linked | |
| runs-on: ubuntu-latest | |
| needs: [check-author-permissions, validate-version-bump] | |
| if: needs.check-author-permissions.outputs.skip_validation == 'false' && needs.validate-version-bump.outputs.is_version_bump == 'false' && !contains(github.event.pull_request.labels.*.name, 'skip changelog') | |
| permissions: | |
| pull-requests: read | |
| issues: read | |
| steps: | |
| - name: Validate Linked Issue | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const prNumber = context.issue.number; | |
| const repo = context.repo; | |
| // Get PR details | |
| const pr = await github.rest.pulls.get({ | |
| owner: repo.owner, | |
| repo: repo.repo, | |
| pull_number: prNumber | |
| }); | |
| const prTitle = pr.data.title || ''; | |
| // First, check for issues linked to the PR via GitHub's native linking | |
| let linkedIssues = []; | |
| try { | |
| // Use GraphQL to get linked issues | |
| const query = ` | |
| query($owner: String!, $repo: String!, $number: Int!) { | |
| repository(owner: $owner, name: $repo) { | |
| pullRequest(number: $number) { | |
| closingIssuesReferences(first: 10) { | |
| nodes { | |
| number | |
| } | |
| } | |
| } | |
| } | |
| } | |
| `; | |
| const result = await github.graphql(query, { | |
| owner: repo.owner, | |
| repo: repo.repo, | |
| number: prNumber | |
| }); | |
| linkedIssues = result.repository.pullRequest.closingIssuesReferences.nodes; | |
| if (linkedIssues.length > 0) { | |
| console.log(`✅ Found ${linkedIssues.length} linked issue(s): ${linkedIssues.map(issue => `#${issue.number}`).join(', ')}`); | |
| console.log('✅ Pull request is properly linked to an issue via GitHub linking.'); | |
| return; | |
| } | |
| } catch (error) { | |
| console.log('⚠️ Could not check for linked issues via GraphQL, falling back to text analysis:', error.message); | |
| } | |
| // Issue reference patterns - more specific to avoid false positives | |
| const keywordPatterns = [ | |
| /(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+#(\d+)/gi, | |
| /(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+https:\/\/github\.com\/[^\/]+\/[^\/]+\/issues\/(\d+)/gi | |
| ]; | |
| // Generic hash pattern (less specific, used as fallback) | |
| const hashPattern = /#(\d+)(?!\w)/g; | |
| let foundIssueNumbers = new Set(); | |
| // Check PR title for issue references | |
| const textToCheck = prTitle; | |
| // First, look for keyword-based references (more reliable) | |
| for (const pattern of keywordPatterns) { | |
| const matches = textToCheck.matchAll(pattern); | |
| for (const match of matches) { | |
| const issueNumber = match[1]; | |
| if (issueNumber && !isNaN(issueNumber)) { | |
| foundIssueNumbers.add(parseInt(issueNumber)); | |
| } | |
| } | |
| } | |
| // If no keyword-based references found, look for simple hash references | |
| if (foundIssueNumbers.size === 0) { | |
| const matches = textToCheck.matchAll(hashPattern); | |
| for (const match of matches) { | |
| const issueNumber = match[1]; | |
| if (issueNumber && !isNaN(issueNumber)) { | |
| foundIssueNumbers.add(parseInt(issueNumber)); | |
| } | |
| } | |
| } | |
| if (foundIssueNumbers.size === 0) { | |
| core.setFailed( | |
| '❌ This pull request must be linked to an issue. Please:\n' + | |
| '1. Reference an issue in the PR title using "Fixes #123", "Closes #456", or "Resolves #789"\n' + | |
| '2. Make sure the referenced issue exists in this repository\n\n' + | |
| 'See our contribution guidelines for more details.' | |
| ); | |
| return; | |
| } | |
| // Verify that the referenced issues actually exist | |
| let validIssueFound = false; | |
| const invalidIssues = []; | |
| for (const issueNumber of foundIssueNumbers) { | |
| try { | |
| await github.rest.issues.get({ | |
| owner: repo.owner, | |
| repo: repo.repo, | |
| issue_number: issueNumber | |
| }); | |
| validIssueFound = true; | |
| console.log(`✅ Found valid issue reference: #${issueNumber}`); | |
| } catch (error) { | |
| if (error.status === 404) { | |
| invalidIssues.push(issueNumber); | |
| console.log(`❌ Issue #${issueNumber} does not exist`); | |
| } | |
| } | |
| } | |
| if (!validIssueFound) { | |
| const invalidList = invalidIssues.length > 0 ? | |
| `\n\nInvalid issue references found: ${invalidIssues.map(n => `#${n}`).join(', ')}` : ''; | |
| core.setFailed( | |
| '❌ This pull request must be linked to a valid issue in this repository.' + | |
| invalidList + | |
| '\n\nPlease:\n' + | |
| '1. Create an issue first if one doesn\'t exist\n' + | |
| '2. Reference the issue in the PR title using "Fixes #123", "Closes #456", or "Resolves #789"\n' + | |
| '3. Make sure the issue number is correct\n\n' + | |
| 'See our contribution guidelines for more details.' | |
| ); | |
| return; | |
| } | |
| console.log('✅ Pull request is properly linked to an issue.'); |