Skip to content
Merged
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
392 changes: 392 additions & 0 deletions .github/workflows/jira_codex_pr.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,392 @@
# .github/workflows/jira-codex-pr.yml
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

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

The filename in the comment on line 1 is ".github/workflows/jira-codex-pr.yml" but the actual filename is "jira_codex_pr.yml" (with underscores, not hyphens). This inconsistency could cause confusion during maintenance.

Suggested change
# .github/workflows/jira-codex-pr.yml
# .github/workflows/jira_codex_pr.yml

Copilot uses AI. Check for mistakes.
name: Implement Jira ticket with Codex and open/update PR (uv + python)

on:
repository_dispatch:
types: [jira_implement]

permissions:
contents: write
pull-requests: write

concurrency:
group: jira-${{ github.event.client_payload.jira_key }}
cancel-in-progress: false

env:
# ---------------- GUARDRAILS ----------------
ALLOWED_JIRA_PROJECT_KEYS: "ABC,DEF" # comma-separated
ALLOWED_ISSUE_TYPES: "Story,Bug,Task" # comma-separated
REQUIRED_LABEL: "codex" # require this label on the Jira issue
REQUIRED_CUSTOM_FIELD_ID: "" # optional; e.g. "customfield_12345" (leave empty to disable)

# ---------------- BRANCH/PR ----------------
BASE_BRANCH: "main"
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

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

The workflow hardcodes BASE_BRANCH as "main" (line 24), but all other workflows in this repository use "production" as the main branch (see tests.yml:8, CD_production.yml:5, format_code.yml:7). The repository appears to follow a production/staging/transfer branch model, not a main-based model. Using "main" as the base branch will likely cause issues if that branch doesn't exist or isn't the actual integration target.

Suggested change
BASE_BRANCH: "main"
BASE_BRANCH: "production"

Copilot uses AI. Check for mistakes.
BRANCH_PREFIX: "jira"
MAX_TITLE_SLUG_LEN: "40"

# ---------------- PYTHON/UV ----------------
PYTHON_VERSION: "3.13"
MAX_DESC_CHARS: "8000"

# Commands (run inside uv env)
FORMAT_COMMAND: "uv run black ."
LINT_COMMAND: "uv run flake8"
TEST_COMMAND: "uv run pytest -q"

jobs:
implement:
runs-on: ubuntu-latest
timeout-minutes: 60

steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0

- name: Ensure jq exists
run: |
set -euo pipefail
if ! command -v jq >/dev/null 2>&1; then
sudo apt-get update
sudo apt-get install -y jq
fi

- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: ${{ env.PYTHON_VERSION }}
Comment on lines +56 to +59
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

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

The Python version is hardcoded as "3.13", which differs from other workflows in the repository that use 'python-version-file: "pyproject.toml"' (see tests.yml:62-63). While 3.13 matches the minimum requirement in pyproject.toml, using 'python-version-file' is more maintainable as it automatically stays in sync with the project configuration. This is especially important for workflows that build and test the actual project code.

Copilot uses AI. Check for mistakes.

- name: Set up uv (with cache)
uses: astral-sh/setup-uv@38f3f104447c67c051c4a08e39b64a148898af3a # v4
Comment on lines +44 to +62
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

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

The action versions are pinned to SHAs which deviates from the established convention in this repository. All other workflows (tests.yml, format_code.yml, CD_production.yml) use semantic version tags (v4, v5, v6) for actions like checkout, setup-python, and setup-uv. For consistency and maintainability, consider using semantic version tags instead of SHA pins. Semantic versions are easier to understand, manage, and update, while still providing security through trusted GitHub Actions publishers.

Copilot uses AI. Check for mistakes.
with:
enable-cache: true

- name: Ensure uv.lock exists (determinism)
run: |
set -euo pipefail
test -f uv.lock || (echo "uv.lock missing; commit it for deterministic CI." && exit 1)

- name: Sync dependencies (pyproject/uv.lock)
run: |
set -euo pipefail
uv sync --all-extras --dev
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

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

