Skip to content

Add Stanza NLP tool and data manager #292

Add Stanza NLP tool and data manager

Add Stanza NLP tool and data manager #292

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".');