Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
root = true

[.*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true

[*.md]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

[*.{yml,yaml}]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true

[Makefile]
charset = utf-8
end_of_line = lf
indent_style = tab
insert_final_newline = true
trim_trailing_whitespace = true

[*.sh]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
6 changes: 6 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
18 changes: 18 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: Tests

on:
workflow_dispatch: {}
pull_request:
branches:
- main

jobs:
tests:
name: Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5

- name: Run tests
shell: bash
run: make test
1 change: 1 addition & 0 deletions .shellcheckrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
external-sources=true
18 changes: 18 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
.PHONY: all test clean

PLATFORM := $(shell docker version --format '{{.Server.Os}}/{{.Server.Arch}}')
DOCKER := docker run --rm --network none --platform $(PLATFORM)

test: unit-tests lint

unit-tests:
@set -e; \
for f in tests/test*.sh; do \
echo "sh $$f"; \
sh "$$f"; \
done

lint:
$(DOCKER) -v ./Makefile:/work/Makefile:ro backplane/checkmake Makefile
$(DOCKER) -v .:/workspace:ro mstruebing/editorconfig-checker ec -exclude '^\.git/'
$(DOCKER) -v .:/mnt:ro koalaman/shellcheck -a -s sh --source-path=tests src/** tests/**
73 changes: 73 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
[![Build and Test](https://github.com/codereaper/create-issue-action/actions/workflows/test.yaml/badge.svg)](https://github.com/codereaper/create-issue-action/actions/workflows/test.yaml)

# Create Issue Action

A simple GitHub Action that **creates, updates, comments on, or closes issues** using the GitHub CLI (`gh`).

Ideal for CI/CD workflows that need to:

- Automatically open or update tracking issues
- Comment on existing issues from automation
- Close issues after builds or deployments are complete

## Features

- Create new issues with titles, bodies, templates, labels, and assignees
- Update existing issues automatically
- Add comments to existing issues
- Close issues by title and label search
- Uses `gh` CLI under the hood (no extra dependencies)

## Inputs

| Name | Description | Default | Required |
| ----------- | --------------------------------------------------------------------------------- | -------------------------- | -------- |
| `token` | GitHub token (PAT or `${{ github.token }}`) used for authentication | `${{ github.token }}` | Yes |
| `mode` | Operation mode: `create` or `close` | `create` | Yes |
| `state` | Issue state filter when searching for existing issues: `open`, `closed`, or `all` | `open` | Yes |
| `title` | Title of the issue to create or update | — | Yes |
| `labels` | Comma-separated list of labels used for creation and search | — | No |
| `assignees` | Comma-separated list of users to assign the issue to | — | No |
| `body` | Custom body text for the issue (overrides `template`) | — | No |
| `comment` | Optional comment text to add to an existing issue | — | No |
| `repo` | Repository to operate on (`owner/repo`) | `${{ github.repository }}` | Yes |

## Example Usage

```yaml
name: Reporting on failed builds
on:
push:
branches:
- main

jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Build
run: make build

report-failure:
runs-on: ubuntu-latest
needs: build
if: failure()
permissions:
contents: read
issues: write
steps:
- name: Report build failure
uses: CodeReaper/create-issue-action@v1
with:
title: "{{ github.workflow }} failed to build"
labels: automation
assignees: "@me"
body: See [the action log](https://github.com/{{ github.repository }}/actions/runs/{{ github.run_id }}) for more details.
```

# License

This project is released under the [MIT License](LICENSE)
74 changes: 74 additions & 0 deletions action.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
name: Create issue
description: Creates an issue

branding:
icon: git-pull-request
color: gray-dark

inputs:
token:
description: Your Github PAT, defaults to actions token
default: ${{ github.token }}
required: true
repo:
description: GitHub repository to create/update/close an issue in
default: ${{ github.repository }}
required: true
mode:
description: >
Dictates whether to create, update or close an issue.
Valid options: create | close
default: create
state:
description: >
State of issue to create, update or close.
Valid options: open | closed | all
default: open
title:
description: Title of issue to create or update
required: true
labels:
description: Labels (comma-separated) to both create the issue with and to filter the existing issue search with
required: false
assignees:
description: GitHub handle of the user(s) to assign the issue (comma-separated), only used for issue creation
required: false
body:
description: Body text of the issue
required: false
comment:
description: >
If set, an existing issue have this comment added to the issue.
Note, if the mode is set to create, then any previously added comment is updated instead
required: false

outputs:
url:
description: URL of the issue that was created
value: ${{ steps.action.outputs.url }}

runs:
using: composite
steps:
- name: Check dependencies are installed
shell: bash
run: |
if ! command -v gh >/dev/null 2>&1; then
echo "Command gh not found. This CLI tool is required."
exit 1
fi

- name: Create issue
id: action
shell: bash
env:
GH_TOKEN: ${{ inputs.token }}
INPUT_REPO: ${{ inputs.repo }}
INPUT_MODE: ${{ inputs.mode }}
INPUT_STATE: ${{ inputs.state }}
INPUT_TITLE: ${{ inputs.title }}
INPUT_LABELS: ${{ inputs.labels }}
INPUT_ASSIGNEES: ${{ inputs.assignees }}
INPUT_BODY: ${{ inputs.body }}
INPUT_COMMENT: ${{ inputs.comment }}
run: chmod +x "${{ github.action_path }}/scripts/main.sh" && "${{ github.action_path }}/src/main.sh"
74 changes: 74 additions & 0 deletions src/main.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#!/bin/sh

# cspell:ignore endgroup

set -eu

MODE="${INPUT_MODE:-create}"
STATE="${INPUT_STATE:-open}"
TITLE="${INPUT_TITLE}"
LABELS="${INPUT_LABELS:-}"
ASSIGNEES="${INPUT_ASSIGNEES:-}"
BODY="${INPUT_BODY:-}"
COMMENT="${INPUT_COMMENT:-}"
REPO="${INPUT_REPO:-}"

BODY_PRESENCE=${BODY:+(set)}
COMMENT_PRESENCE=${COMMENT:+(set)}

echo "::group::Debug info:"
echo " REPO: ${REPO}"
echo " MODE: ${MODE}"
echo " STATE: ${STATE}"
echo " TITLE: ${TITLE}"
echo " LABELS: ${LABELS}"
echo " ASSIGNEES: ${ASSIGNEES}"
echo " BODY: ${BODY_PRESENCE:-(not set)}"
echo " COMMENT: ${COMMENT_PRESENCE:-(not set)}"
echo '::endgroup::'

ISSUE_NUMBER=$(gh issue list --repo "${REPO}" --state "${STATE}" --label "${LABELS}" --search "in:title \"${TITLE}\"" --limit 1 --json number --jq '.[].number' || true)

# Perform action based on mode
case "${MODE}" in
create)
# Determine body args
if [ -n "${BODY}" ]; then
BODY_ARGS="\"${BODY}\""
else
BODY_ARGS="\"Auto-generated issue with title: ${TITLE}\""
fi

if [ -n "${ISSUE_NUMBER}" ]; then
echo "Issue already exists (#${ISSUE_NUMBER}), updating instead."
if [ -n "${COMMENT}" ]; then
echo "Adding comment to existing issue..."
gh issue comment "${ISSUE_NUMBER}" --repo "${REPO}" --body "${COMMENT}"
else
echo "Updating issue body..."
gh issue edit "${ISSUE_NUMBER}" --repo "${REPO}" --title "${TITLE}" --body "${BODY_ARGS}"
fi
else
echo "Creating new issue..."
gh issue create --repo "${REPO}" --title "${TITLE}" --body "${BODY_ARGS}" --label "${LABELS}" --assignee "${ASSIGNEES}"
fi
;;
close)
if [ -z "${ISSUE_NUMBER}" ]; then
echo "No matching issue found to close."
exit 0
fi
if [ -n "${COMMENT}" ]; then
echo "Adding closure comment..."
gh issue comment "${ISSUE_NUMBER}" --repo "${REPO}" --body "${COMMENT}"
fi
echo "Closing issue #${ISSUE_NUMBER}..."
gh issue close "${ISSUE_NUMBER}" --repo "${REPO}"
;;
*)
echo "Invalid mode '${MODE}'. Valid options: create | close"
exit 1
;;
esac

echo "Done."
29 changes: 29 additions & 0 deletions tests/gh.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#!/bin/sh
set -eu

case "$*" in
*"issue list"*)
# Simulate no issue found unless overridden
if [ "${GH_FAKE_MODE:-none}" = "issue-exists" ]; then
echo '42'
else
echo ''
fi
;;
*"issue create"*)
echo "FAKE: created issue"
;;
*"issue edit"*)
echo "FAKE: edited issue"
;;
*"issue comment"*)
echo "FAKE: added comment"
;;
*"issue close"*)
echo "FAKE: closed issue"
;;
*)
echo "FAKE: unknown gh command $*" >&2
exit 1
;;
esac
13 changes: 13 additions & 0 deletions tests/test-add-comment-to-existing-issue.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/bin/sh
set -eu

export GH_FAKE_MODE="issue-exists"
# shellcheck source=utils.sh
. tests/utils.sh

export INPUT_TITLE="Hello world"
export INPUT_COMMENT="Hello comment of world"
output=$(bash "${SCRIPT_PATH}" 2>&1)
assert_contains "$output" "Adding comment to existing issue..."
assert_contains "$output" "FAKE: added comment"
echo "passed"
16 changes: 16 additions & 0 deletions tests/test-close-existing-issue-with-comment.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/bin/sh
set -eu

export GH_FAKE_MODE="issue-exists"
# shellcheck source=utils.sh
. tests/utils.sh

export INPUT_MODE="close"
export INPUT_TITLE="Hello world"
export INPUT_COMMENT="Hello comment of world"
output=$(bash "${SCRIPT_PATH}" 2>&1)
assert_contains "$output" "Adding closure comment..."
assert_contains "$output" "FAKE: added comment"
assert_contains "$output" "Closing issue #42..."
assert_contains "$output" "FAKE: closed issue"
echo "passed"
13 changes: 13 additions & 0 deletions tests/test-close-existing-issue.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/bin/sh
set -eu

export GH_FAKE_MODE="issue-exists"
# shellcheck source=utils.sh
. tests/utils.sh

export INPUT_MODE="close"
export INPUT_TITLE="Hello world"
output=$(bash "${SCRIPT_PATH}" 2>&1)
assert_contains "$output" "Closing issue #42..."
assert_contains "$output" "FAKE: closed issue"
echo "passed"
11 changes: 11 additions & 0 deletions tests/test-close-with-no-matches.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/bin/sh
set -eu

# shellcheck source=utils.sh
. tests/utils.sh

export INPUT_MODE="close"
export INPUT_TITLE="Hello world"
output=$(bash "${SCRIPT_PATH}" 2>&1)
assert_contains "$output" "No matching issue found to close."
echo "passed"
Loading