The workflow uses 'uv sync --all-extras --dev' which differs from the established pattern 'uv sync --locked --all-extras --dev' used in tests.yml:66. The '--locked' flag ensures that uv uses the exact versions from uv.lock without updating it, which is critical for deterministic CI builds. While line 69 checks for uv.lock existence, without --locked, uv might still update the lock file if dependencies are out of sync, undermining the determinism check.

Suggested change
uv sync --all-extras --dev
uv sync --locked --all-extras --dev

Copilot uses AI. Check for mistakes.

- name: Verify tooling exists
run: |
set -euo pipefail
uv run black --version
uv run flake8 --version
uv run pytest --version

- name: Read Jira key
id: jira
run: |
set -euo pipefail
KEY="${{ github.event.client_payload.jira_key }}"
if [ -z "$KEY" ] || [ "$KEY" = "null" ]; then
echo "Missing jira_key in dispatch payload."
exit 1
fi
echo "JIRA_KEY=$KEY" >> $GITHUB_OUTPUT

- name: Fetch Jira issue JSON
id: issue
env:
JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }}
JIRA_EMAIL: ${{ secrets.JIRA_EMAIL }}
JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }}
JIRA_KEY: ${{ steps.jira.outputs.JIRA_KEY }}
MAX_DESC_CHARS: ${{ env.MAX_DESC_CHARS }}
run: |
set -euo pipefail
curl -fsS --retry 3 --retry-all-errors -u "$JIRA_EMAIL:$JIRA_API_TOKEN" \
-H "Accept: application/json" \
"$JIRA_BASE_URL/rest/api/3/issue/$JIRA_KEY" > jira.json
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Avoid committing Jira payload data into the repo

This writes the Jira API response to jira.json in the repository root, and later the workflow does git add -A, which stages all untracked files; as a result, Jira issue payloads can be committed into PRs unintentionally. This also causes the "there is a diff" check to pass even when no real code changes were made, because jira.json itself creates a diff.

Useful? React with 👍 / 👎.


SUMMARY=$(jq -r '.fields.summary // empty' jira.json)
ISSUE_TYPE=$(jq -r '.fields.issuetype.name // empty' jira.json)
PROJECT_KEY=$(jq -r '.fields.project.key // empty' jira.json)

if [ -z "$SUMMARY" ] || [ -z "$ISSUE_TYPE" ] || [ -z "$PROJECT_KEY" ]; then
echo "Missing one of: summary, issuetype, project.key"
exit 1
fi

LABELS=$(jq -r '.fields.labels[]? // empty' jira.json | tr '\n' ',' | sed 's/,$//')
DESC=$(jq -c '.fields.description // {}' jira.json)
DESC_TRIMMED="${DESC:0:${MAX_DESC_CHARS}}"

{
echo "SUMMARY<<EOF"
echo "$SUMMARY"
echo "EOF"
echo "ISSUE_TYPE<<EOF"
echo "$ISSUE_TYPE"
echo "EOF"
echo "PROJECT_KEY<<EOF"
echo "$PROJECT_KEY"
echo "EOF"
echo "LABELS<<EOF"
echo "$LABELS"
echo "EOF"
echo "DESC<<EOF"
echo "$DESC_TRIMMED"
echo "EOF"
} >> "$GITHUB_OUTPUT"
Comment on lines +121 to +137
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

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

The GITHUB_OUTPUT assignments use heredoc syntax (EOF) for multiline safety, which is good. However, for LABELS (line 131-133) and PROJECT_KEY (line 128-130), which are typically single-line values, this might be unnecessary. More importantly, the DESC_TRIMMED value is truncated to MAX_DESC_CHARS (line 119), but if it contains the string "EOF" naturally, the heredoc parsing could break. Consider using a more unique delimiter like "EODESC_${RANDOM}" or validate that the content doesn't contain the delimiter.

Copilot uses AI. Check for mistakes.

