Add Stanza NLP tool and data manager #292
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: Auto-Label Ready for Review | |
| on: | |
| issue_comment: | |
| types: [created] | |
| permissions: {} | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} | |
| jobs: | |
| check-and-label: | |
| if: | | |
| github.event.issue.pull_request != null && | |
| contains(github.event.comment.body, 'please review') | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - name: Generate App Token (repo-scoped) | |
| id: repo-token | |
| uses: actions/create-github-app-token@v3 | |
| with: | |
| client-id: ${{ secrets.APP_CLIENT_ID }} | |
| private-key: ${{ secrets.APP_PRIVATE_KEY }} | |
| permission-issues: write | |
| permission-pull-requests: write | |
| - name: Check PR Status and Apply Label | |
| id: check-pr | |
| uses: actions/github-script@v9 | |
| with: | |
| github-token: ${{ steps.repo-token.outputs.token }} | |
| script: | | |
| const owner = context.repo.owner; | |
| const repo = context.repo.repo; | |
| const pull_number = context.payload.issue.number; | |
| // 1. Fetch PR details | |
| const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number }); | |
| if (pr.draft) { | |
| await github.rest.issues.createComment({ | |
| owner, | |
| repo, | |
| issue_number: pull_number, | |
| body: `⏸️ This PR is still a draft. Please mark it as ready for review before requesting a review.` | |
| }); | |
| console.log("PR is a draft. Skipping."); | |
| return; | |
| } | |
| // 2. Check for unresolved review threads | |
| const query = ` | |
| query($owner: String!, $repo: String!, $pull_number: Int!, $cursor: String) { | |
| repository(owner: $owner, name: $repo) { | |
| pullRequest(number: $pull_number) { | |
| reviewThreads(first: 100, after: $cursor) { | |
| nodes { isResolved } | |
| pageInfo { hasNextPage endCursor } | |
| } | |
| } | |
| } | |
| } | |
| `; | |
| let cursor = null; | |
| let hasUnresolved = false; | |
| let hasNextPage = false; | |
| do { | |
| const result = await github.graphql(query, { owner, repo, pull_number, cursor }); | |
| const { nodes, pageInfo } = result.repository.pullRequest.reviewThreads; | |
| if (nodes.some(t => !t.isResolved)) { | |
| hasUnresolved = true; | |
| break; | |
| } | |
| cursor = pageInfo.endCursor; | |
| hasNextPage = pageInfo.hasNextPage; | |
| } while (hasNextPage); | |
| if (hasUnresolved) { | |
| await github.rest.issues.createComment({ | |
| owner, | |
| repo, | |
| issue_number: pull_number, | |
| body: `💬 There are unresolved review comments on this PR. Please resolve them before requesting a review.` | |
| }); | |
| console.log("Unresolved review comments exist. Skipping."); | |
| return; | |
| } | |
| // 3. Check CI - exclude this workflow's own run | |
| const { data: checkRunsData } = await github.rest.checks.listForRef({ | |
| owner, | |
| repo, | |
| ref: pr.head.sha, | |
| per_page: 100, | |
| }); | |
| const relevant = checkRunsData.check_runs.filter(r => | |
| r.name !== context.workflow | |
| ); | |
| // No check runs yet → not ready | |
| if (relevant.length === 0) { | |
| await github.rest.issues.createComment({ | |
| owner, | |
| repo, | |
| issue_number: pull_number, | |
| body: `⏳ No CI checks have reported for this PR yet. Please wait for CI to run and then try again.` | |
| }); | |
| console.log("No check runs found. Skipping."); | |
| return; | |
| } | |
| // Any check still running → not ready | |
| const hasIncomplete = relevant.some(r => r.status !== 'completed'); | |
| if (hasIncomplete) { | |
| await github.rest.issues.createComment({ | |
| owner, | |
| repo, | |
| issue_number: pull_number, | |
| body: `⏳ Some CI checks are still running. Please wait for them to complete before requesting a review.` | |
| }); | |
| console.log("Some CI checks are still running. Skipping."); | |
| return; | |
| } | |
| // Any check failed → report and stop | |
| const failing = relevant.filter(r => | |
| r.conclusion !== 'success' && | |
| r.conclusion !== 'skipped' && | |
| r.conclusion !== 'neutral' | |
| ); | |
| if (failing.length > 0) { | |
| const lines = failing | |
| .map(r => `- **${r.name}**: ${r.conclusion} → [view](${r.html_url})`) | |
| .join('\n'); | |
| await github.rest.issues.createComment({ | |
| owner, | |
| repo, | |
| issue_number: pull_number, | |
| body: `❌ Please check the CI errors before requesting a review.\n\nFailing checks:\n\n${lines}\n\n[View full CI summary](https://github.com/${owner}/${repo}/pull/${pull_number}/checks)` | |
| }); | |
| return; | |
| } | |
| // 4. All good - add the label | |
| console.log("All checks passed. Adding 'ready-for-review' label."); | |
| await github.rest.issues.addLabels({ | |
| owner, | |
| repo, | |
| issue_number: pull_number, | |
| labels: ['ready-for-review'] | |
| }); | |
| core.setOutput('labeled', 'true'); | |
| - name: Generate App Token (project-scoped) | |
| if: steps.check-pr.outputs.labeled == 'true' | |
| id: project-token | |
| uses: actions/create-github-app-token@v3 | |
| with: | |
| client-id: ${{ secrets.APP_CLIENT_ID }} | |
| private-key: ${{ secrets.APP_PRIVATE_KEY }} | |
| permission-organization-projects: write | |
| - name: Move PR to "Needs Review" on Project Board | |
| if: steps.check-pr.outputs.labeled == 'true' | |
| uses: actions/github-script@v9 | |
| with: | |
| github-token: ${{ steps.project-token.outputs.token }} | |
| script: | | |
| const org = 'galaxyproject'; | |
| const projectNumber = 81; | |
| // Get project metadata | |
| const { organization } = await github.graphql(` | |
| query($org: String!, $number: Int!) { | |
| organization(login: $org) { | |
| projectV2(number: $number) { | |
| id | |
| fields(first: 20) { | |
| nodes { | |
| ... on ProjectV2SingleSelectField { | |
| id | |
| name | |
| options { id name } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| `, { org, number: projectNumber }); | |
| const project = organization.projectV2; | |
| const statusField = project.fields.nodes.find(f => f.name === 'Status'); | |
| const option = statusField.options.find(o => o.name === 'Needs Review'); | |
| // Find the PR's item ID on the project board | |
| const prNodeId = context.payload.issue.node_id; | |
| const { node } = await github.graphql(` | |
| query($id: ID!) { | |
| node(id: $id) { | |
| ... on PullRequest { | |
| projectItems(first: 10) { | |
| nodes { | |
| id | |
| project { id } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| `, { id: prNodeId }); | |
| const item = node.projectItems.nodes.find(i => i.project.id === project.id); | |
| if (!item) { console.log('PR not found on project board'); return; } | |
| // Update status | |
| await github.graphql(` | |
| mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) { | |
| updateProjectV2ItemFieldValue(input: { | |
| projectId: $projectId | |
| itemId: $itemId | |
| fieldId: $fieldId | |
| value: { singleSelectOptionId: $optionId } | |
| }) { projectV2Item { id } } | |
| } | |
| `, { | |
| projectId: project.id, | |
| itemId: item.id, | |
| fieldId: statusField.id, | |
| optionId: option.id | |
| }); | |
| console.log('PR moved to "Needs Review".'); |