feat: add upsert-issue composite action#55
Conversation
Create, update, reopen, or close a GitHub issue by title. Searches for an existing issue with the exact title and manages its lifecycle based on the 'open' input: - open=true: create new issue or update+reopen existing - open=false: close existing open issue or do nothing Includes composite action, README, and test workflow. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace the local composite action with devantler-tech/actions/upsert-issue@main, a shared org-level action that manages the full issue lifecycle. The 'open' input maps directly to the report's 'has-violations' output: open/reopen the issue with the report body close the issue or do nothing Depends on: devantler-tech/actions#55 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds a reusable upsert-issue composite action to manage GitHub issues by title (create/update/reopen/close) for report-style workflows, along with documentation and a smoke-test workflow.
Changes:
- Introduces
upsert-issuecomposite action implemented viaghCLI. - Adds README documentation and usage examples for the new action.
- Adds CI smoke test workflow for create + close behavior, and updates the repo actions index.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 7 comments.
| File | Description |
|---|---|
upsert-issue/action.yaml |
Implements the composite action logic and defines inputs/outputs. |
upsert-issue/README.md |
Documents inputs/outputs and provides usage examples. |
.github/workflows/test-upsert-issue.yaml |
Adds a smoke test workflow that exercises open + close flows. |
README.md |
Adds upsert-issue to the actions table. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if [ -z "${BODY:-}" ]; then | ||
| echo "::error::Either 'body' or 'body-file' input must be provided" | ||
| exit 1 | ||
| fi |
There was a problem hiding this comment.
The script currently errors if neither body nor body-file is provided, even when open is false (close/no-op path). This contradicts the input metadata (body is optional) and prevents a “close if exists, otherwise do nothing” usage. Consider only requiring body when open=true (or, alternatively, editing the issue body when closing), and align the validation + input docs accordingly.
| --search "in:title \"${TITLE}\"" \ | ||
| --json number,title,state \ | ||
| --jq " | ||
| [ .[] | select(.title == \"${TITLE}\") ] | | ||
| ([ .[] | select(.state == \"OPEN\") ] | first // empty) as \$open | | ||
| if \$open then \$open.number | ||
| else (last // empty | .number) | ||
| end | ||
| " |
There was a problem hiding this comment.
TITLE is interpolated directly into the --search string and the jq program. Titles containing quotes/newlines/backslashes can break the query, and direct interpolation into jq can enable jq-expression injection. Safer approach: output JSON from gh issue list and pipe it to jq -r --arg title "$TITLE" ... (or otherwise ensure robust escaping).
| --search "in:title \"${TITLE}\"" \ | |
| --json number,title,state \ | |
| --jq " | |
| [ .[] | select(.title == \"${TITLE}\") ] | | |
| ([ .[] | select(.state == \"OPEN\") ] | first // empty) as \$open | | |
| if \$open then \$open.number | |
| else (last // empty | .number) | |
| end | |
| " | |
| --json number,title,state | | |
| jq -r --arg title "$TITLE" ' | |
| [ .[] | select(.title == $title) ] | | |
| ([ .[] | select(.state == "OPEN") ] | first // empty) as $open | | |
| if $open then $open.number | |
| else (last // empty | .number // empty) | |
| end | |
| ' |
| gh issue list \ | ||
| --repo "$REPO" \ | ||
| --state all \ | ||
| --search "in:title \"${TITLE}\"" \ |
There was a problem hiding this comment.
gh issue list defaults to a limited number of results (30). Without an explicit --limit, an older matching issue may not be returned, causing the action to create duplicates. Add an appropriate --limit (e.g., 100 or more) when searching by title.
| --search "in:title \"${TITLE}\"" \ | |
| --search "in:title \"${TITLE}\"" \ | |
| --limit 100 \ |
| fi | ||
|
|
||
| if [ -n "${ISSUE_NUMBER:-}" ]; then | ||
| ISSUE_URL="https://github.com/${REPO}/issues/${ISSUE_NUMBER}" |
There was a problem hiding this comment.
Hardcoding https://github.com makes the issue-url output incorrect on GitHub Enterprise Server / non-public hosts. Prefer constructing the URL from $GITHUB_SERVER_URL (available in GitHub Actions env) or retrieving the URL from the API/gh output.
| ISSUE_URL="https://github.com/${REPO}/issues/${ISSUE_NUMBER}" | |
| ISSUE_URL="${GITHUB_SERVER_URL:-https://github.com}/${REPO}/issues/${ISSUE_NUMBER}" |
| if [ -n "$LABELS" ]; then | ||
| gh issue edit "$ISSUE_NUMBER" --repo "$REPO" --add-label "$LABELS" | ||
| fi | ||
| gh issue reopen "$ISSUE_NUMBER" --repo "$REPO" 2>/dev/null || true |
There was a problem hiding this comment.
gh issue reopen ... 2>/dev/null || true suppresses all errors, including real failures (e.g., permission issues), making debugging harder. Consider checking the issue state first and only calling reopen when needed, or selectively ignoring the specific “already open” case.
| gh issue reopen "$ISSUE_NUMBER" --repo "$REPO" 2>/dev/null || true | |
| ISSUE_STATE=$( | |
| gh issue view "$ISSUE_NUMBER" --repo "$REPO" --json state --jq .state | |
| ) | |
| if [ "$ISSUE_STATE" = "OPEN" ]; then | |
| echo "Issue #${ISSUE_NUMBER} is already open — nothing to do" | |
| else | |
| gh issue reopen "$ISSUE_NUMBER" --repo "$REPO" | |
| fi |
| | `title` | Title of the issue to create or update | ✅ | — | | ||
| | `body` | Body content of the issue | ❌ | — | | ||
| | `body-file` | Path to a file containing the body content (takes precedence over `body`) | ❌ | — | | ||
| | `labels` | Comma-separated list of labels to assign | ❌ | — | | ||
| | `open` | Whether the issue should be open (`true`) or closed (`false`) | ❌ | `true` | | ||
| | `close-comment` | Comment to post when closing the issue | ❌ | `✅ Resolved — closing this issue.` | |
There was a problem hiding this comment.
Documentation marks body as optional, but the action currently fails if both body and body-file are omitted (even when open=false). Either update the docs to reflect the requirement, or adjust the action to make body truly optional for the close/no-op path.
| on: | ||
| push: | ||
| branches: ["main"] | ||
| pull_request: | ||
|
|
||
| permissions: | ||
| contents: read | ||
| issues: write | ||
|
|
There was a problem hiding this comment.
This workflow runs on pull_request and requests issues: write, then calls the action with GITHUB_TOKEN to create/close issues. On PRs from forks, GITHUB_TOKEN is read-only regardless of requested permissions, so this job will fail. Consider gating these jobs to only run for same-repo PRs (or only on push to main).
Replace the local composite action with devantler-tech/actions/upsert-issue@main, a shared org-level action that manages the full issue lifecycle. The 'open' input maps directly to the report's 'has-violations' output: open/reopen the issue with the report body close the issue or do nothing Depends on: devantler-tech/actions#55 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…13) * refactor: use shared upsert-issue action from devantler-tech/actions Replace the local composite action with devantler-tech/actions/upsert-issue@main, a shared org-level action that manages the full issue lifecycle. The 'open' input maps directly to the report's 'has-violations' output: open/reopen the issue with the report body close the issue or do nothing Depends on: devantler-tech/actions#55 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: pin upsert-issue action to commit SHA Pin devantler-tech/actions/upsert-issue to e3a0bd51 to satisfy zizmor and CodeQL unpinned action reference checks. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
🎉 This PR is included in version 2.1.0 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
Summary
Adds a reusable
upsert-issuecomposite action that manages the full lifecycle of a GitHub issue by title:open=true: Creates a new issue or updates + reopens an existing oneopen=false: Closes an existing open issue with a comment, or does nothingFiles
upsert-issue/action. Composite action usingghCLIyamlupsert-issue/README. Documentation with usage examplesmd.github/workflows/test-upsert-issue. Test workflow (create + close)yamlREADME. Updated actions tablemdMotivation
The
devantler-tech/maintenancerepo needs to manage report issues based on whether violations were found. This generic action can be used by any workflow that needs to create-or-update issues based on dynamic conditions.Inspired by dvt-engineering's upsert-issue, implemented as a composite action per CONTRIBUTING.md conventions.