- name: Guardrails - allowlists
env:
PROJECT_KEY: ${{ steps.issue.outputs.PROJECT_KEY }}
ISSUE_TYPE: ${{ steps.issue.outputs.ISSUE_TYPE }}
LABELS: ${{ steps.issue.outputs.LABELS }}
REQUIRED_LABEL: ${{ env.REQUIRED_LABEL }}
ALLOWED_JIRA_PROJECT_KEYS: ${{ env.ALLOWED_JIRA_PROJECT_KEYS }}
ALLOWED_ISSUE_TYPES: ${{ env.ALLOWED_ISSUE_TYPES }}
run: |
set -euo pipefail

echo "$ALLOWED_JIRA_PROJECT_KEYS" | tr ',' '\n' | grep -Fxq "$PROJECT_KEY" || {
echo "Project $PROJECT_KEY not allowed (allowed: $ALLOWED_JIRA_PROJECT_KEYS)."
exit 1
}

echo "$ALLOWED_ISSUE_TYPES" | tr ',' '\n' | grep -Fxq "$ISSUE_TYPE" || {
echo "Issue type $ISSUE_TYPE not allowed (allowed: $ALLOWED_ISSUE_TYPES)."
exit 1
}

if [ -n "$REQUIRED_LABEL" ]; then
echo "$LABELS" | tr ',' '\n' | grep -Fxq "$REQUIRED_LABEL" || {
echo "Required label '$REQUIRED_LABEL' not present."
exit 1
}
fi

- name: Guardrails - optional required custom field
if: ${{ env.REQUIRED_CUSTOM_FIELD_ID != '' }}
env:
FIELD_ID: ${{ env.REQUIRED_CUSTOM_FIELD_ID }}
run: |
set -euo pipefail
VAL=$(jq -r --arg f "$FIELD_ID" '.fields[$f] // empty' jira.json)
if [ -z "$VAL" ] || [ "$VAL" = "false" ]; then
echo "Required Jira field $FIELD_ID not set."
exit 1
fi

- name: Compute branch name
id: branch
env:
JIRA_KEY: ${{ steps.jira.outputs.JIRA_KEY }}
SUMMARY: ${{ steps.issue.outputs.SUMMARY }}
BRANCH_PREFIX: ${{ env.BRANCH_PREFIX }}
MAX_TITLE_SLUG_LEN: ${{ env.MAX_TITLE_SLUG_LEN }}
run: |
set -euo pipefail
SAFE=$(echo "$SUMMARY" | tr '[:upper:]' '[:lower:]' | tr -cd 'a-z0-9 -' | tr ' ' '-' | sed 's/--*/-/g' | sed 's/^-//;s/-$//')
SAFE=$(echo "$SAFE" | cut -c1-"$MAX_TITLE_SLUG_LEN")
if [ -z "$SAFE" ]; then
SAFE="ticket"
fi
BRANCH="${BRANCH_PREFIX}/${JIRA_KEY}-${SAFE}"
echo "BRANCH=$BRANCH" >> $GITHUB_OUTPUT
echo "BRANCH=$BRANCH" >> $GITHUB_ENV

- name: Ensure branch exists (idempotent)
env:
BASE_BRANCH: ${{ env.BASE_BRANCH }}
run: |
set -euo pipefail
git fetch origin "$BASE_BRANCH"
git fetch origin "$BRANCH" || true

if git show-ref --verify --quiet "refs/remotes/origin/$BRANCH"; then
echo "Branch exists on origin. Checking it out."
git checkout -B "$BRANCH" "origin/$BRANCH"
else
echo "Creating new branch from $BASE_BRANCH."
git checkout -B "$BRANCH" "origin/$BASE_BRANCH"
fi
Comment on lines +203 to +211
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

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

There's a potential race condition in the branch checkout logic. Lines 203-211 check if the branch exists and either checkout the existing branch or create a new one from BASE_BRANCH. However, if this workflow runs concurrently for the same JIRA key (which is prevented by the concurrency group on lines 12-14), or if the branch is deleted between the fetch (line 203) and the checkout (line 207), this could fail. While the concurrency setting mitigates the first issue, consider adding error handling for the checkout operations.

