Skip to content

feat: add upsert-issue composite action#55

Merged
botantler[bot] merged 1 commit intomainfrom
feat/upsert-issue
Apr 12, 2026
Merged

feat: add upsert-issue composite action#55
botantler[bot] merged 1 commit intomainfrom
feat/upsert-issue

Conversation

@devantler
Copy link
Copy Markdown
Contributor

Summary

Adds a reusable upsert-issue composite action that manages the full lifecycle of a GitHub issue by title:

  • open=true: Creates a new issue or updates + reopens an existing one
  • open=false: Closes an existing open issue with a comment, or does nothing

Files

  • upsert-issue/action. Composite action using gh CLIyaml
  • upsert-issue/README. Documentation with usage examplesmd
  • .github/workflows/test-upsert-issue. Test workflow (create + close)yaml
  • README. Updated actions tablemd

Motivation

The devantler-tech/maintenance repo 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.

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>
Copilot AI review requested due to automatic review settings April 12, 2026 19:24
@botantler botantler Bot enabled auto-merge (squash) April 12, 2026 19:24
devantler added a commit to devantler-tech/maintenance that referenced this pull request Apr 12, 2026
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>
@botantler botantler Bot merged commit e3a0bd5 into main Apr 12, 2026
30 checks passed
@botantler botantler Bot deleted the feat/upsert-issue branch April 12, 2026 19:25
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-issue composite action implemented via gh CLI.
  • 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.

Comment thread upsert-issue/action.yaml
Comment on lines +65 to +68
if [ -z "${BODY:-}" ]; then
echo "::error::Either 'body' or 'body-file' input must be provided"
exit 1
fi
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread upsert-issue/action.yaml
Comment on lines +76 to +84
--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
"
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
--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
'

Copilot uses AI. Check for mistakes.
Comment thread upsert-issue/action.yaml
gh issue list \
--repo "$REPO" \
--state all \
--search "in:title \"${TITLE}\"" \
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
--search "in:title \"${TITLE}\"" \
--search "in:title \"${TITLE}\"" \
--limit 100 \

Copilot uses AI. Check for mistakes.
Comment thread upsert-issue/action.yaml
fi

if [ -n "${ISSUE_NUMBER:-}" ]; then
ISSUE_URL="https://github.com/${REPO}/issues/${ISSUE_NUMBER}"
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
ISSUE_URL="https://github.com/${REPO}/issues/${ISSUE_NUMBER}"
ISSUE_URL="${GITHUB_SERVER_URL:-https://github.com}/${REPO}/issues/${ISSUE_NUMBER}"

Copilot uses AI. Check for mistakes.
Comment thread upsert-issue/action.yaml
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
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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

Copilot uses AI. Check for mistakes.
Comment thread upsert-issue/README.md
Comment on lines +9 to +14
| `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.` |
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +3 to +11
on:
push:
branches: ["main"]
pull_request:

permissions:
contents: read
issues: write

Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
devantler added a commit to devantler-tech/maintenance that referenced this pull request Apr 12, 2026
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>
botantler Bot pushed a commit to devantler-tech/maintenance that referenced this pull request Apr 12, 2026
…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>
@botantler
Copy link
Copy Markdown

botantler Bot commented Apr 12, 2026

🎉 This PR is included in version 2.1.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

@botantler botantler Bot added the released label Apr 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants