Allow integration of Listeners execution into the Workflow execution #38
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
| # | |
| # Copyright 2021-Present The Serverless Workflow Specification Authors | |
| # | |
| # Licensed under the Apache License, Version 2.0 (the "License"); | |
| # you may not use this file except in compliance with the License. | |
| # You may obtain a copy of the License at | |
| # | |
| # http://www.apache.org/licenses/LICENSE-2.0 | |
| # | |
| # Unless required by applicable law or agreed to in writing, software | |
| # distributed under the License is distributed on an "AS IS" BASIS, | |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| # See the License for the specific language governing permissions and | |
| # limitations under the License. | |
| # | |
| name: Sync Issues to Target GitHub Project | |
| on: | |
| issues: | |
| types: [opened, closed] | |
| workflow_dispatch: | |
| jobs: | |
| sync: | |
| runs-on: ubuntu-latest | |
| if: github.event_name == 'workflow_dispatch' || (vars.PSYNC_ENABLED != 'false' && vars.PSYNC_ENABLED != 'off') | |
| steps: | |
| - name: Parse target project | |
| id: parse | |
| run: | | |
| VALUE="${{ vars.PSYNC_TARGET }}" | |
| if ! [[ "$VALUE" =~ ^[^:]+:[0-9]+$ ]]; then | |
| echo "Error: PSYNC_TARGET value '$VALUE' is invalid. Expected format: org:project_number (e.g. my-org:1)" | |
| exit 1 | |
| fi | |
| ORG="${VALUE%%:*}" | |
| NUMBER="${VALUE##*:}" | |
| echo "org=$ORG" >> "$GITHUB_OUTPUT" | |
| echo "number=$NUMBER" >> "$GITHUB_OUTPUT" | |
| - name: Check author filter | |
| id: author_filter | |
| if: github.event_name == 'issues' | |
| run: | | |
| FILTER="${{ vars.PSYNC_AUTHORS_FILTER }}" | |
| if [ -z "$FILTER" ]; then | |
| echo "allowed=true" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| AUTHOR="${{ github.event.issue.user.login }}" | |
| if echo "$FILTER" | tr ',' '\n' | xargs -I{} echo {} | xargs | tr ' ' '\n' | grep -qx "$AUTHOR"; then | |
| echo "allowed=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "Issue author '$AUTHOR' is not in PSYNC_AUTHORS_FILTER — skipping." | |
| echo "allowed=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Get project ID | |
| id: project | |
| if: github.event_name == 'workflow_dispatch' || (vars.PSYNC_ENABLED != 'false' && vars.PSYNC_ENABLED != 'off' && steps.author_filter.outputs.allowed != 'false') | |
| env: | |
| GH_TOKEN: ${{ secrets.PSYNC_PAT }} | |
| run: | | |
| PROJECT_ID=$(gh api graphql -f query=' | |
| query($org: String!, $number: Int!) { | |
| organization(login: $org) { | |
| projectV2(number: $number) { | |
| id | |
| } | |
| } | |
| }' \ | |
| -f org="${{ steps.parse.outputs.org }}" \ | |
| -F number=${{ steps.parse.outputs.number }} \ | |
| --jq '.data.organization.projectV2.id') | |
| if [ -z "$PROJECT_ID" ] || [ "$PROJECT_ID" = "null" ]; then | |
| echo "Error: could not resolve project ID for '${{ steps.parse.outputs.org }}' project number ${{ steps.parse.outputs.number }}. Check PSYNC_TARGET and that PSYNC_PAT has access to the target org's project." | |
| exit 1 | |
| fi | |
| echo "id=$PROJECT_ID" >> "$GITHUB_OUTPUT" | |
| - name: Write apply-fields helper | |
| if: github.event_name == 'workflow_dispatch' || (github.event.action == 'opened' && vars.PSYNC_ENABLED != 'false' && vars.PSYNC_ENABLED != 'off' && steps.author_filter.outputs.allowed != 'false') | |
| run: | | |
| cat > "$RUNNER_TEMP/psync_apply_fields.sh" << 'PSYNC_HELPER_EOF' | |
| # apply_fields ITEM_ID ISSUE_NUMBER | |
| # Required env: PSYNC_PROJECT_ID, PSYNC_FIELDS_JSON (nodes array), PSYNC_INITIAL_VALUES, PSYNC_REPO | |
| apply_fields() { | |
| local ITEM_ID="$1" | |
| local ISSUE_NUMBER="$2" | |
| while IFS= read -r PAIR; do | |
| PAIR=$(echo "$PAIR" | xargs) | |
| [ -z "$PAIR" ] && continue | |
| KEY=$(echo "$PAIR" | cut -d= -f1 | xargs) | |
| VALUE=$(echo "$PAIR" | cut -d= -f2- | xargs) | |
| if [ "$KEY" = "Assignees" ]; then | |
| ASSIGNEES_JSON=$(echo "$VALUE" | tr ' ' '\n' | jq -R . | jq -s '[.[] | select(length > 0)]') | |
| [ "$(echo "$ASSIGNEES_JSON" | jq 'length')" -eq 0 ] && continue | |
| echo "{\"assignees\": $ASSIGNEES_JSON}" | \ | |
| gh api "repos/$PSYNC_REPO/issues/$ISSUE_NUMBER/assignees" \ | |
| --method POST --input - | |
| else | |
| FIELD_ID=$(echo "$PSYNC_FIELDS_JSON" | jq -r \ | |
| --arg name "$KEY" \ | |
| '.[] | select(.name == $name) | .id // empty' | head -1) | |
| [ -z "$FIELD_ID" ] && echo "Warning: field '$KEY' not found in project fields. Available fields: $(echo "$PSYNC_FIELDS_JSON" | jq -r '[.[] | .name // empty] | join(", ")'). Skipping." && continue | |
| DATA_TYPE=$(echo "$PSYNC_FIELDS_JSON" | jq -r \ | |
| --arg name "$KEY" \ | |
| '.[] | select(.name == $name) | .dataType // empty' | head -1) | |
| OPTION_ID=$(echo "$PSYNC_FIELDS_JSON" | jq -r \ | |
| --arg name "$KEY" --arg val "$VALUE" \ | |
| '.[] | select(.name == $name) | .options[]? | select(.name == $val) | .id // empty') | |
| echo "Debug: field='$KEY' value='$VALUE' field_id='$FIELD_ID' data_type='${DATA_TYPE:-single-select}' option_id='${OPTION_ID:-none}'" | |
| if [ -n "$OPTION_ID" ]; then | |
| gh api graphql -f query=' | |
| mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) { | |
| updateProjectV2ItemFieldValue(input: { | |
| projectId: $projectId | |
| itemId: $itemId | |
| fieldId: $fieldId | |
| value: { singleSelectOptionId: $optionId } | |
| }) { projectV2Item { id } } | |
| }' \ | |
| -f projectId="$PSYNC_PROJECT_ID" -f itemId="$ITEM_ID" \ | |
| -f fieldId="$FIELD_ID" -f optionId="$OPTION_ID" | |
| elif [ "$DATA_TYPE" = "TEXT" ]; then | |
| gh api graphql -f query=' | |
| mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $text: String!) { | |
| updateProjectV2ItemFieldValue(input: { | |
| projectId: $projectId | |
| itemId: $itemId | |
| fieldId: $fieldId | |
| value: { text: $text } | |
| }) { projectV2Item { id } } | |
| }' \ | |
| -f projectId="$PSYNC_PROJECT_ID" -f itemId="$ITEM_ID" \ | |
| -f fieldId="$FIELD_ID" -f text="$VALUE" | |
| else | |
| echo "Warning: field '$KEY' has unsupported type '${DATA_TYPE:-single-select}' — only TEXT and single-select fields are supported. Skipping." | |
| fi | |
| fi | |
| done < <(echo "$PSYNC_INITIAL_VALUES" | tr ',' '\n') | |
| } | |
| PSYNC_HELPER_EOF | |
| - name: Import existing repo issues | |
| if: github.event_name == 'workflow_dispatch' && (vars.PSYNC_IMPORT_EXISTING) == 'true' | |
| env: | |
| GH_TOKEN: ${{ secrets.PSYNC_PAT }} | |
| run: | | |
| PROJECT_ID="${{ steps.project.outputs.id }}" | |
| INITIAL_VALUES="${{ vars.PSYNC_INITIAL_VALUES }}" | |
| # Fetch project fields once if initial values are configured | |
| FIELDS_JSON="[]" | |
| if [ -n "$INITIAL_VALUES" ]; then | |
| FIELDS_JSON=$(gh api graphql -f query=' | |
| query($projectId: ID!) { | |
| node(id: $projectId) { | |
| ... on ProjectV2 { | |
| fields(first: 20) { | |
| nodes { | |
| ... on ProjectV2SingleSelectField { | |
| id | |
| name | |
| options { id name } | |
| } | |
| ... on ProjectV2Field { | |
| id | |
| name | |
| dataType | |
| } | |
| } | |
| } | |
| } | |
| } | |
| }' \ | |
| -f projectId="$PROJECT_ID" \ | |
| --jq '.data.node.fields.nodes') | |
| fi | |
| # Source shared apply-fields helper | |
| # shellcheck source=/dev/null | |
| source "$RUNNER_TEMP/psync_apply_fields.sh" | |
| export PSYNC_PROJECT_ID="$PROJECT_ID" | |
| export PSYNC_FIELDS_JSON="$FIELDS_JSON" | |
| export PSYNC_INITIAL_VALUES="$INITIAL_VALUES" | |
| export PSYNC_REPO="${{ github.repository }}" | |
| # Fetch all open repo issues (node_id + number + author) | |
| REPO_ISSUES=$(gh api "repos/${{ github.repository }}/issues" \ | |
| --paginate | jq -s '[ .[][] | select(.pull_request == null) | {node_id, number, author: .user.login} ]') | |
| # Filter by author if PSYNC_AUTHORS_FILTER is set | |
| AUTHORS_FILTER="${{ vars.PSYNC_AUTHORS_FILTER }}" | |
| if [ -n "$AUTHORS_FILTER" ]; then | |
| AUTHORS_JSON=$(echo "$AUTHORS_FILTER" | tr ',' '\n' | xargs -I{} echo {} | jq -R . | jq -s '.') | |
| REPO_ISSUES=$(echo "$REPO_ISSUES" | jq --argjson authors "$AUTHORS_JSON" \ | |
| '[ .[] | select(.author | IN($authors[])) ]') | |
| fi | |
| # Fetch all existing project content node IDs (paginated) | |
| PROJECT_IDS="" | |
| CURSOR="" | |
| while true; do | |
| if [ -z "$CURSOR" ]; then | |
| PAGE=$(gh api graphql -f query=' | |
| query($projectId: ID!) { | |
| node(id: $projectId) { | |
| ... on ProjectV2 { | |
| items(first: 100) { | |
| pageInfo { hasNextPage endCursor } | |
| nodes { content { ... on Issue { id } } } | |
| } | |
| } | |
| } | |
| }' \ | |
| -f projectId="$PROJECT_ID") | |
| else | |
| PAGE=$(gh api graphql -f query=' | |
| query($projectId: ID!, $cursor: String!) { | |
| node(id: $projectId) { | |
| ... on ProjectV2 { | |
| items(first: 100, after: $cursor) { | |
| pageInfo { hasNextPage endCursor } | |
| nodes { content { ... on Issue { id } } } | |
| } | |
| } | |
| } | |
| }' \ | |
| -f projectId="$PROJECT_ID" -f cursor="$CURSOR") | |
| fi | |
| PROJECT_IDS="$PROJECT_IDS"$'\n'"$(echo "$PAGE" | jq -r '.data.node.items.nodes[].content.id // empty')" | |
| HAS_NEXT=$(echo "$PAGE" | jq -r '.data.node.items.pageInfo.hasNextPage') | |
| [ "$HAS_NEXT" != "true" ] && break | |
| CURSOR=$(echo "$PAGE" | jq -r '.data.node.items.pageInfo.endCursor') | |
| done | |
| PROJECT_IDS_JSON=$(echo "$PROJECT_IDS" | grep -v '^$' | jq -R . | jq -s '.') || PROJECT_IDS_JSON='[]' | |
| # Find issues not yet in the project | |
| MISSING=$(echo "$REPO_ISSUES" | jq -c \ | |
| --argjson existing "$PROJECT_IDS_JSON" \ | |
| '.[] | select(.node_id | IN($existing[]) | not)') | |
| if [ -z "$MISSING" ]; then | |
| echo "No new issues to import." | |
| exit 0 | |
| fi | |
| IMPORTED=0 | |
| FAILED=0 | |
| while IFS= read -r ISSUE; do | |
| NODE_ID=$(echo "$ISSUE" | jq -r '.node_id') | |
| ISSUE_NUMBER=$(echo "$ISSUE" | jq -r '.number') | |
| ITEM_ID=$(gh api graphql -f query=' | |
| mutation($projectId: ID!, $contentId: ID!) { | |
| addProjectV2ItemById(input: {projectId: $projectId, contentId: $contentId}) { | |
| item { id } | |
| } | |
| }' \ | |
| -f projectId="$PROJECT_ID" \ | |
| -f contentId="$NODE_ID" \ | |
| --jq '.data.addProjectV2ItemById.item.id') || ITEM_ID="" | |
| if [ -z "$ITEM_ID" ] || [ "$ITEM_ID" = "null" ]; then | |
| echo "Warning: failed to add issue #$ISSUE_NUMBER" | |
| FAILED=$((FAILED + 1)) | |
| continue | |
| fi | |
| [ -n "$INITIAL_VALUES" ] && apply_fields "$ITEM_ID" "$ISSUE_NUMBER" | |
| IMPORTED=$((IMPORTED + 1)) | |
| echo "Imported issue #$ISSUE_NUMBER" | |
| done <<< "$MISSING" | |
| echo "Done. Imported: $IMPORTED, Failed: $FAILED" | |
| - name: Add issue to project | |
| id: add_item | |
| if: github.event.action == 'opened' && vars.PSYNC_ENABLED != 'false' && vars.PSYNC_ENABLED != 'off' && steps.author_filter.outputs.allowed != 'false' | |
| env: | |
| GH_TOKEN: ${{ secrets.PSYNC_PAT }} | |
| run: | | |
| ITEM_ID=$(gh api graphql -f query=' | |
| mutation($projectId: ID!, $contentId: ID!) { | |
| addProjectV2ItemById(input: {projectId: $projectId, contentId: $contentId}) { | |
| item { id } | |
| } | |
| }' \ | |
| -f projectId="${{ steps.project.outputs.id }}" \ | |
| -f contentId="${{ github.event.issue.node_id }}" \ | |
| --jq '.data.addProjectV2ItemById.item.id' || true) | |
| if [ -z "$ITEM_ID" ] || [ "$ITEM_ID" = "null" ]; then | |
| echo "Issue already exists in project or add failed — searching for existing item ID." | |
| CURSOR="" | |
| while true; do | |
| if [ -z "$CURSOR" ]; then | |
| PAGE_RESULT=$(gh api graphql -f query=' | |
| query($projectId: ID!) { | |
| node(id: $projectId) { | |
| ... on ProjectV2 { | |
| items(first: 100) { | |
| pageInfo { hasNextPage endCursor } | |
| nodes { | |
| id | |
| content { ... on Issue { id } } | |
| } | |
| } | |
| } | |
| } | |
| }' \ | |
| -f projectId="${{ steps.project.outputs.id }}") | |
| else | |
| PAGE_RESULT=$(gh api graphql -f query=' | |
| query($projectId: ID!, $cursor: String!) { | |
| node(id: $projectId) { | |
| ... on ProjectV2 { | |
| items(first: 100, after: $cursor) { | |
| pageInfo { hasNextPage endCursor } | |
| nodes { | |
| id | |
| content { ... on Issue { id } } | |
| } | |
| } | |
| } | |
| } | |
| }' \ | |
| -f projectId="${{ steps.project.outputs.id }}" \ | |
| -f cursor="$CURSOR") | |
| fi | |
| ITEM_ID=$(echo "$PAGE_RESULT" | jq -r \ | |
| --arg issueId "${{ github.event.issue.node_id }}" \ | |
| '.data.node.items.nodes[] | select(.content.id == $issueId) | .id') | |
| [ -n "$ITEM_ID" ] && break | |
| HAS_NEXT=$(echo "$PAGE_RESULT" | jq -r '.data.node.items.pageInfo.hasNextPage') | |
| [ "$HAS_NEXT" != "true" ] && break | |
| CURSOR=$(echo "$PAGE_RESULT" | jq -r '.data.node.items.pageInfo.endCursor') | |
| done | |
| if [ -z "$ITEM_ID" ]; then | |
| echo "Error: failed to add issue to project and could not find an existing item — the PAT may lack access." | |
| exit 1 | |
| fi | |
| echo "Found existing project item: $ITEM_ID" | |
| fi | |
| echo "item_id=$ITEM_ID" >> "$GITHUB_OUTPUT" | |
| - name: Set initial field values | |
| if: github.event.action == 'opened' && vars.PSYNC_ENABLED != 'false' && vars.PSYNC_ENABLED != 'off' && steps.author_filter.outputs.allowed != 'false' | |
| env: | |
| GH_TOKEN: ${{ secrets.PSYNC_PAT }} | |
| run: | | |
| INITIAL_VALUES="${{ vars.PSYNC_INITIAL_VALUES }}" | |
| [ -z "$INITIAL_VALUES" ] && exit 0 | |
| PSYNC_FIELDS_JSON=$(gh api graphql -f query=' | |
| query($projectId: ID!) { | |
| node(id: $projectId) { | |
| ... on ProjectV2 { | |
| fields(first: 20) { | |
| nodes { | |
| ... on ProjectV2SingleSelectField { | |
| id | |
| name | |
| options { id name } | |
| } | |
| ... on ProjectV2Field { | |
| id | |
| name | |
| dataType | |
| } | |
| } | |
| } | |
| } | |
| } | |
| }' \ | |
| -f projectId="${{ steps.project.outputs.id }}" \ | |
| --jq '.data.node.fields.nodes') | |
| export PSYNC_PROJECT_ID="${{ steps.project.outputs.id }}" | |
| export PSYNC_FIELDS_JSON | |
| export PSYNC_INITIAL_VALUES="$INITIAL_VALUES" | |
| export PSYNC_REPO="${{ github.repository }}" | |
| # shellcheck source=/dev/null | |
| source "$RUNNER_TEMP/psync_apply_fields.sh" | |
| apply_fields "${{ steps.add_item.outputs.item_id }}" "${{ github.event.issue.number }}" | |
| - name: Get item ID and close Status option ID | |
| id: find_item | |
| if: github.event.action == 'closed' && vars.PSYNC_ENABLED != 'false' && vars.PSYNC_ENABLED != 'off' && steps.author_filter.outputs.allowed != 'false' | |
| env: | |
| GH_TOKEN: ${{ secrets.PSYNC_PAT }} | |
| run: | | |
| CLOSE_STATUS="${{ vars.PSYNC_CLOSE_STATUS }}" | |
| CLOSE_STATUS="${CLOSE_STATUS:-Done}" | |
| # Fetch Status field info once | |
| FIELDS_RESULT=$(gh api graphql -f query=' | |
| query($projectId: ID!) { | |
| node(id: $projectId) { | |
| ... on ProjectV2 { | |
| fields(first: 20) { | |
| nodes { | |
| ... on ProjectV2SingleSelectField { | |
| id | |
| name | |
| options { id name } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| }' \ | |
| -f projectId="${{ steps.project.outputs.id }}") | |
| FIELD_ID=$(echo "$FIELDS_RESULT" | jq -r \ | |
| '.data.node.fields.nodes[] | select(.name == "Status") | .id // empty') | |
| if [ -z "$FIELD_ID" ]; then | |
| echo "Error: Status field not found in the target project. Ensure the project has a single-select field named exactly 'Status' (case-sensitive)." | |
| exit 1 | |
| fi | |
| CLOSE_OPTION_ID=$(echo "$FIELDS_RESULT" | jq -r \ | |
| --arg status "$CLOSE_STATUS" \ | |
| '.data.node.fields.nodes[] | select(.name == "Status") | .options[] | select(.name == $status) | .id // empty') | |
| if [ -z "$CLOSE_OPTION_ID" ]; then | |
| echo "Error: Status option '$CLOSE_STATUS' not found in the target project. Check PSYNC_CLOSE_STATUS (default: Done) — value must match a Status option exactly (case-sensitive)." | |
| exit 1 | |
| fi | |
| # Paginate items until the matching issue is found | |
| ITEM_ID="" | |
| CURSOR="" | |
| while true; do | |
| if [ -z "$CURSOR" ]; then | |
| PAGE_RESULT=$(gh api graphql -f query=' | |
| query($projectId: ID!) { | |
| node(id: $projectId) { | |
| ... on ProjectV2 { | |
| items(first: 100) { | |
| pageInfo { hasNextPage endCursor } | |
| nodes { | |
| id | |
| content { ... on Issue { id } } | |
| } | |
| } | |
| } | |
| } | |
| }' \ | |
| -f projectId="${{ steps.project.outputs.id }}") | |
| else | |
| PAGE_RESULT=$(gh api graphql -f query=' | |
| query($projectId: ID!, $cursor: String!) { | |
| node(id: $projectId) { | |
| ... on ProjectV2 { | |
| items(first: 100, after: $cursor) { | |
| pageInfo { hasNextPage endCursor } | |
| nodes { | |
| id | |
| content { ... on Issue { id } } | |
| } | |
| } | |
| } | |
| } | |
| }' \ | |
| -f projectId="${{ steps.project.outputs.id }}" \ | |
| -f cursor="$CURSOR") | |
| fi | |
| ITEM_ID=$(echo "$PAGE_RESULT" | jq -r \ | |
| --arg issueId "${{ github.event.issue.node_id }}" \ | |
| '.data.node.items.nodes[] | select(.content.id == $issueId) | .id') | |
| [ -n "$ITEM_ID" ] && break | |
| HAS_NEXT=$(echo "$PAGE_RESULT" | jq -r '.data.node.items.pageInfo.hasNextPage') | |
| [ "$HAS_NEXT" != "true" ] && break | |
| CURSOR=$(echo "$PAGE_RESULT" | jq -r '.data.node.items.pageInfo.endCursor') | |
| done | |
| # If not found, add the issue now (handles race where closed fires before opened) | |
| if [ -z "$ITEM_ID" ]; then | |
| echo "Issue not yet in project — adding it now before setting close Status." | |
| ITEM_ID=$(gh api graphql -f query=' | |
| mutation($projectId: ID!, $contentId: ID!) { | |
| addProjectV2ItemById(input: { projectId: $projectId, contentId: $contentId }) { | |
| item { id } | |
| } | |
| }' \ | |
| -f projectId="${{ steps.project.outputs.id }}" \ | |
| -f contentId="${{ github.event.issue.node_id }}" \ | |
| --jq '.data.addProjectV2ItemById.item.id') | |
| if [ -z "$ITEM_ID" ] || [ "$ITEM_ID" = "null" ]; then | |
| echo "Error: failed to add issue to project. Cannot set close Status." | |
| exit 1 | |
| fi | |
| fi | |
| echo "item_id=$ITEM_ID" >> "$GITHUB_OUTPUT" | |
| echo "field_id=$FIELD_ID" >> "$GITHUB_OUTPUT" | |
| echo "close_option_id=$CLOSE_OPTION_ID" >> "$GITHUB_OUTPUT" | |
| - name: Set item close Status | |
| if: github.event.action == 'closed' && vars.PSYNC_ENABLED != 'false' && vars.PSYNC_ENABLED != 'off' && steps.author_filter.outputs.allowed != 'false' | |
| env: | |
| GH_TOKEN: ${{ secrets.PSYNC_PAT }} | |
| run: | | |
| ITEM_ID="${{ steps.find_item.outputs.item_id }}" | |
| OPTION_ID="${{ steps.find_item.outputs.close_option_id }}" | |
| gh api graphql -f query=' | |
| mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) { | |
| updateProjectV2ItemFieldValue(input: { | |
| projectId: $projectId | |
| itemId: $itemId | |
| fieldId: $fieldId | |
| value: { singleSelectOptionId: $optionId } | |
| }) { | |
| projectV2Item { id } | |
| } | |
| }' \ | |
| -f projectId="${{ steps.project.outputs.id }}" \ | |
| -f itemId="$ITEM_ID" \ | |
| -f fieldId="${{ steps.find_item.outputs.field_id }}" \ | |
| -f optionId="$OPTION_ID" |