Copilot uses AI. Check for mistakes.

- name: Run Codex to implement ticket
uses: openai/codex-action@94bb7a052e529936e5260a35838e61b190855739 # v1
with:
openai_api_key: ${{ secrets.OPENAI_API_KEY }}
prompt: |
You are implementing Jira ticket ${{ steps.jira.outputs.JIRA_KEY }} in this repository.

Ticket metadata:
- Title: ${{ steps.issue.outputs.SUMMARY }}
- Type: ${{ steps.issue.outputs.ISSUE_TYPE }}
- Project: ${{ steps.issue.outputs.PROJECT_KEY }}
- Description (ADF/JSON): ${{ steps.issue.outputs.DESC }}

Scope & guardrails:
- Minimal, well-scoped change set; avoid refactors unless necessary.
- Do NOT touch secrets, credentials, or CI config unless explicitly required.
- Avoid these paths unless absolutely necessary:
- .github/
- infra/
- terraform/
- k8s/
- deploy/
- helm/

Python repo conventions (must follow):
- Format: black .
- Lint: flake8
- Tests: pytest -q
- Add/update tests when behavior changes.
- Keep style consistent with existing code.

Before finishing:
- Ensure black, flake8, and pytest pass in this workflow environment.

Operational constraints:
- Implement changes directly in the checked-out branch.
- Do not create additional branches.
- Do not rewrite git history.

Comment on lines +213 to +251
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

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

The reference to 'openai/codex-action@94bb7a052e529936e5260a35838e61b190855739' appears to be a placeholder or non-existent action. OpenAI does not publish a GitHub Action called 'codex-action' in their official repositories. This will cause the workflow to fail when executed. You'll need to either use a valid action for AI code generation, or implement this step with direct API calls to your AI service.

Suggested change
- name: Run Codex to implement ticket
uses: openai/codex-action@94bb7a052e529936e5260a35838e61b190855739 # v1
with:
openai_api_key: ${{ secrets.OPENAI_API_KEY }}
prompt: |
You are implementing Jira ticket ${{ steps.jira.outputs.JIRA_KEY }} in this repository.
Ticket metadata:
- Title: ${{ steps.issue.outputs.SUMMARY }}
- Type: ${{ steps.issue.outputs.ISSUE_TYPE }}
- Project: ${{ steps.issue.outputs.PROJECT_KEY }}
- Description (ADF/JSON): ${{ steps.issue.outputs.DESC }}
Scope & guardrails:
- Minimal, well-scoped change set; avoid refactors unless necessary.
- Do NOT touch secrets, credentials, or CI config unless explicitly required.
- Avoid these paths unless absolutely necessary:
- .github/
- infra/
- terraform/
- k8s/
- deploy/
- helm/
Python repo conventions (must follow):
- Format: black .
- Lint: flake8
- Tests: pytest -q
- Add/update tests when behavior changes.
- Keep style consistent with existing code.
Before finishing:
- Ensure black, flake8, and pytest pass in this workflow environment.
Operational constraints:
- Implement changes directly in the checked-out branch.
- Do not create additional branches.
- Do not rewrite git history.
- name: Run OpenAI API to implement ticket
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
run: |
set -euo pipefail
if [ -z "${OPENAI_API_KEY:-}" ]; then
echo "ERROR: OPENAI_API_KEY is not set. Please configure secrets.OPENAI_API_KEY."
exit 1
fi
python - << 'PY'
import json
import os
import sys
import urllib.request
api_key = os.environ.get("OPENAI_API_KEY")
if not api_key:
sys.stderr.write("OPENAI_API_KEY is not set in environment.\n")
sys.exit(1)
prompt = """You are implementing Jira ticket ${{ steps.jira.outputs.JIRA_KEY }} in this repository.
Ticket metadata:
- Title: ${{ steps.issue.outputs.SUMMARY }}
- Type: ${{ steps.issue.outputs.ISSUE_TYPE }}
- Project: ${{ steps.issue.outputs.PROJECT_KEY }}
- Description (ADF/JSON): ${{ steps.issue.outputs.DESC }}
Scope & guardrails:
- Minimal, well-scoped change set; avoid refactors unless necessary.
- Do NOT touch secrets, credentials, or CI config unless explicitly required.
- Avoid these paths unless absolutely necessary:
- .github/
- infra/
- terraform/
- k8s/
- deploy/
- helm/
Python repo conventions (must follow):
- Format: black .
- Lint: flake8
- Tests: pytest -q
- Add/update tests when behavior changes.
- Keep style consistent with existing code.
Before finishing:
- Ensure black, flake8, and pytest pass in this workflow environment.
Operational constraints:
- Implement changes directly in the checked-out branch.
- Do not create additional branches.
- Do not rewrite git history.
"""
payload = {
"model": "gpt-4.1-mini",
"messages": [
{
"role": "system",
"content": "You are an AI assistant that helps implement Jira tickets by proposing concrete code changes."
},
{
"role": "user",
"content": prompt,
},
],
}
data = json.dumps(payload).encode("utf-8")
req = urllib.request.Request(
"https://api.openai.com/v1/chat/completions",
data=data,
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {api_key}",
},
method="POST",
)
try:
with urllib.request.urlopen(req) as resp:
body = resp.read().decode("utf-8")
except Exception as e:
sys.stderr.write(f"Error calling OpenAI API: {e}\n")
sys.exit(1)
# Print the raw response to the workflow logs.
print(body)
PY

