name: Shared - Create Issue on Failure
on: workflow_call: inputs: status: description: 'Pipeline status: success, failure, cancelled' required: true type: string environment: description: 'Deployment environment (develop, prod)' required: false type: string default: '' version: description: 'Release version (e.g., 1.2.3)' required: false type: string default: '' changelog: description: 'Release changelog (commit list)' required: false type: string default: '' message: description: 'Custom message to include in the issue body' required: false type: string default: '' labels: description: 'Labels for the GitHub issue (comma-separated)' required: false type: string default: 'bug,pipeline-failure'
# Least privilege for what this workflow actually does. Without it the# job inherits whatever the calling repository's default happens to be,# which on older repositories is write-all.permissions: contents: read issues: write
jobs: create-issue: name: Create Issue if: inputs.status == 'failure' runs-on: ubuntu-latest timeout-minutes: 2
steps: - name: Create GitHub issue on failure env: GH_TOKEN: ${{ github.token }} ENVIRONMENT: ${{ inputs.environment }} VERSION: ${{ inputs.version }} CHANGELOG: ${{ inputs.changelog }} CUSTOM_MESSAGE: ${{ inputs.message }} REPO: ${{ github.repository }} BRANCH: ${{ github.ref_name }} ACTOR: ${{ github.actor }} SERVER_URL: ${{ github.server_url }} RUN_ID: ${{ github.run_id }} ISSUE_LABELS: ${{ inputs.labels }} run: | set -euo pipefail
ISSUE_TITLE="Pipeline failure: ${REPO##*/} (${BRANCH}) — ${ENVIRONMENT:-no-env}"
ISSUE_BODY="## Pipeline Failure\n\n" ISSUE_BODY+="| Field | Value |\n" ISSUE_BODY+="|-------|-------|\n" ISSUE_BODY+="| **Repository** | [${REPO}](${SERVER_URL}/${REPO}) |\n" ISSUE_BODY+="| **Branch** | \`${BRANCH}\` |\n" ISSUE_BODY+="| **Environment** | ${ENVIRONMENT:-N/A} |\n" ISSUE_BODY+="| **Actor** | @${ACTOR} |\n" ISSUE_BODY+="| **Run** | [View workflow run](${SERVER_URL}/${REPO}/actions/runs/${RUN_ID}) |\n" ISSUE_BODY+="| **Commit** | \`${GITHUB_SHA:0:7}\` |\n"
if [ -n "$CUSTOM_MESSAGE" ]; then ISSUE_BODY+="\n### Message\n${CUSTOM_MESSAGE}\n" fi
if [ -n "$VERSION" ] && [ -n "$CHANGELOG" ]; then ISSUE_BODY+="\n### Changelog (v${VERSION})\n${CHANGELOG}\n" fi
ISSUE_BODY+="\n---\n*Auto-generated by CI pipeline*"
# The body reaches gh through a file and the arguments through an array. # Both title and body carry text this workflow does not control: BRANCH is # github.ref_name and CHANGELOG is raw `git log %s` output, so a commit # subject is attacker-authored input. Any construct that re-parses these # values as shell -- `eval`, or a command substitution around them -- turns # a commit subject into code running on the runner. printf '%b' expands the # \n escapes above without ever re-entering the parser. BODY_FILE="${RUNNER_TEMP:-/tmp}/issue-body.md" printf '%b' "$ISSUE_BODY" > "$BODY_FILE"
GH_ARGS=( --repo "$REPO" --title "$ISSUE_TITLE" --body-file "$BODY_FILE" --assignee "$ACTOR" )
if [ -n "$ISSUE_LABELS" ]; then IFS=',' read -ra LABELS <<< "$ISSUE_LABELS" for LABEL in "${LABELS[@]}"; do LABEL=$(echo "$LABEL" | xargs) if [ -n "$LABEL" ]; then GH_ARGS+=(--label "$LABEL") fi done fi
ISSUE_URL=$(gh issue create "${GH_ARGS[@]}" 2>&1) || true
if [ -n "$ISSUE_URL" ]; then echo "GitHub issue created: $ISSUE_URL" else echo "::warning::Failed to create GitHub issue" fiShared (cross-cutting)·Reusable workflow·on: workflow_call
Shared Create Issue On Failure
Shared - Create Issue on Failure
.github/workflows/shared-create-issue-on-failure.yml