5353 required : false
5454 type : boolean
5555 default : true
56+ linear_state_name :
57+ description : ' Linear workflow state for newly created or reopened issues.'
58+ required : false
59+ type : string
60+ default : ' Triage'
61+ linear_cycle_id :
62+ description : ' Linear cycle UUID to assign. Takes precedence over linear_use_active_cycle.'
63+ required : false
64+ type : string
65+ default : ' '
66+ linear_use_active_cycle :
67+ description : ' Assign issues to the Linear team active cycle when linear_cycle_id is empty.'
68+ required : false
69+ type : boolean
70+ default : false
5671 secrets :
5772 LINEAR_API_KEY :
5873 required : true
@@ -550,13 +565,17 @@ jobs:
550565 LINEAR_API_KEY : ${{ secrets.LINEAR_API_KEY }}
551566 LINEAR_TEAM_ID : ${{ secrets.LINEAR_TEAM_ID }}
552567 REPOSITORY : ${{ github.repository }}
568+ LINEAR_STATE_NAME : ${{ inputs.linear_state_name || 'Triage' }}
553569 run : |
554570 set -euo pipefail
555- # Resolve team UUID + the "CVE" label, "Triage" state, and API key owner once,
556- # and expose them as outputs so the issue-creation step can reuse them.
557- METADATA_QUERY='query($teamId: String!) { team(id: $teamId) { id labels(filter: { name: { eq: "CVE" } }) { nodes { id } } states(filter: { name: { eq: "Triage" } }) { nodes { id } } } viewer { id } }'
558- METADATA_PAYLOAD=$(jq -n --arg query "$METADATA_QUERY" --arg teamId "$LINEAR_TEAM_ID" \
559- '{query: $query, variables: {teamId: $teamId}}')
571+ # Resolve team UUID, labels, configured state, active cycle, and API key
572+ # owner once, then expose them for issue creation and reopening.
573+ METADATA_QUERY='query($teamId: String!, $stateName: String!) { team(id: $teamId) { id labels(filter: { name: { eq: "CVE" } }) { nodes { id } } states(filter: { name: { eq: $stateName } }) { nodes { id } } activeCycle { id } } viewer { id } }'
574+ METADATA_PAYLOAD=$(jq -n \
575+ --arg query "$METADATA_QUERY" \
576+ --arg teamId "$LINEAR_TEAM_ID" \
577+ --arg stateName "$LINEAR_STATE_NAME" \
578+ '{query: $query, variables: {teamId: $teamId, stateName: $stateName}}')
560579 METADATA_RESPONSE=$(curl -s -X POST https://api.linear.app/graphql \
561580 -H "Content-Type: application/json" \
562581 -H "Authorization: $LINEAR_API_KEY" \
@@ -565,6 +584,7 @@ jobs:
565584 TEAM_UUID=$(echo "$METADATA_RESPONSE" | jq -r '.data.team.id // empty')
566585 LABEL_ID=$(echo "$METADATA_RESPONSE" | jq -r '.data.team.labels.nodes[0].id // empty')
567586 STATE_ID=$(echo "$METADATA_RESPONSE" | jq -r '.data.team.states.nodes[0].id // empty')
587+ ACTIVE_CYCLE_ID=$(echo "$METADATA_RESPONSE" | jq -r '.data.team.activeCycle.id // empty')
568588 VIEWER_ID=$(echo "$METADATA_RESPONSE" | jq -r '.data.viewer.id // empty')
569589
570590 if [ -z "$TEAM_UUID" ]; then
@@ -603,6 +623,7 @@ jobs:
603623 echo "label_id=$LABEL_ID"
604624 echo "repo_label_id=$REPO_LABEL_ID"
605625 echo "state_id=$STATE_ID"
626+ echo "active_cycle_id=$ACTIVE_CYCLE_ID"
606627 echo "viewer_id=$VIEWER_ID"
607628 } >> "$GITHUB_OUTPUT"
608629
@@ -682,19 +703,27 @@ jobs:
682703 LABEL_ID : ${{ steps.match.outputs.label_id }}
683704 REPO_LABEL_ID : ${{ steps.match.outputs.repo_label_id }}
684705 STATE_ID : ${{ steps.match.outputs.state_id }}
706+ ACTIVE_CYCLE_ID : ${{ steps.match.outputs.active_cycle_id }}
685707 VIEWER_ID : ${{ steps.match.outputs.viewer_id }}
686708 CONFIGURED_ASSIGNEE_ID : ${{ inputs.linear_assignee_id }}
687709 ASSIGN_TO_API_KEY_OWNER : ${{ inputs.assign_to_api_key_owner }}
710+ LINEAR_STATE_NAME : ${{ inputs.linear_state_name || 'Triage' }}
711+ CONFIGURED_CYCLE_ID : ${{ inputs.linear_cycle_id }}
712+ USE_ACTIVE_CYCLE : ${{ inputs.linear_use_active_cycle }}
688713 run : |
689714 set -uo pipefail
690- # Team UUID, the "CVE" + repository labels, "Triage" state, and assignee
691- # metadata were already resolved by the "Match existing Linear issues" step.
715+ # Team UUID, labels, state, cycle, and assignee metadata were already
716+ # resolved by the "Match existing Linear issues" step.
692717 STRUCTURED_OUTPUT=$(cat findings.json)
693718
694719 TARGET_ASSIGNEE_ID="$CONFIGURED_ASSIGNEE_ID"
695720 if [ -z "$TARGET_ASSIGNEE_ID" ] && [ "$ASSIGN_TO_API_KEY_OWNER" = "true" ]; then
696721 TARGET_ASSIGNEE_ID="$VIEWER_ID"
697722 fi
723+ TARGET_CYCLE_ID="$CONFIGURED_CYCLE_ID"
724+ if [ -z "$TARGET_CYCLE_ID" ] && [ "$USE_ACTIVE_CYCLE" = "true" ]; then
725+ TARGET_CYCLE_ID="$ACTIVE_CYCLE_ID"
726+ fi
698727
699728 if [ -z "$LABEL_ID" ]; then
700729 echo "::warning::Could not find 'CVE' label in Linear team. Creating issues without label."
@@ -703,11 +732,14 @@ jobs:
703732 echo "::warning::Could not resolve '$REPOSITORY' repository label. Creating issues without it."
704733 fi
705734 if [ -z "$STATE_ID" ]; then
706- echo "::warning::Could not find 'Triage ' state in Linear team. Using default state."
735+ echo "::warning::Could not find '$LINEAR_STATE_NAME ' state in Linear team. Using default state."
707736 fi
708737 if [ -z "$TARGET_ASSIGNEE_ID" ]; then
709738 echo "Issues will be created or reopened unassigned."
710739 fi
740+ if [ "$USE_ACTIVE_CYCLE" = "true" ] && [ -z "$TARGET_CYCLE_ID" ]; then
741+ echo "::warning::Could not resolve an active Linear cycle. Issues will be created without a cycle."
742+ fi
711743
712744 # Map severity to Linear priority
713745 severity_to_priority() {
@@ -731,7 +763,7 @@ jobs:
731763 # Write CVEs to temp file so the while loop doesn't run in a pipe subshell
732764 echo "$STRUCTURED_OUTPUT" | jq -c '.cves[]' > /tmp/cves.jsonl
733765
734- MUTATION='mutation CreateIssue($teamId: String!, $title: String!, $description: String, $priority: Int, $labelIds: [String!], $stateId: String, $assigneeId: String) { issueCreate(input: { teamId: $teamId, title: $title, description: $description, priority: $priority, labelIds: $labelIds, stateId: $stateId, assigneeId: $assigneeId }) { success issue { id identifier url } } }'
766+ MUTATION='mutation CreateIssue($teamId: String!, $title: String!, $description: String, $priority: Int, $labelIds: [String!], $stateId: String, $assigneeId: String, $cycleId: String ) { issueCreate(input: { teamId: $teamId, title: $title, description: $description, priority: $priority, labelIds: $labelIds, stateId: $stateId, assigneeId: $assigneeId, cycleId: $cycleId }) { success issue { id identifier url } } }'
735767
736768 while IFS= read -r cve; do
737769 CVE_ID=$(echo "$cve" | jq -r '.cveId')
@@ -752,16 +784,16 @@ jobs:
752784 fi
753785
754786 if [ "$LINEAR_EXISTS" = "true" ] && [ "$LINEAR_CLOSED" = "true" ]; then
755- # Reopen the closed issue by setting its state back to Triage
787+ # Reopen the closed issue using the configured repository routing.
756788 echo "Found closed Linear issue $LINEAR_IDENTIFIER for $CVE_ID ($LINEAR_URL) — will attempt to reopen"
757789 if [ -z "$STATE_ID" ]; then
758- echo "::warning::Cannot reopen $CVE_ID ($LINEAR_IDENTIFIER) — no Triage state found. Skipping."
759- echo "- Skipped **$CVE_ID** — found closed issue [$LINEAR_IDENTIFIER]($LINEAR_URL) but no Triage state to reopen" >> "$GITHUB_STEP_SUMMARY"
790+ echo "::warning::Cannot reopen $CVE_ID ($LINEAR_IDENTIFIER) — no '$LINEAR_STATE_NAME' state found. Skipping."
791+ echo "- Skipped **$CVE_ID** — found closed issue [$LINEAR_IDENTIFIER]($LINEAR_URL) but no '$LINEAR_STATE_NAME' state to reopen" >> "$GITHUB_STEP_SUMMARY"
760792 SKIPPED_COUNT=$((SKIPPED_COUNT + 1))
761793 continue
762794 fi
763795
764- REOPEN_MUTATION='mutation($issueId: String!, $stateId: String!, $assigneeId: String) { issueUpdate(id: $issueId, input: { stateId: $stateId, assigneeId: $assigneeId }) { success issue { id identifier url } } }'
796+ REOPEN_MUTATION='mutation($issueId: String!, $stateId: String!, $assigneeId: String, $cycleId: String ) { issueUpdate(id: $issueId, input: { stateId: $stateId, assigneeId: $assigneeId, cycleId: $cycleId }) { success issue { id identifier url } } }'
765797 REOPEN_VARIABLES=$(jq -n \
766798 --arg issueId "$LINEAR_ISSUE_ID" \
767799 --arg stateId "$STATE_ID" \
@@ -771,6 +803,9 @@ jobs:
771803 else
772804 REOPEN_VARIABLES=$(echo "$REOPEN_VARIABLES" | jq '. + {assigneeId: null}')
773805 fi
806+ if [ -n "$TARGET_CYCLE_ID" ]; then
807+ REOPEN_VARIABLES=$(echo "$REOPEN_VARIABLES" | jq --arg cid "$TARGET_CYCLE_ID" '. + {cycleId: $cid}')
808+ fi
774809 REOPEN_PAYLOAD=$(jq -n --arg query "$REOPEN_MUTATION" --argjson vars "$REOPEN_VARIABLES" '{query: $query, variables: $vars}')
775810
776811 REOPEN_RESPONSE=$(curl -s -X POST https://api.linear.app/graphql \
@@ -782,7 +817,7 @@ jobs:
782817 REOPEN_IDENTIFIER=$(echo "$REOPEN_RESPONSE" | jq -r '.data.issueUpdate.issue.identifier // empty')
783818 if [ -n "$REOPEN_URL" ]; then
784819 echo "Reopened Linear issue $REOPEN_IDENTIFIER for $CVE_ID: $REOPEN_URL"
785- echo "- Reopened [$REOPEN_IDENTIFIER]($REOPEN_URL) for **$CVE_ID** — $TITLE (moved back to Triage )" >> "$GITHUB_STEP_SUMMARY"
820+ echo "- Reopened [$REOPEN_IDENTIFIER]($REOPEN_URL) for **$CVE_ID** — $TITLE (moved to $LINEAR_STATE_NAME )" >> "$GITHUB_STEP_SUMMARY"
786821 REOPENED_COUNT=$((REOPENED_COUNT + 1))
787822 else
788823 echo "::error::Failed to reopen Linear issue $LINEAR_IDENTIFIER for $CVE_ID"
@@ -818,6 +853,9 @@ jobs:
818853 else
819854 VARIABLES=$(echo "$VARIABLES" | jq '. + {assigneeId: null}')
820855 fi
856+ if [ -n "$TARGET_CYCLE_ID" ]; then
857+ VARIABLES=$(echo "$VARIABLES" | jq --arg cid "$TARGET_CYCLE_ID" '. + {cycleId: $cid}')
858+ fi
821859
822860 PAYLOAD=$(jq -n --arg query "$MUTATION" --argjson vars "$VARIABLES" '{query: $query, variables: $vars}')
823861
0 commit comments