Copilot uses AI. Check for mistakes.
- name: Enforce forbidden paths policy
env:
LABELS: ${{ steps.issue.outputs.LABELS }}
run: |
set -euo pipefail
FORBIDDEN_REGEX='^(\.github/|infra/|terraform/|k8s/|deploy/|helm/)'
ALLOW_LABEL="codex-allow-infra"

if echo "$LABELS" | tr ',' '\n' | grep -Fxq "$ALLOW_LABEL"; then
echo "Override label present ($ALLOW_LABEL); skipping forbidden-path check."
exit 0
fi

git fetch origin "$BASE_BRANCH"
CHANGED=$(git diff --name-only "origin/$BASE_BRANCH...HEAD" || true)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Inspect forbidden paths in uncommitted changes

The forbidden-path guard runs before the commit, but git diff --name-only "origin/$BASE_BRANCH...HEAD" only compares commits (git diff -h shows the <commit>...<commit> form), so it does not include the working-tree edits produced by Codex in this job. That means changes under .github/, infra/, terraform/, etc. can bypass this policy and still get committed/pushed in later steps.

Useful? React with 👍 / 👎.


if echo "$CHANGED" | grep -E "$FORBIDDEN_REGEX"; then
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

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

The grep command on line 268 uses '-E' for extended regex matching of the forbidden paths. However, the command doesn't handle the case where CHANGED is empty (no files changed). An empty string piped to grep will cause grep to hang waiting for stdin. While line 266 uses '|| true' which should prevent errors, if there are no changes, $CHANGED will be empty and 'echo ""' | grep -E' will not match anything and exit 1, which is correct. However, for clarity and robustness, consider adding a check for empty CHANGED before the grep.

Suggested change
if echo "$CHANGED" | grep -E "$FORBIDDEN_REGEX"; then
if [ -n "$CHANGED" ] && echo "$CHANGED" | grep -E "$FORBIDDEN_REGEX"; then

Copilot uses AI. Check for mistakes.
echo "Forbidden paths modified. Add label '$ALLOW_LABEL' on Jira issue to allow."
echo "$CHANGED" | sed 's/^/ - /'
exit 1
fi

