fix(flink): add tenacity to flink extras in setup.py/pyproject.toml/uv.lock #1404
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: Create Linear Ticket for PR & Issue Review | |
| # Routes PRs and issues to the correct engineering team via Claude and creates Linear tickets. | |
| # | |
| # Requires secrets: | |
| # ANTHROPIC_KEY_FOR_ROUTING - Anthropic API key for LLM-based team routing | |
| # INGESTION_LINEAR_KEY - Linear API key | |
| on: | |
| pull_request_target: | |
| types: [opened, reopened, ready_for_review, closed] | |
| issues: | |
| types: [opened, reopened, closed] | |
| env: | |
| LINEAR_TEAM_CONFIG: | | |
| [ | |
| { | |
| "key": "ING", | |
| "team_id": "b1c9a278-522e-47cb-81af-8b860ead0c76", | |
| "assignee_id": "bb4daa2b-1748-4830-af1f-60198ebd9a63", | |
| "description": "Python ingestion framework, connectors, sources, and CLI tools for extracting metadata from external data sources. Any PR touching metadata-ingestion/ or metadata-ingestion-modules/ belongs to this team." | |
| }, | |
| { | |
| "key": "CAT", | |
| "team_id": "56af88f4-c7c2-40f6-9858-dcba33a5f55e", | |
| "assignee_id": "bb4daa2b-1748-4830-af1f-60198ebd9a63", | |
| "description": "DataHub frontend (React/TypeScript, datahub-web-react), GraphQL API layer (datahub-graphql-core), and application-layer GMS services including auth, search API, entity services, and REST/OpenAPI endpoints" | |
| }, | |
| { | |
| "key": "PFP", | |
| "team_id": "71e24163-bc3b-4dfb-bba5-3acb822830e3", | |
| "assignee_id": "854a61e3-dba9-4293-b527-f953bd742201", | |
| "description": "Platform infrastructure — Ebean/database storage layer, Elasticsearch indexing, Kafka consumers (MAE/MCE/PE), GMS infrastructure (factories, wiring), metadata-utils, Docker, and Kubernetes" | |
| }, | |
| { | |
| "key": "OBS", | |
| "team_id": "1a592e48-d6ba-4b92-a241-8172b568e9bb", | |
| "assignee_id": "2bd2434a-cf1e-47a4-8243-0e77ed19faf7", | |
| "description": "Data Quality and Assertions. Any PR touching the Assertions codebase belongs to this team." | |
| } | |
| ] | |
| jobs: | |
| handle-pr: | |
| if: github.event_name == 'pull_request_target' | |
| runs-on: ubuntu-latest | |
| continue-on-error: true | |
| timeout-minutes: 5 | |
| steps: | |
| - name: Mark Linear ticket as Done if PR closed | |
| if: github.event.action == 'closed' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| GH_REPO: ${{ github.repository }} | |
| LINEAR_API_KEY: ${{ secrets.INGESTION_LINEAR_KEY }} | |
| PR_NUMBER: ${{ github.event.pull_request.number }} | |
| run: | | |
| if ! COMMENTS_JSON=$(gh pr view "$PR_NUMBER" --json comments 2>/dev/null); then | |
| echo "⚠️ Failed to fetch PR comments for PR #$PR_NUMBER" | |
| exit 0 | |
| fi | |
| COMMENTS=$(echo "$COMMENTS_JSON" | jq -r '.comments[].body' 2>/dev/null || echo "") | |
| TICKET_ID=$(echo "$COMMENTS" | grep -oP 'Linear: \K([A-Z]+-\d+)' 2>/dev/null | head -n1 || echo "") | |
| if [ -z "$TICKET_ID" ]; then | |
| echo "⚠️ No Linear ticket found for PR #$PR_NUMBER" | |
| exit 0 | |
| fi | |
| TEAM_KEY=$(echo "$TICKET_ID" | grep -oP '^[A-Z]+') | |
| echo "Found Linear ticket: $TICKET_ID (team: $TEAM_KEY)" | |
| TICKET_RESPONSE=$(curl -s -X POST https://api.linear.app/graphql \ | |
| -H "Authorization: $LINEAR_API_KEY" \ | |
| -H "Content-Type: application/json" \ | |
| -d "{\"query\": \"query { issue(id: \\\"$TICKET_ID\\\") { id state { name } } }\"}") | |
| TICKET_UUID=$(echo "$TICKET_RESPONSE" | jq -r '.data.issue.id' 2>/dev/null || echo "") | |
| if [ -z "$TICKET_UUID" ] || [ "$TICKET_UUID" = "null" ]; then | |
| echo "❌ Could not find Linear ticket $TICKET_ID" | |
| exit 0 | |
| fi | |
| STATE_RESPONSE=$(curl -s -X POST https://api.linear.app/graphql \ | |
| -H "Authorization: $LINEAR_API_KEY" \ | |
| -H "Content-Type: application/json" \ | |
| -d "{\"query\": \"query { workflowStates(filter: { team: { key: { eq: \\\"$TEAM_KEY\\\" } }, name: { eq: \\\"Done\\\" } }) { nodes { id name } } }\"}") | |
| DONE_STATE_ID=$(echo "$STATE_RESPONSE" | jq -r '.data.workflowStates.nodes[0].id' 2>/dev/null || echo "") | |
| if [ -z "$DONE_STATE_ID" ] || [ "$DONE_STATE_ID" = "null" ]; then | |
| echo "❌ Could not find 'Done' state for team $TEAM_KEY" | |
| exit 0 | |
| fi | |
| UPDATE_RESPONSE=$(curl -s -X POST https://api.linear.app/graphql \ | |
| -H "Authorization: $LINEAR_API_KEY" \ | |
| -H "Content-Type: application/json" \ | |
| -d "{\"query\": \"mutation { issueUpdate(id: \\\"$TICKET_UUID\\\", input: { stateId: \\\"$DONE_STATE_ID\\\" }) { success issue { identifier state { name } } } }\"}") | |
| SUCCESS=$(echo "$UPDATE_RESPONSE" | jq -r '.data.issueUpdate.success' 2>/dev/null || echo "false") | |
| if [ "$SUCCESS" = "true" ]; then | |
| echo "✅ Marked $TICKET_ID as Done" | |
| else | |
| echo "❌ Failed to mark ticket as Done" | |
| echo "$UPDATE_RESPONSE" | jq . 2>/dev/null || echo "$UPDATE_RESPONSE" | |
| fi | |
| - name: Check PR status | |
| if: github.event.action != 'closed' | |
| id: check-pr | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| PR_DRAFT: ${{ github.event.pull_request.draft }} | |
| PR_AUTHOR: ${{ github.event.pull_request.user.login }} | |
| PR_NUMBER: ${{ github.event.pull_request.number }} | |
| TEAM_MEMBERS: ${{ vars.DATAHUB_TEAM_MEMBERS }} | |
| run: | | |
| if [ "$PR_DRAFT" = "true" ]; then | |
| echo "should_skip=true" >> "$GITHUB_OUTPUT" | |
| echo "⏭️ Skipping - PR is in draft" | |
| exit 0 | |
| fi | |
| # Deduplication guard - skip if a Linear ticket already exists | |
| if COMMENTS_JSON=$(gh pr view "$PR_NUMBER" --json comments 2>/dev/null); then | |
| EXISTING=$(echo "$COMMENTS_JSON" | jq -r '.comments[].body' 2>/dev/null | grep -oP 'Linear: [A-Z]+-\d+' | head -n1 || echo "") | |
| if [ -n "$EXISTING" ]; then | |
| echo "should_skip=true" >> "$GITHUB_OUTPUT" | |
| echo "⏭️ Linear ticket already exists ($EXISTING), skipping" | |
| exit 0 | |
| fi | |
| fi | |
| echo "should_skip=false" >> "$GITHUB_OUTPUT" | |
| if [ -z "$TEAM_MEMBERS" ]; then | |
| echo "⚠️ DATAHUB_TEAM_MEMBERS variable is not set — defaulting to Internal" | |
| echo "contributor_type=Internal" >> "$GITHUB_OUTPUT" | |
| elif echo "$TEAM_MEMBERS" | jq -e --arg actor "$PR_AUTHOR" '[.[].github] | map(ascii_downcase) | contains([($actor | ascii_downcase)])' > /dev/null 2>&1; then | |
| echo "contributor_type=Internal" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "contributor_type=External" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Route PR to team | |
| if: github.event.action != 'closed' && steps.check-pr.outputs.should_skip == 'false' | |
| id: route-team | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_KEY_FOR_ROUTING }} | |
| PR_TITLE: ${{ github.event.pull_request.title }} | |
| PR_BODY: ${{ github.event.pull_request.body }} | |
| PR_NUMBER: ${{ github.event.pull_request.number }} | |
| PR_AUTHOR: ${{ github.event.pull_request.user.login }} | |
| run: | | |
| if FILES_JSON=$(gh pr view "$PR_NUMBER" --json files 2>/dev/null); then | |
| FILES_LIST=$(echo "$FILES_JSON" | jq -r '.files | map(.path) | join("\n")' 2>/dev/null || echo "") | |
| else | |
| FILES_LIST="" | |
| fi | |
| TEAM_DESCRIPTIONS=$(echo "$LINEAR_TEAM_CONFIG" | jq -r '.[] | "- " + .key + ": " + .description') | |
| PROMPT=$(cat <<PROMPT_EOF | |
| You are routing a GitHub PR to the correct engineering team for review. Based on the PR details below, respond with ONLY the team key (e.g. ING, CAT, PFP), or NONE if it doesn't clearly belong to any team. | |
| Teams: | |
| ${TEAM_DESCRIPTIONS} | |
| PR Title: ${PR_TITLE} | |
| PR Author: ${PR_AUTHOR} | |
| Files Changed: | |
| ${FILES_LIST} | |
| PR Description: | |
| ${PR_BODY} | |
| Respond with only the team key or NONE. | |
| PROMPT_EOF | |
| ) | |
| PROMPT_JSON=$(echo "$PROMPT" | jq -Rs .) | |
| CLAUDE_RESPONSE=$(curl -s https://api.anthropic.com/v1/messages \ | |
| -H "x-api-key: $ANTHROPIC_API_KEY" \ | |
| -H "anthropic-version: 2023-06-01" \ | |
| -H "content-type: application/json" \ | |
| -d "{\"model\": \"claude-haiku-4-5-20251001\", \"max_tokens\": 10, \"messages\": [{\"role\": \"user\", \"content\": $PROMPT_JSON}]}") | |
| TEAM_KEY=$(echo "$CLAUDE_RESPONSE" | jq -r '.content[0].text' 2>/dev/null | tr -d '[:space:]' | tr '[:lower:]' '[:upper:]') | |
| if [ -z "$TEAM_KEY" ] || [ "$TEAM_KEY" = "NONE" ] || [ "$TEAM_KEY" = "NULL" ]; then | |
| echo "⏭️ No matching team found for PR #$PR_NUMBER" | |
| echo "team_key=" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| TEAM_CONFIG=$(echo "$LINEAR_TEAM_CONFIG" | jq --arg key "$TEAM_KEY" '.[] | select(.key == $key)') | |
| if [ -z "$TEAM_CONFIG" ]; then | |
| echo "❌ Unrecognized team key returned: $TEAM_KEY" | |
| echo "team_key=" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| echo "✅ Routed PR #$PR_NUMBER to team: $TEAM_KEY" | |
| { | |
| echo "team_key=$TEAM_KEY" | |
| echo "team_id=$(echo "$TEAM_CONFIG" | jq -r '.team_id')" | |
| echo "assignee_id=$(echo "$TEAM_CONFIG" | jq -r '.assignee_id')" | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Create Linear Ticket for PR | |
| if: github.event.action != 'closed' && steps.check-pr.outputs.should_skip == 'false' && steps.route-team.outputs.team_key != '' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| GH_REPO: ${{ github.repository }} | |
| LINEAR_API_KEY: ${{ secrets.INGESTION_LINEAR_KEY }} | |
| PR_TITLE: ${{ github.event.pull_request.title }} | |
| PR_URL: ${{ github.event.pull_request.html_url }} | |
| PR_AUTHOR: ${{ github.event.pull_request.user.login }} | |
| PR_NUMBER: ${{ github.event.pull_request.number }} | |
| PR_BODY: ${{ github.event.pull_request.body }} | |
| FILES_CHANGED: ${{ github.event.pull_request.changed_files }} | |
| ADDITIONS: ${{ github.event.pull_request.additions }} | |
| DELETIONS: ${{ github.event.pull_request.deletions }} | |
| CONTRIBUTOR_TYPE: ${{ steps.check-pr.outputs.contributor_type }} | |
| TEAM_KEY: ${{ steps.route-team.outputs.team_key }} | |
| TEAM_ID: ${{ steps.route-team.outputs.team_id }} | |
| ASSIGNEE_ID: ${{ steps.route-team.outputs.assignee_id }} | |
| run: | | |
| if [ -z "$PR_BODY" ]; then | |
| PR_BODY="No description provided" | |
| fi | |
| if FILES_JSON=$(gh pr view "$PR_NUMBER" --json files 2>/dev/null); then | |
| FILES_LIST=$(echo "$FILES_JSON" | jq -r '.files[:20] | map("- `" + .path + "`") | join("\n")' 2>/dev/null || echo "- Unable to fetch file list") | |
| if [ "$FILES_CHANGED" -gt 20 ]; then | |
| FILES_LIST="${FILES_LIST}\n- ... and $((FILES_CHANGED - 20)) more files" | |
| fi | |
| else | |
| FILES_LIST="- Unable to fetch file list" | |
| fi | |
| if REVIEWERS_JSON=$(gh pr view "$PR_NUMBER" --json reviewRequests 2>/dev/null); then | |
| REVIEWERS=$(echo "$REVIEWERS_JSON" | jq -r '.reviewRequests | map("@" + .login) | join(", ")' 2>/dev/null || echo "None") | |
| if [ -z "$REVIEWERS" ] || [ "$REVIEWERS" = "" ]; then | |
| REVIEWERS="None" | |
| fi | |
| else | |
| REVIEWERS="Unable to fetch" | |
| fi | |
| DESCRIPTION=$(cat <<DESCRIPTION_EOF | |
| Review PR #${PR_NUMBER} from @${PR_AUTHOR} (${CONTRIBUTOR_TYPE}) | |
| ${PR_URL} | |
| ## Description | |
| ${PR_BODY} | |
| ## Changes | |
| - **Files changed**: ${FILES_CHANGED} | |
| - **Lines**: +${ADDITIONS} / -${DELETIONS} | |
| - **Requested reviewers**: ${REVIEWERS} | |
| ## Files Changed | |
| ${FILES_LIST} | |
| DESCRIPTION_EOF | |
| ) | |
| TITLE="[PR Review] $PR_TITLE" | |
| if ! TITLE_JSON=$(echo "$TITLE" | jq -Rs . 2>/dev/null) || [ -z "$TITLE_JSON" ]; then | |
| echo "❌ Failed to JSON-encode title" | |
| exit 0 | |
| fi | |
| if ! DESCRIPTION_JSON=$(echo "$DESCRIPTION" | jq -Rs . 2>/dev/null) || [ -z "$DESCRIPTION_JSON" ]; then | |
| echo "❌ Failed to JSON-encode description" | |
| exit 0 | |
| fi | |
| MUTATION=$(cat <<EOF | |
| { | |
| "query": "mutation CreateIssue(\$input: IssueCreateInput!) { issueCreate(input: \$input) { success issue { id identifier title url } } }", | |
| "variables": { | |
| "input": { | |
| "teamId": "$TEAM_ID", | |
| "title": $TITLE_JSON, | |
| "description": $DESCRIPTION_JSON, | |
| "assigneeId": "$ASSIGNEE_ID" | |
| } | |
| } | |
| } | |
| EOF | |
| ) | |
| RESPONSE=$(curl -s -X POST https://api.linear.app/graphql \ | |
| -H "Authorization: $LINEAR_API_KEY" \ | |
| -H "Content-Type: application/json" \ | |
| -d "$MUTATION") | |
| if [ -z "$RESPONSE" ]; then | |
| echo "❌ Empty response from Linear API" | |
| exit 0 | |
| fi | |
| SUCCESS=$(echo "$RESPONSE" | jq -r '.data.issueCreate.success' 2>/dev/null) | |
| if [ "$SUCCESS" = "true" ]; then | |
| ISSUE_ID=$(echo "$RESPONSE" | jq -r '.data.issueCreate.issue.identifier') | |
| ISSUE_URL=$(echo "$RESPONSE" | jq -r '.data.issueCreate.issue.url') | |
| echo "✅ Created Linear ticket: $ISSUE_ID ($TEAM_KEY)" | |
| echo "📎 $ISSUE_URL" | |
| if [ "$CONTRIBUTOR_TYPE" = "External" ]; then | |
| PR_COMMENT=$(printf 'Linear: %s\n\nThanks for your contribution! We have created an internal ticket to track this PR. A member of the core DataHub team will be assigned to review it within the next few business days - you will get a follow-up comment once a reviewer is assigned.' "$ISSUE_ID") | |
| else | |
| PR_COMMENT=$(printf 'Linear: %s' "$ISSUE_ID") | |
| fi | |
| if ! gh pr comment "$PR_NUMBER" --body "$PR_COMMENT" 2>/dev/null; then | |
| echo "⚠️ Created ticket but failed to comment on PR" | |
| fi | |
| else | |
| echo "❌ Failed to create Linear ticket" | |
| echo "$RESPONSE" | jq . 2>/dev/null || echo "$RESPONSE" | |
| exit 0 | |
| fi | |
| handle-issue: | |
| if: github.event_name == 'issues' | |
| runs-on: ubuntu-latest | |
| continue-on-error: true | |
| timeout-minutes: 5 | |
| steps: | |
| - name: Mark Linear ticket as Done if issue closed | |
| if: github.event.action == 'closed' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| GH_REPO: ${{ github.repository }} | |
| LINEAR_API_KEY: ${{ secrets.INGESTION_LINEAR_KEY }} | |
| ISSUE_NUMBER: ${{ github.event.issue.number }} | |
| run: | | |
| if ! COMMENTS_JSON=$(gh issue view "$ISSUE_NUMBER" --json comments 2>/dev/null); then | |
| echo "⚠️ Failed to fetch comments for issue #$ISSUE_NUMBER" | |
| exit 0 | |
| fi | |
| COMMENTS=$(echo "$COMMENTS_JSON" | jq -r '.comments[].body' 2>/dev/null || echo "") | |
| TICKET_ID=$(echo "$COMMENTS" | grep -oP 'Linear: \K([A-Z]+-\d+)' 2>/dev/null | head -n1 || echo "") | |
| if [ -z "$TICKET_ID" ]; then | |
| echo "⚠️ No Linear ticket found for issue #$ISSUE_NUMBER" | |
| exit 0 | |
| fi | |
| TEAM_KEY=$(echo "$TICKET_ID" | grep -oP '^[A-Z]+') | |
| echo "Found Linear ticket: $TICKET_ID (team: $TEAM_KEY)" | |
| TICKET_RESPONSE=$(curl -s -X POST https://api.linear.app/graphql \ | |
| -H "Authorization: $LINEAR_API_KEY" \ | |
| -H "Content-Type: application/json" \ | |
| -d "{\"query\": \"query { issue(id: \\\"$TICKET_ID\\\") { id state { name } } }\"}") | |
| TICKET_UUID=$(echo "$TICKET_RESPONSE" | jq -r '.data.issue.id' 2>/dev/null || echo "") | |
| if [ -z "$TICKET_UUID" ] || [ "$TICKET_UUID" = "null" ]; then | |
| echo "❌ Could not find Linear ticket $TICKET_ID" | |
| exit 0 | |
| fi | |
| STATE_RESPONSE=$(curl -s -X POST https://api.linear.app/graphql \ | |
| -H "Authorization: $LINEAR_API_KEY" \ | |
| -H "Content-Type: application/json" \ | |
| -d "{\"query\": \"query { workflowStates(filter: { team: { key: { eq: \\\"$TEAM_KEY\\\" } }, name: { eq: \\\"Done\\\" } }) { nodes { id name } } }\"}") | |
| DONE_STATE_ID=$(echo "$STATE_RESPONSE" | jq -r '.data.workflowStates.nodes[0].id' 2>/dev/null || echo "") | |
| if [ -z "$DONE_STATE_ID" ] || [ "$DONE_STATE_ID" = "null" ]; then | |
| echo "❌ Could not find 'Done' state for team $TEAM_KEY" | |
| exit 0 | |
| fi | |
| UPDATE_RESPONSE=$(curl -s -X POST https://api.linear.app/graphql \ | |
| -H "Authorization: $LINEAR_API_KEY" \ | |
| -H "Content-Type: application/json" \ | |
| -d "{\"query\": \"mutation { issueUpdate(id: \\\"$TICKET_UUID\\\", input: { stateId: \\\"$DONE_STATE_ID\\\" }) { success issue { identifier state { name } } } }\"}") | |
| SUCCESS=$(echo "$UPDATE_RESPONSE" | jq -r '.data.issueUpdate.success' 2>/dev/null || echo "false") | |
| if [ "$SUCCESS" = "true" ]; then | |
| echo "✅ Marked $TICKET_ID as Done" | |
| else | |
| echo "❌ Failed to mark ticket as Done" | |
| echo "$UPDATE_RESPONSE" | jq . 2>/dev/null || echo "$UPDATE_RESPONSE" | |
| fi | |
| - name: Route issue to team | |
| if: github.event.action != 'closed' | |
| id: route-team | |
| env: | |
| ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_KEY_FOR_ROUTING }} | |
| ISSUE_TITLE: ${{ github.event.issue.title }} | |
| ISSUE_BODY: ${{ github.event.issue.body }} | |
| ISSUE_NUMBER: ${{ github.event.issue.number }} | |
| ISSUE_AUTHOR: ${{ github.event.issue.user.login }} | |
| run: | | |
| TEAM_DESCRIPTIONS=$(echo "$LINEAR_TEAM_CONFIG" | jq -r '.[] | "- " + .key + ": " + .description') | |
| PROMPT=$(cat <<PROMPT_EOF | |
| You are routing a GitHub issue to the correct engineering team. Based on the issue details below, respond with ONLY the team key (e.g. ING, CAT, PFP), or NONE if it doesn't clearly belong to any team. | |
| Teams: | |
| ${TEAM_DESCRIPTIONS} | |
| Issue Title: ${ISSUE_TITLE} | |
| Issue Author: ${ISSUE_AUTHOR} | |
| Issue Description: | |
| ${ISSUE_BODY} | |
| Respond with only the team key or NONE. | |
| PROMPT_EOF | |
| ) | |
| PROMPT_JSON=$(echo "$PROMPT" | jq -Rs .) | |
| CLAUDE_RESPONSE=$(curl -s https://api.anthropic.com/v1/messages \ | |
| -H "x-api-key: $ANTHROPIC_API_KEY" \ | |
| -H "anthropic-version: 2023-06-01" \ | |
| -H "content-type: application/json" \ | |
| -d "{\"model\": \"claude-haiku-4-5-20251001\", \"max_tokens\": 10, \"messages\": [{\"role\": \"user\", \"content\": $PROMPT_JSON}]}") | |
| TEAM_KEY=$(echo "$CLAUDE_RESPONSE" | jq -r '.content[0].text' 2>/dev/null | tr -d '[:space:]' | tr '[:lower:]' '[:upper:]') | |
| if [ -z "$TEAM_KEY" ] || [ "$TEAM_KEY" = "NONE" ] || [ "$TEAM_KEY" = "NULL" ]; then | |
| echo "⏭️ No matching team found for issue #$ISSUE_NUMBER" | |
| echo "team_key=" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| TEAM_CONFIG=$(echo "$LINEAR_TEAM_CONFIG" | jq --arg key "$TEAM_KEY" '.[] | select(.key == $key)') | |
| if [ -z "$TEAM_CONFIG" ]; then | |
| echo "❌ Unrecognized team key returned: $TEAM_KEY" | |
| echo "team_key=" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| echo "✅ Routed issue #$ISSUE_NUMBER to team: $TEAM_KEY" | |
| { | |
| echo "team_key=$TEAM_KEY" | |
| echo "team_id=$(echo "$TEAM_CONFIG" | jq -r '.team_id')" | |
| echo "assignee_id=$(echo "$TEAM_CONFIG" | jq -r '.assignee_id')" | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Create Linear Ticket for Issue | |
| if: github.event.action != 'closed' && steps.route-team.outputs.team_key != '' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| GH_REPO: ${{ github.repository }} | |
| LINEAR_API_KEY: ${{ secrets.INGESTION_LINEAR_KEY }} | |
| ISSUE_TITLE: ${{ github.event.issue.title }} | |
| ISSUE_URL: ${{ github.event.issue.html_url }} | |
| ISSUE_AUTHOR: ${{ github.event.issue.user.login }} | |
| ISSUE_NUMBER: ${{ github.event.issue.number }} | |
| ISSUE_BODY: ${{ github.event.issue.body }} | |
| TEAM_KEY: ${{ steps.route-team.outputs.team_key }} | |
| TEAM_ID: ${{ steps.route-team.outputs.team_id }} | |
| ASSIGNEE_ID: ${{ steps.route-team.outputs.assignee_id }} | |
| run: | | |
| # Deduplication guard | |
| if COMMENTS_JSON=$(gh issue view "$ISSUE_NUMBER" --json comments 2>/dev/null); then | |
| EXISTING=$(echo "$COMMENTS_JSON" | jq -r '.comments[].body' 2>/dev/null | grep -oP 'Linear: [A-Z]+-\d+' | head -n1 || echo "") | |
| if [ -n "$EXISTING" ]; then | |
| echo "⏭️ Linear ticket already exists ($EXISTING), skipping" | |
| exit 0 | |
| fi | |
| fi | |
| if [ -z "$ISSUE_BODY" ]; then | |
| ISSUE_BODY="No description provided" | |
| fi | |
| DESCRIPTION=$(cat <<DESCRIPTION_EOF | |
| Issue #${ISSUE_NUMBER} from @${ISSUE_AUTHOR} | |
| ${ISSUE_URL} | |
| ## Description | |
| ${ISSUE_BODY} | |
| DESCRIPTION_EOF | |
| ) | |
| TITLE="[Issue] $ISSUE_TITLE" | |
| if ! TITLE_JSON=$(echo "$TITLE" | jq -Rs . 2>/dev/null) || [ -z "$TITLE_JSON" ]; then | |
| echo "❌ Failed to JSON-encode title" | |
| exit 0 | |
| fi | |
| if ! DESCRIPTION_JSON=$(echo "$DESCRIPTION" | jq -Rs . 2>/dev/null) || [ -z "$DESCRIPTION_JSON" ]; then | |
| echo "❌ Failed to JSON-encode description" | |
| exit 0 | |
| fi | |
| MUTATION=$(cat <<EOF | |
| { | |
| "query": "mutation CreateIssue(\$input: IssueCreateInput!) { issueCreate(input: \$input) { success issue { id identifier title url } } }", | |
| "variables": { | |
| "input": { | |
| "teamId": "$TEAM_ID", | |
| "title": $TITLE_JSON, | |
| "description": $DESCRIPTION_JSON, | |
| "assigneeId": "$ASSIGNEE_ID" | |
| } | |
| } | |
| } | |
| EOF | |
| ) | |
| RESPONSE=$(curl -s -X POST https://api.linear.app/graphql \ | |
| -H "Authorization: $LINEAR_API_KEY" \ | |
| -H "Content-Type: application/json" \ | |
| -d "$MUTATION") | |
| if [ -z "$RESPONSE" ]; then | |
| echo "❌ Empty response from Linear API" | |
| exit 0 | |
| fi | |
| SUCCESS=$(echo "$RESPONSE" | jq -r '.data.issueCreate.success' 2>/dev/null) | |
| if [ "$SUCCESS" = "true" ]; then | |
| ISSUE_ID=$(echo "$RESPONSE" | jq -r '.data.issueCreate.issue.identifier') | |
| ISSUE_URL=$(echo "$RESPONSE" | jq -r '.data.issueCreate.issue.url') | |
| echo "✅ Created Linear ticket: $ISSUE_ID ($TEAM_KEY)" | |
| echo "📎 $ISSUE_URL" | |
| if ! gh issue comment "$ISSUE_NUMBER" --body "Linear: $ISSUE_ID" 2>/dev/null; then | |
| echo "⚠️ Created ticket but failed to comment on issue" | |
| fi | |
| else | |
| echo "❌ Failed to create Linear ticket" | |
| echo "$RESPONSE" | jq . 2>/dev/null || echo "$RESPONSE" | |
| exit 0 | |
| fi |