- name: Run format, lint, tests
env:
FORMAT_COMMAND: ${{ env.FORMAT_COMMAND }}
LINT_COMMAND: ${{ env.LINT_COMMAND }}
TEST_COMMAND: ${{ env.TEST_COMMAND }}
run: |
set -euo pipefail
eval "$FORMAT_COMMAND"
eval "$LINT_COMMAND"
eval "$TEST_COMMAND"
Comment on lines +281 to +283
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

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

The 'eval' command is used to execute commands from environment variables, which could be a security risk if those variables are ever influenced by untrusted input. While in this case the commands come from the workflow's own env section (lines 33-35), this pattern should be avoided. Instead, directly execute the commands without eval, or if flexibility is needed, use a more restrictive approach like parameter arrays.

Suggested change
eval "$FORMAT_COMMAND"
eval "$LINT_COMMAND"
eval "$TEST_COMMAND"
$FORMAT_COMMAND
$LINT_COMMAND
$TEST_COMMAND

Copilot uses AI. Check for mistakes.

- name: Ensure there is a diff (fail-fast)
run: |
set -euo pipefail
if git status --porcelain | grep .; then
echo "Changes detected."
else
echo "No changes produced; failing to avoid empty PR."
exit 1
fi

- name: Commit & push (idempotent)
env:
JIRA_KEY: ${{ steps.jira.outputs.JIRA_KEY }}
SUMMARY: ${{ steps.issue.outputs.SUMMARY }}
run: |
set -euo pipefail
git add -A
git commit -m "${JIRA_KEY}: ${SUMMARY}" || echo "Nothing new to commit."
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Fail the job when git commit returns an error

Using git commit ... || echo "Nothing new to commit." masks all commit failures and continues to push. Since the previous step already requires a dirty worktree, a non-zero git commit here indicates a real error (for example identity/rejection issues), and suppressing it can push a branch without the generated changes and create stale or empty PR updates.

Useful? React with 👍 / 👎.

Comment on lines +295 to +302
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

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

Git user configuration is missing before the commit step. Other workflows in this repository (CD_production.yml:103-104, CD_staging.yml:104-105) explicitly set 'git config --global user.name' and 'user.email' before committing. Without this configuration, the git commit on line 302 will fail with an error about missing user identity. Add a step to configure git user before the commit step.

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

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

The commit on line 302 uses '|| echo "Nothing new to commit."' which means if the commit fails for any reason (not just because there's nothing to commit), the workflow will silently continue. This could mask real errors. Consider checking git status explicitly before attempting the commit, or handle the specific exit code that indicates nothing to commit (exit code 1 with no changes).

Suggested change
git commit -m "${JIRA_KEY}: ${SUMMARY}" || echo "Nothing new to commit."
if git diff --cached --quiet; then
echo "Nothing new to commit."
else
git commit -m "${JIRA_KEY}: ${SUMMARY}"
fi

Copilot uses AI. Check for mistakes.
git push --set-upstream origin "$BRANCH"
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

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

The 'git push' command on line 303 lacks the '--force-with-lease' option or any handling for diverged branches. If the remote branch has been updated by another process (or manually) since the workflow last fetched it, the push will fail. Since line 285-293 checks for changes but line 302 might skip the commit, there's a scenario where the workflow could fail here unexpectedly. Consider adding error handling or using '--force-with-lease' if overwrites are intentional.

Suggested change
git push --set-upstream origin "$BRANCH"
git push --force-with-lease --set-upstream origin "$BRANCH"

Copilot uses AI. Check for mistakes.

- name: Create or update PR (idempotent)
id: pr
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BASE_BRANCH: ${{ env.BASE_BRANCH }}
JIRA_KEY: ${{ steps.jira.outputs.JIRA_KEY }}
SUMMARY: ${{ steps.issue.outputs.SUMMARY }}
run: |
set -euo pipefail

EXISTING=$(gh pr list --head "$BRANCH" --json number,state,url --jq '.[0] // empty')

BODY_FILE="$(mktemp)"
cat > "$BODY_FILE" <<EOF
Automated implementation via Codex.

Jira: ${JIRA_KEY}

CI:
- black
- flake8
- pytest

Notes:
- Forbidden paths are blocked unless Jira has label: codex-allow-infra
EOF

if [ -n "$EXISTING" ]; then
NUM=$(echo "$EXISTING" | jq -r '.number')
STATE=$(echo "$EXISTING" | jq -r '.state')
URL=$(echo "$EXISTING" | jq -r '.url')

echo "Found existing PR #$NUM ($STATE): $URL"

if [ "$STATE" = "CLOSED" ]; then
gh pr reopen "$NUM"
fi

gh pr edit "$NUM" --title "${JIRA_KEY}: ${SUMMARY}" --body-file "$BODY_FILE" --base "$BASE_BRANCH"

echo "PR_URL=$URL" >> $GITHUB_OUTPUT
echo "PR_NUMBER=$NUM" >> $GITHUB_OUTPUT
else
URL=$(gh pr create \
--title "${JIRA_KEY}: ${SUMMARY}" \
--body-file "$BODY_FILE" \
--base "$BASE_BRANCH" \
--head "$BRANCH")
echo "Created PR: $URL"
echo "PR_URL=$URL" >> $GITHUB_OUTPUT
Comment on lines +352 to +354
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

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

The PR number is only set in the GITHUB_OUTPUT when an existing PR is found (line 346), but not when a new PR is created (lines 348-355). This means downstream steps that might depend on PR_NUMBER will not have access to it for newly created PRs. While the current workflow doesn't appear to use PR_NUMBER after this step, for consistency and future-proofing, the PR number should be extracted and set for new PRs as well. The 'gh pr create' command returns a URL, so you'd need to parse it or use 'gh pr list' afterward to get the number.

Suggested change
--head "$BRANCH")
echo "Created PR: $URL"
echo "PR_URL=$URL" >> $GITHUB_OUTPUT
--head "$BRANCH")
NUM="${URL##*/}"
echo "Created PR: $URL"
echo "PR_URL=$URL" >> $GITHUB_OUTPUT
echo "PR_NUMBER=$NUM" >> $GITHUB_OUTPUT

Copilot uses AI. Check for mistakes.
fi

- name: Comment back on Jira with PR link
env:
JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }}
JIRA_EMAIL: ${{ secrets.JIRA_EMAIL }}
JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }}
JIRA_KEY: ${{ steps.jira.outputs.JIRA_KEY }}
PR_URL: ${{ steps.pr.outputs.PR_URL }}
run: |
set -euo pipefail
if [ -z "$PR_URL" ] || [ "$PR_URL" = "null" ]; then
echo "No PR URL found; skipping Jira comment."
exit 0
fi

payload=$(jq -n --arg url "$PR_URL" '{
body: {
type: "doc",
version: 1,
content: [
{
type: "paragraph",
content: [
{type: "text", text: "PR created/updated: "},
{type: "text", text: $url, marks: [{type: "link", attrs: {href: $url}}]}
]
}
]
}
}')

curl -fsS --retry 3 --retry-all-errors -u "$JIRA_EMAIL:$JIRA_API_TOKEN" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-X POST \
--data "$payload" \
"$JIRA_BASE_URL/rest/api/3/issue/$JIRA_KEY/comment" > /dev/null
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

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

If the Jira comment API call fails (line 387-392), the workflow will fail due to 'set -euo pipefail' on line 365. However, posting a comment back to Jira is not critical to the workflow's main purpose (creating/updating the PR). Consider adding '|| true' to the curl command or wrapping it in a conditional to make this step non-blocking, so that PR creation succeeds even if Jira commenting fails. This would make the workflow more resilient to Jira API issues.

Suggested change
"$JIRA_BASE_URL/rest/api/3/issue/$JIRA_KEY/comment" > /dev/null
"$JIRA_BASE_URL/rest/api/3/issue/$JIRA_KEY/comment" > /dev/null || {
echo "Warning: Failed to post comment to Jira; continuing without failing workflow." >&2
}

Copilot uses AI. Check for mistakes.
Loading