Skip to content

feat(jira): add fullsend jira enroll command for Jira project enrollment - #3207

Closed
manish-jangra wants to merge 1 commit into
fullsend-ai:mainfrom
manish-jangra:fullsend-jira-enroll
Closed

manish-jangra wants to merge 1 commit into
fullsend-ai:mainfrom
manish-jangra:fullsend-jira-enroll

Conversation

@manish-jangra

Copy link
Copy Markdown

Summary

Adds a new fullsend jira enroll <owner/repo> CLI command that automates connecting a Jira Cloud project to a GitHub repository's fullsend triage pipeline.

Agents: Uses the existing triage agent — no new agents are created. Jira source support is handled via the customization layer (.fullsend/customized/).

Scope: Triage only. Prioritize and code agents are deferred — prioritize lacks Jira-aware prompt/scripts, and code requires a repo-selection mechanism for the 1-Jira-project-to-N-repos pattern common in enterprise teams.

What the command does

fullsend jira enroll <owner/repo> --jira-host <host> --project-key <KEY>
  1. Validates the Jira project is accessible (GET /rest/api/3/project/{key})
  2. Fetches the Jira Cloud ID and current user account ID
  3. Creates/updates .jira.yml enrollment config in the GitHub repo
  4. Creates Jira Automation rules via the public Automation API (2 rules: auto-triage on issue creation + /fs-triage slash command)
  5. Commits jira-dispatch.yml and jira-triage.yml workflow files to the repo
  6. Sets JIRA_HOST, JIRA_EMAIL, JIRA_API_TOKEN as GitHub repo secrets

Automation API limitation (AUTO-2120)

The Jira Automation Rule Management API requires site admin for write operations, even though project admins can create rules via the Jira UI. When the API returns 403, the CLI gracefully falls back to printing pre-filled manual instructions with the exact rule configuration (webhook URLs, headers, payloads) so the user can create them in the Jira UI at Space settings > Automation > Create flow.

Files changed

File Purpose
internal/jira/config.go .jira.yml parsing — JiraConfig, AddProject (idempotent), HasProject
internal/jira/types.go Jira Automation REST API types (CreateRuleRequest, RulePayload, RuleComponent, RuleActor)
internal/jira/client.go Jira API client — GetCloudID, ValidateProject, GetCurrentUser, CreateAutomationRule, ListAutomationRules, RuleExistsByName
internal/jira/rules.go Automation rule builders — AutoTriageRule, SlashCommandRule, ManualRuleInstructions. Component types reverse-engineered from existing rules
internal/jira/rules_test.go 20 tests covering rule structure, JSON roundtrip, parameterization
internal/jira/client_test.go 22 tests with httptest mocks — success paths, error codes, malformed JSON
internal/jira/config_test.go 7 tests — parsing, idempotency, round-trip
internal/cli/jira.go fullsend jira enroll command — flags, credential resolution, execution flow, 403 fallback
internal/cli/root.go Register newJiraCmd()
internal/scaffold/scaffold.go JiraDispatchTemplate(), JiraTriageTemplate() helpers
internal/scaffold/fullsend-repo/templates/jira-dispatch.yml Dispatch workflow — validates enrollment, routes to triage
internal/scaffold/fullsend-repo/templates/jira-triage.yml Triage workflow — layers customizations, runs fullsend run triage with TRIAGE_SOURCE=jira

Design decisions

  • Follows fullsend github pattern — fullsend jira is a parallel command group with the same cobra structure, ui.Printer output, and credential resolution cascade (flag → env → prompt)
  • Public Automation API (api.atlassian.com/automation/public/jira/{cloudId}/rest/v1/rule) with Basic auth — confirmed by Atlassian support as the only supported auth method (OAuth is explicitly blocked)
  • Component types hardcoded — jira.issue.event.trigger:created, jira.issue.event.trigger:commented, jira.comparator.condition, jira.issue.outgoing.webhook — discovered by reverse-engineering existing rules (Atlassian does not document these)
  • Idempotent — .jira.yml skips existing projects, rule creation skips rules with matching names, CreateOrUpdateFile for config, CreateRepoSecret overwrites

Test plan

  • go test ./internal/jira/... — 49 tests pass, 93.5% coverage
  • go build ./cmd/fullsend/ — clean build
  • fullsend jira enroll --help — displays correct usage and flags
  • Input validation — rejects invalid project keys, URLs as hostnames, missing credentials
  • --dry-run — previews all steps without making changes
  • --skip-workflows — tested against live stage-redhat.atlassian.net with KFLUXINFRA project
  • 403 fallback — prints pre-filled manual instructions when Automation API returns 403
  • Pre-commit hooks — gofmt, go vet, actionlint, gitleaks, shellcheck, lychee all pass

Out of scope (future work)

  • Prioritize/code agent Jira support (needs Jira-aware prompts, pre/post scripts, repo-selection)
  • fullsend jira unenroll / fullsend jira status subcommands
  • Jira OAuth 2.0 / Forge app auth (currently static API token)
  • Per-org Jira enrollment

🤖 Generated with Claude Code

…lment

Add a new `fullsend jira enroll <owner/repo>` CLI command that automates
connecting a Jira Cloud project to a GitHub repository's fullsend
triage pipeline.

The command performs the following steps:
- Validates the Jira project is accessible (GET /rest/api/3/project)
- Fetches the Jira Cloud ID and current user account ID
- Creates or updates .jira.yml enrollment config in the GitHub repo
- Attempts to create Jira Automation rules via the public API
- Commits jira-dispatch.yml and jira-triage.yml workflow files
- Sets JIRA_HOST, JIRA_EMAIL, JIRA_API_TOKEN as GitHub repo secrets

The Jira Automation Rule Management API currently requires site admin
permissions for write operations (https://jira.atlassian.com/browse/AUTO-2120).
When the API returns 403, the CLI gracefully falls back to printing
pre-filled manual instructions for creating the rules in the Jira UI.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@manish-jangra
manish-jangra requested a review from a team July 7, 2026 06:32
@github-actions github-actions Bot closed this Jul 7, 2026
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

Thank you for your interest in contributing to fullsend, @manish-jangra.

This project uses a vouch system for first-time contributors. Before submitting a pull request, you need to be vouched by a maintainer.

To get vouched:

  1. Open a Vouch Request discussion.
  2. Describe what you want to change and why.
  3. Write in your own words — do not have an AI generate the request.
  4. A maintainer will comment /vouch if approved.
  5. Once vouched, open a new PR (preferred) or reopen this one.

See CONTRIBUTING.md for details.

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

E2E tests did not run

E2E tests run automatically for org/repo members and collaborators on pull requests.

For other contributors, a maintainer must add the ok-to-test label after the latest push.

See E2E testing guide for details.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Add fullsend jira enroll command to enroll Jira projects for triage

✨ Enhancement 🧪 Tests ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Add fullsend jira enroll to connect a Jira Cloud project to a GitHub repo.
• Create/idempotently manage .jira.yml, automation rules, workflows, and repo secrets.
• Add Jira Automation API client + rule builders with extensive unit/httptest coverage.
Diagram

graph TD
  A["fullsend CLI: jira enroll"] --> B["Jira API Client"] --> C["Jira Cloud REST APIs"]
  A --> D["Rule Builders"] --> B
  A --> E["GitHub Forge Client"] --> F["Target GitHub Repo"]
  A --> G["Scaffold Templates"] --> F
  F --> H["GitHub Actions Workflows"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Skip Automation API; always output manual Jira UI instructions
  • ➕ Avoids AUTO-2120 site-admin limitation and brittle internal component identifiers
  • ➕ Fewer moving parts; less risk of API drift breaking enroll
  • ➖ Significantly worse UX; manual setup becomes the default path
  • ➖ Harder to guarantee consistent rule configuration across repos/projects
2. Use a GitHub App + shared webhook endpoint (no Jira Automation rules)
  • ➕ Centralized webhook auth/rotation; avoids storing PAT-like credentials in Jira rule config
  • ➕ Can support 1 Jira project → N repos more naturally via server-side routing
  • ➖ Requires hosting/operating an external service and app installation flow
  • ➖ More complex rollout and permissions model than a CLI-only approach
3. Atlassian app auth (OAuth/Forge/Connect) instead of API token Basic auth
  • ➕ More enterprise-friendly auth and revocation story
  • ➕ Potentially richer Jira integration surface
  • ➖ Not feasible for the Automation public API (OAuth blocked per Atlassian guidance)
  • ➖ Considerably larger product scope (app distribution, consent, tenancy)

Recommendation: Keep the current CLI-first approach with Automation API best-effort + 403 manual fallback. Given AUTO-2120 and undocumented component identifiers, the fallback path is essential; the current implementation strikes a pragmatic balance between automation and reliability without introducing a hosted service.

Files changed (12) +2310 / -0

Enhancement (7) +986 / -0
jira.goAdd 'fullsend jira enroll' command and enrollment orchestration +378/-0

Add 'fullsend jira enroll' command and enrollment orchestration

• Introduces a new 'jira' Cobra command group with an 'enroll' subcommand. Implements input validation, credential resolution, dry-run behavior, idempotent '.jira.yml' updates, Automation rule creation with 403 fallback instructions, workflow commits, and GitHub secret configuration.

internal/cli/jira.go

root.goRegister Jira command group in root CLI +1/-0

Register Jira command group in root CLI

• Adds 'newJiraCmd()' to the root command so 'fullsend jira ...' is available.

internal/cli/root.go

client.goImplement Jira Cloud + Automation API client +222/-0

Implement Jira Cloud + Automation API client

• Adds a Basic-auth HTTP client for Jira Cloud tenant info, project validation, and current-user lookup. Implements Automation public API operations to create rules and list rule summaries for idempotency checks.

internal/jira/client.go

config.goAdd '.jira.yml' config model with idempotent enrollment helpers +68/-0

Add '.jira.yml' config model with idempotent enrollment helpers

• Defines 'JiraConfig' / 'JiraProjectConfig' structures and YAML parse/marshal helpers. Adds 'HasProject' and 'AddProject' to ensure enrollment updates are idempotent and versioned.

internal/jira/config.go

rules.goAdd Jira Automation rule builders and manual fallback instructions +224/-0

Add Jira Automation rule builders and manual fallback instructions

• Implements constructors for two Automation rules: auto-triage on issue creation and a configurable slash-command rule that triggers on comment content. Hardcodes reverse-engineered component type identifiers and provides printable manual setup instructions for 403 API failures.

internal/jira/rules.go

types.goDefine Jira Automation API request/response payload types +83/-0

Define Jira Automation API request/response payload types

• Adds Go structs for tenant info, rule creation payloads, rule components, rule actor, connections, and rule summary responses used for idempotency checks.

internal/jira/types.go

scaffold.goExpose scaffold helpers for Jira workflow templates +10/-0

Expose scaffold helpers for Jira workflow templates

• Adds 'JiraDispatchTemplate()' and 'JiraTriageTemplate()' to load the embedded Jira workflow YAML templates for enrollment-time commits.

internal/scaffold/scaffold.go

Tests (3) +1085 / -0
client_test.goAdd httptest coverage for Jira client behavior and error handling +554/-0

Add httptest coverage for Jira client behavior and error handling

• Provides extensive tests using 'httptest.Server', including transport rewriting to handle hardcoded 'https://' URLs. Covers happy paths, common HTTP error codes, malformed JSON responses, and header formatting expectations.

internal/jira/client_test.go

config_test.goTest '.jira.yml' parsing, defaults, idempotency, and round-trip marshaling +137/-0

Test '.jira.yml' parsing, defaults, idempotency, and round-trip marshaling

• Adds tests for YAML parsing (including default version behavior), invalid YAML handling, 'HasProject' matching logic, and 'Marshal'→'Parse' round-trip correctness.

internal/jira/config_test.go

rules_test.goValidate rule payload structure, JSON integrity, and instructions output +394/-0

Validate rule payload structure, JSON integrity, and instructions output

• Adds unit tests asserting rule names, trigger/action/condition component wiring, request payload JSON validity, and manual instruction content. Verifies header construction and parameterization across multiple slash commands.

internal/jira/rules_test.go

Other (2) +239 / -0
jira-dispatch.ymlAdd Jira dispatch workflow to route repository_dispatch events to triage +65/-0

Add Jira dispatch workflow to route repository_dispatch events to triage

• Introduces a GitHub Actions workflow that listens for Jira-originated 'repository_dispatch' events. Validates '.jira.yml' enrollment and triggers 'jira-triage.yml' with extracted issue/project inputs.

internal/scaffold/fullsend-repo/templates/jira-dispatch.yml

jira-triage.ymlAdd Jira triage workflow to run triage agent with Jira source context +174/-0

Add Jira triage workflow to run triage agent with Jira source context

• Introduces a 'workflow_dispatch' triage workflow that layers upstream defaults with repo customizations. Extracts Jira host and linked repos from '.jira.yml', sets up environment/credentials, and runs the existing triage agent with 'TRIAGE_SOURCE=jira' and Jira secrets.

internal/scaffold/fullsend-repo/templates/jira-triage.yml

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (4) 📘 Rule violations (2) 📜 Skill insights (0)

Context used
⚠️ Tickets: not configured — ticket URL found in PR but could not be fetched — check ticket provider credentials
✅ Compliance rules (platform): 61 rules

Grey Divider


Action required

1. Swallowed .jira.yml read errors 🐞 Bug ≡ Correctness
Description
loadOrCreateJiraConfig treats any GetFileContent error as “file missing” and returns an empty
config, which can cause the command to overwrite an existing .jira.yml after transient/permission
errors and drop previously enrolled projects. This differs from the existing CLI pattern of only
falling back on forge.IsNotFound(err) and propagating other errors.
Code

internal/cli/jira.go[R371-376]

+func loadOrCreateJiraConfig(ctx context.Context, client forge.Client, owner, repo string) (*jira.JiraConfig, error) {
+	data, err := client.GetFileContent(ctx, owner, repo, ".jira.yml")
+	if err != nil {
+		// File does not exist — start with empty config.
+		return jira.NewJiraConfig(), nil
+	}
Relevance

⭐⭐⭐ High

Repo prefers propagating non-EOF/non-NotFound errors; similar “don’t swallow read errors” fix
accepted in #383.

PR-#383

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new helper unconditionally converts any GetFileContent error into a new empty config, while
existing CLI code explicitly checks forge.IsNotFound(err) and returns other errors, showing the
intended error-handling contract for forge reads.

internal/cli/jira.go[369-378]
internal/cli/github.go[970-983]
internal/forge/forge.go[19-25]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`loadOrCreateJiraConfig()` currently assumes any error reading `.jira.yml` means the file doesn’t exist, returning a new empty config. If the read failed for a non-404 reason (rate limit, auth, network, repo permissions), subsequent writes can clobber the real `.jira.yml` and lose enrolled projects.

### Issue Context
Other CLI code paths already distinguish not-found from other errors using `forge.IsNotFound(err)`.

### Fix Focus Areas
- internal/cli/jira.go[369-378]

### Suggested fix
- Change `loadOrCreateJiraConfig` to:
 - `if err != nil { if forge.IsNotFound(err) { return jira.NewJiraConfig(), nil }; return nil, fmt.Errorf("reading .jira.yml: %w", err) }`
- (Optional) Add a small unit test around this helper using `forge.FakeClient` configured to return a non-not-found error and assert the error propagates.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Empty GITHUB_ISSUE_URL breaks triage 🐞 Bug ≡ Correctness
Description
jira-triage.yml forces GITHUB_ISSUE_URL to an empty string, but the default triage harness runs
scripts/pre-triage.sh/scripts/post-triage.sh, which validate GITHUB_ISSUE_URL as a GitHub
issue URL and exit non-zero if it’s missing/invalid. This means the newly scaffolded Jira triage
workflow will fail unless the repo provides Jira-specific harness/script overrides.
Code

internal/scaffold/fullsend-repo/templates/jira-triage.yml[R156-164]

+      - name: Run triage agent
+        uses: ./.defaults/
+        env:
+          TRIAGE_SOURCE: jira
+          GITHUB_ISSUE_URL: ""
+          LINKED_GITHUB_REPOS: ${{ steps.jira-config.outputs.linked_repos }}
+          JIRA_EMAIL: ${{ secrets.JIRA_EMAIL }}
+          JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }}
+        with:
Relevance

⭐⭐⭐ High

Workflow input validation for GITHUB_ISSUE_URL is enforced in scaffold scripts; empty value would
break runs (see #286/#279).

PR-#286
PR-#279

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The workflow explicitly sets GITHUB_ISSUE_URL to empty. The default triage harness uses pre/post
scripts that require GITHUB_ISSUE_URL to match a GitHub issues URL regex and exit 1 when it does
not, so these templates are incompatible without overrides.

internal/scaffold/fullsend-repo/templates/jira-triage.yml[145-164]
internal/scaffold/fullsend-repo/harness/triage.yaml[20-45]
internal/scaffold/fullsend-repo/scripts/pre-triage.sh[18-23]
internal/scaffold/fullsend-repo/scripts/post-triage.sh[47-52]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The scaffolded `jira-triage.yml` sets `GITHUB_ISSUE_URL: ""`, but the default triage harness uses GitHub-issue-centric pre/post scripts that hard-fail when `GITHUB_ISSUE_URL` isn’t a GitHub issue URL. As a result, Jira triage will break out-of-the-box.

### Issue Context
`internal/scaffold/fullsend-repo/harness/triage.yaml` wires `pre_script`/`post_script` to `scripts/pre-triage.sh` and `scripts/post-triage.sh`, and both scripts enforce a GitHub issue URL regex.

### Fix Focus Areas
- internal/scaffold/fullsend-repo/templates/jira-triage.yml[79-168]
- internal/cli/jira.go[43-310] (optional: preflight validation before committing workflows)

### Suggested fix (choose one approach)
**A) Fail fast with a clear message (minimal + matches PR prerequisite):**
- Add a step in `jira-triage.yml` before “Run triage agent” that verifies required Jira overrides exist (e.g. `.fullsend/customized/harness/triage.yaml` and/or Jira-specific `scripts/pre-triage.sh` + `scripts/post-triage.sh`). If missing, emit `::error::` explaining that Jira triage requires customization overrides and exit 1.
- Remove `GITHUB_ISSUE_URL: ""` from the workflow env to avoid forcing an invalid value; let the customized harness/scripts decide what they need.

**B) Make defaults Jira-aware (more invasive):**
- Update `scripts/pre-triage.sh` and `scripts/post-triage.sh` to branch on `TRIAGE_SOURCE==jira` (or similar) and skip GitHub label mutations / GitHub URL validation in Jira mode, using `ISSUE_KEY` instead.

**C) CLI preflight (recommended in addition to A or B):**
- In `fullsend jira enroll`, before committing workflows, check the repo contains the required `.fullsend/customized/...` overrides; otherwise return a user-facing error telling them what to add.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. webhookURL() outside forge/github 📘 Rule violation ⌂ Architecture
Description
internal/jira/rules.go constructs GitHub API URLs/headers (api.github.com, GitHub Accept header)
outside internal/forge/github, violating the rule that GitHub-specific API details must be
confined to that package. This increases the chance of inconsistent GitHub API behavior and bypasses
the intended abstraction boundary.
Code

internal/jira/rules.go[R41-50]

+func webhookURL(owner, repo string) string {
+	return "https://api.github.com/repos/" + owner + "/" + repo + "/dispatches"
+}
+
+func webhookHeaders(githubPAT string) json.RawMessage {
+	now := time.Now().UnixMilli()
+	headers := []map[string]interface{}{
+		{"id": fmt.Sprintf("_header_%d", now), "name": "Authorization", "value": "Bearer " + githubPAT, "headerSecure": false},
+		{"id": fmt.Sprintf("_header_%d", now+1), "name": "Accept", "value": "application/vnd.github.v3+json", "headerSecure": false},
+		{"id": fmt.Sprintf("_header_%d", now+2), "name": "Content-Type", "value": "application/json", "headerSecure": false},
Relevance

⭐⭐ Medium

Mixed enforcement: prior request to avoid hardcoded api.github.com behavior was rejected (#1333); no
clear boundary precedent found.

PR-#1333

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The compliance rule requires GitHub API URLs/headers to be constructed only within
internal/forge/github/. The PR adds webhookURL() and webhookHeaders() in
internal/jira/rules.go, which hardcode https://api.github.com/... and GitHub-specific Accept
headers outside the allowed package.

Rule 1062054: Restrict direct GitHub API calls to internal/forge/github
internal/jira/rules.go[41-50]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`internal/jira/rules.go` hardcodes GitHub API URL construction and GitHub-specific headers (`api.github.com`, `application/vnd.github.v3+json`) outside `internal/forge/github/`, which violates the rule restricting direct GitHub API details to that package.

## Issue Context
The Jira automation rule builder needs a webhook URL and headers for GitHub's repository dispatch endpoint, but the GitHub-specific URL/header construction must live under `internal/forge/github/` and be consumed via a helper/abstraction.

## Fix Focus Areas
- internal/jira/rules.go[41-50]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

4. Dry-run unnecessarily requires PAT 🐞 Bug ☼ Reliability
Description
runJiraEnroll errors if FULLSEND_JIRA_GITHUB_PAT is missing whenever --skip-automation-rules
is false, even when --dry-run is enabled and the code will never call the Automation API. This
prevents “preview only” runs without supplying a PAT that won’t be used.
Code

internal/cli/jira.go[R116-119]

+	githubPAT := resolveEnvOrFlag(cfg.githubPAT, "FULLSEND_JIRA_GITHUB_PAT")
+	if githubPAT == "" && !cfg.skipAutomationRules {
+		return fmt.Errorf("GitHub PAT is required for automation rules: use --github-pat or set FULLSEND_JIRA_GITHUB_PAT")
+	}
Relevance

⭐⭐⭐ High

Dry-run bypass/guard changes are commonly accepted in CLI flows (e.g., added dry-run guard accepted
in #2370).

PR-#2370

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The PAT check is unconditional with respect to dryRun, while the automation rule loop clearly
bypasses rule creation when cfg.dryRun is true.

internal/cli/jira.go[106-120]
internal/cli/jira.go[234-242]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The CLI requires a GitHub PAT for automation rules even in `--dry-run` mode, despite the dry-run path not creating rules. This makes dry-run less useful as a low-friction preview.

### Issue Context
The PAT requirement is enforced before the dry-run short-circuit in the rule creation loop.

### Fix Focus Areas
- internal/cli/jira.go[116-120]
- internal/cli/jira.go[234-242]

### Suggested fix
- Gate the PAT requirement on actually creating rules:
 - `if githubPAT == "" && !cfg.skipAutomationRules && !cfg.dryRun { ... }`
- (Optional) Consider similarly relaxing Jira credential requirements under `--dry-run` if no Jira API calls will be made in that mode.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Undeclared PyYAML workflow dependency 🐞 Bug ☼ Reliability
Description
Both Jira workflow templates parse .jira.yml via python3 -c 'import yaml' but don’t install
PyYAML or otherwise ensure the yaml module is present, creating an undeclared runner dependency
that can break enrollment validation/config extraction. This makes the scaffolds less portable
across runner images.
Code

internal/scaffold/fullsend-repo/templates/jira-dispatch.yml[R41-49]

+          ENROLLED=$(PROJECT_KEY="${PROJECT_KEY}" python3 -c "
+          import os, yaml
+          key = os.environ['PROJECT_KEY']
+          with open('.jira.yml') as f:
+              cfg = yaml.safe_load(f)
+          projects = cfg.get('jira_projects', [])
+          keys = [p.get('project_key','') for p in projects]
+          print('true' if key in keys else 'false')
+          " 2>/dev/null || echo "false")
Relevance

⭐⭐ Medium

No clear precedent for PyYAML install in workflow templates; related Python-dependency issues show
mixed outcomes (#441 vs rejected #279).

PR-#441
PR-#279
PR-#1055

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The templates use import os, yaml inline to read .jira.yml in multiple steps, and there is no
corresponding dependency installation step in the workflow templates themselves.

internal/scaffold/fullsend-repo/templates/jira-dispatch.yml[36-53]
internal/scaffold/fullsend-repo/templates/jira-triage.yml[85-134]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The workflow templates rely on `python3` having the `yaml` module available (`import os, yaml`) to parse `.jira.yml`, but the templates don’t install PyYAML. This is an undeclared dependency and can cause runtime failures on environments where PyYAML isn’t preinstalled.

### Issue Context
The Jira dispatch and triage templates both use inline Python with `yaml.safe_load`.

### Fix Focus Areas
- internal/scaffold/fullsend-repo/templates/jira-dispatch.yml[21-57]
- internal/scaffold/fullsend-repo/templates/jira-triage.yml[79-135]

### Suggested fix
- Add an explicit setup step before the first `python3 -c` invocation:
 - `python3 -m pip install --user --quiet pyyaml`
- Or replace the Python YAML parsing with a tool already expected by the repo (e.g., `yq`) and include a presence check + install step if needed.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

6. runJiraEnroll lacks unit tests 📘 Rule violation ▣ Testability
Description
A substantial new CLI command implementation was added in internal/cli/jira.go without adding or
modifying corresponding unit tests in this PR. This violates the requirement to include tests for
new or modified Go logic and increases regression risk for enrollment behavior.
Code

internal/cli/jira.go[R43-214]

+func newJiraEnrollCmd() *cobra.Command {
+	var cfg jiraEnrollConfig
+
+	cmd := &cobra.Command{
+		Use:   "enroll <owner/repo>",
+		Short: "Enroll a Jira project for fullsend triage",
+		Long: `Connects a Jira Cloud project to a GitHub repository's fullsend pipeline.
+
+This command:
+  1. Creates or updates .jira.yml in the GitHub repo
+  2. Creates Jira Automation rules for triage dispatch
+  3. Commits dispatch and triage workflow files
+  4. Sets Jira credential secrets on the GitHub repo
+
+Prerequisites:
+  - The GitHub repo must have fullsend installed (per-repo mode)
+  - Jira agent customization files must exist in .fullsend/customized/
+  - A Jira API token (https://id.atlassian.com/manage-profile/security/api-tokens)
+  - A GitHub fine-grained PAT with Contents: Read and write on the target repo`,
+		Args: cobra.ExactArgs(1),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			owner, repo, isRepo := parseTarget(args[0])
+			if !isRepo {
+				return fmt.Errorf("argument must be owner/repo (e.g., myorg/myrepo)")
+			}
+			return runJiraEnroll(cmd.Context(), owner, repo, cfg)
+		},
+	}
+
+	cmd.Flags().StringVar(&cfg.jiraHost, "jira-host", "", "Jira Cloud hostname (e.g., myorg.atlassian.net)")
+	cmd.Flags().StringVar(&cfg.projectKey, "project-key", "", "Jira project key (e.g., MYPROJ)")
+	cmd.Flags().StringVar(&cfg.jiraEmail, "jira-email", "", "Jira user email (env: JIRA_EMAIL)")
+	cmd.Flags().StringVar(&cfg.jiraAPIToken, "jira-api-token", "", "Jira API token (env: JIRA_API_TOKEN)")
+	cmd.Flags().StringVar(&cfg.githubPAT, "github-pat", "", "GitHub PAT for Jira webhook auth (env: FULLSEND_JIRA_GITHUB_PAT)")
+	cmd.Flags().StringVar(&cfg.linkedRepos, "linked-repos", "", "comma-separated org/repo values for cross-repo search")
+	cmd.Flags().BoolVar(&cfg.skipAutomationRules, "skip-automation-rules", false, "skip creating Jira Automation rules")
+	cmd.Flags().BoolVar(&cfg.skipWorkflows, "skip-workflows", false, "skip committing GitHub workflow files (use when workflows already exist in the repo)")
+	cmd.Flags().BoolVar(&cfg.dryRun, "dry-run", false, "preview changes without executing")
+
+	_ = cmd.MarkFlagRequired("jira-host")
+	_ = cmd.MarkFlagRequired("project-key")
+
+	return cmd
+}
+
+func runJiraEnroll(ctx context.Context, owner, repo string, cfg jiraEnrollConfig) error {
+	printer := ui.New(os.Stdout)
+	printer.Banner(version)
+
+	// --- Validate inputs ---
+
+	if !jiraProjectKeyPattern.MatchString(cfg.projectKey) {
+		return fmt.Errorf("invalid project key %q: must match %s", cfg.projectKey, jiraProjectKeyPattern.String())
+	}
+	if cfg.jiraHost == "" {
+		return fmt.Errorf("--jira-host is required")
+	}
+	if strings.HasPrefix(cfg.jiraHost, "https://") || strings.HasPrefix(cfg.jiraHost, "http://") {
+		return fmt.Errorf("--jira-host should be a hostname (e.g., myorg.atlassian.net), not a URL")
+	}
+
+	// --- Resolve credentials ---
+
+	jiraEmail := resolveEnvOrFlag(cfg.jiraEmail, "JIRA_EMAIL")
+	if jiraEmail == "" {
+		return fmt.Errorf("Jira email is required: use --jira-email or set JIRA_EMAIL")
+	}
+
+	jiraAPIToken := resolveEnvOrFlag(cfg.jiraAPIToken, "JIRA_API_TOKEN")
+	if jiraAPIToken == "" {
+		return fmt.Errorf("Jira API token is required: use --jira-api-token or set JIRA_API_TOKEN")
+	}
+
+	githubPAT := resolveEnvOrFlag(cfg.githubPAT, "FULLSEND_JIRA_GITHUB_PAT")
+	if githubPAT == "" && !cfg.skipAutomationRules {
+		return fmt.Errorf("GitHub PAT is required for automation rules: use --github-pat or set FULLSEND_JIRA_GITHUB_PAT")
+	}
+
+	ghToken, err := resolveToken()
+	if err != nil {
+		return fmt.Errorf("resolving GitHub token: %w", err)
+	}
+	ghClient := gh.New(ghToken)
+
+	printer.Header(fmt.Sprintf("Enrolling Jira project %s on %s → %s/%s", cfg.projectKey, cfg.jiraHost, owner, repo))
+
+	if cfg.dryRun {
+		printer.StepInfo("DRY RUN — no changes will be made")
+	}
+
+	// --- Validate Jira project ---
+
+	printer.StepStart("Validating Jira project")
+	jiraClient := jira.NewClient(jiraEmail, jiraAPIToken)
+
+	var projectID string
+	if !cfg.dryRun {
+		projectInfo, err := jiraClient.ValidateProject(ctx, cfg.jiraHost, cfg.projectKey)
+		if err != nil {
+			printer.StepFail("Jira project validation failed")
+			return err
+		}
+		projectID = projectInfo.ID
+	} else {
+		projectID = "<dry-run>"
+	}
+	printer.StepDone(fmt.Sprintf("Jira project %s is accessible (ID: %s)", cfg.projectKey, projectID))
+
+	// --- Get Cloud ID and Account ID ---
+
+	var cloudID, accountID string
+	if !cfg.skipAutomationRules {
+		printer.StepStart("Fetching Jira Cloud ID and account info")
+		if !cfg.dryRun {
+			cloudID, err = jiraClient.GetCloudID(ctx, cfg.jiraHost)
+			if err != nil {
+				printer.StepFail("Failed to get Cloud ID")
+				return err
+			}
+			accountID, err = jiraClient.GetCurrentUser(ctx, cfg.jiraHost)
+			if err != nil {
+				printer.StepFail("Failed to get account ID")
+				return err
+			}
+		} else {
+			cloudID = "<dry-run>"
+			accountID = "<dry-run>"
+		}
+		printer.StepDone(fmt.Sprintf("Cloud ID: %s", cloudID))
+	}
+
+	// --- Create/update .jira.yml ---
+
+	printer.StepStart("Updating .jira.yml")
+
+	var linkedRepos []string
+	if cfg.linkedRepos != "" {
+		linkedRepos = strings.Split(cfg.linkedRepos, ",")
+		for i := range linkedRepos {
+			linkedRepos[i] = strings.TrimSpace(linkedRepos[i])
+		}
+	}
+
+	jiraCfg, err := loadOrCreateJiraConfig(ctx, ghClient, owner, repo)
+	if err != nil {
+		printer.StepFail("Failed to load .jira.yml")
+		return err
+	}
+
+	project := jira.JiraProjectConfig{
+		ProjectKey:        cfg.projectKey,
+		Host:              cfg.jiraHost,
+		LinkedGitHubRepos: linkedRepos,
+	}
+
+	added := jiraCfg.AddProject(project)
+	if !added {
+		printer.StepDone(fmt.Sprintf("Project %s already enrolled in .jira.yml", cfg.projectKey))
+	} else if cfg.dryRun {
+		printer.StepDone(fmt.Sprintf("Would add %s to .jira.yml", cfg.projectKey))
+	} else {
+		data, err := jiraCfg.Marshal()
+		if err != nil {
+			return fmt.Errorf("marshaling .jira.yml: %w", err)
+		}
+		if err := ghClient.CreateOrUpdateFile(ctx, owner, repo, ".jira.yml",
+			"chore: enroll Jira project "+cfg.projectKey, data); err != nil {
+			printer.StepFail("Failed to write .jira.yml")
+			return fmt.Errorf("writing .jira.yml: %w", err)
+		}
+		printer.StepDone(fmt.Sprintf("Added %s to .jira.yml", cfg.projectKey))
+	}
Relevance

⭐ Low

Team often rejects adding unit tests for new CLI branches (e.g., test-coverage asks rejected in
#2860, #372).

PR-#2860
PR-#372
PR-#1627

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The compliance rule requires adding/updating tests alongside new Go logic. This PR adds the new Jira
enroll command implementation in internal/cli/jira.go (including newJiraEnrollCmd() and
runJiraEnroll()), but no corresponding test additions/updates are present in the PR diff for this
new logic.

Rule 1062049: Require tests for new or modified Go logic
internal/cli/jira.go[43-214]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The PR introduces new Go logic for the `fullsend jira enroll` command but does not add/modify any `_test.go` coverage for this logic in the same change.

## Issue Context
Rule requires tests for new/modified Go logic. `internal/cli/jira.go` includes input validation, credential resolution, `.jira.yml` updates, workflow commits, and secret setting logic that should be covered by unit tests (e.g., dry-run behavior, validation failures, and expected forge calls).

## Fix Focus Areas
- internal/cli/jira.go[43-214]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread internal/jira/rules.go
Comment on lines +41 to +50
func webhookURL(owner, repo string) string {
return "https://api.github.com/repos/" + owner + "/" + repo + "/dispatches"
}

func webhookHeaders(githubPAT string) json.RawMessage {
now := time.Now().UnixMilli()
headers := []map[string]interface{}{
{"id": fmt.Sprintf("_header_%d", now), "name": "Authorization", "value": "Bearer " + githubPAT, "headerSecure": false},
{"id": fmt.Sprintf("_header_%d", now+1), "name": "Accept", "value": "application/vnd.github.v3+json", "headerSecure": false},
{"id": fmt.Sprintf("_header_%d", now+2), "name": "Content-Type", "value": "application/json", "headerSecure": false},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. webhookurl() outside forge/github 📘 Rule violation ⌂ Architecture

internal/jira/rules.go constructs GitHub API URLs/headers (api.github.com, GitHub Accept header)
outside internal/forge/github, violating the rule that GitHub-specific API details must be
confined to that package. This increases the chance of inconsistent GitHub API behavior and bypasses
the intended abstraction boundary.
Agent Prompt
## Issue description
`internal/jira/rules.go` hardcodes GitHub API URL construction and GitHub-specific headers (`api.github.com`, `application/vnd.github.v3+json`) outside `internal/forge/github/`, which violates the rule restricting direct GitHub API details to that package.

## Issue Context
The Jira automation rule builder needs a webhook URL and headers for GitHub's repository dispatch endpoint, but the GitHub-specific URL/header construction must live under `internal/forge/github/` and be consumed via a helper/abstraction.

## Fix Focus Areas
- internal/jira/rules.go[41-50]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread internal/cli/jira.go
Comment on lines +371 to +376
func loadOrCreateJiraConfig(ctx context.Context, client forge.Client, owner, repo string) (*jira.JiraConfig, error) {
data, err := client.GetFileContent(ctx, owner, repo, ".jira.yml")
if err != nil {
// File does not exist — start with empty config.
return jira.NewJiraConfig(), nil
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

3. Swallowed .jira.yml read errors 🐞 Bug ≡ Correctness

loadOrCreateJiraConfig treats any GetFileContent error as “file missing” and returns an empty
config, which can cause the command to overwrite an existing .jira.yml after transient/permission
errors and drop previously enrolled projects. This differs from the existing CLI pattern of only
falling back on forge.IsNotFound(err) and propagating other errors.
Agent Prompt
### Issue description
`loadOrCreateJiraConfig()` currently assumes any error reading `.jira.yml` means the file doesn’t exist, returning a new empty config. If the read failed for a non-404 reason (rate limit, auth, network, repo permissions), subsequent writes can clobber the real `.jira.yml` and lose enrolled projects.

### Issue Context
Other CLI code paths already distinguish not-found from other errors using `forge.IsNotFound(err)`.

### Fix Focus Areas
- internal/cli/jira.go[369-378]

### Suggested fix
- Change `loadOrCreateJiraConfig` to:
  - `if err != nil { if forge.IsNotFound(err) { return jira.NewJiraConfig(), nil }; return nil, fmt.Errorf("reading .jira.yml: %w", err) }`
- (Optional) Add a small unit test around this helper using `forge.FakeClient` configured to return a non-not-found error and assert the error propagates.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +156 to +164
- name: Run triage agent
uses: ./.defaults/
env:
TRIAGE_SOURCE: jira
GITHUB_ISSUE_URL: ""
LINKED_GITHUB_REPOS: ${{ steps.jira-config.outputs.linked_repos }}
JIRA_EMAIL: ${{ secrets.JIRA_EMAIL }}
JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }}
with:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

4. Empty github_issue_url breaks triage 🐞 Bug ≡ Correctness

jira-triage.yml forces GITHUB_ISSUE_URL to an empty string, but the default triage harness runs
scripts/pre-triage.sh/scripts/post-triage.sh, which validate GITHUB_ISSUE_URL as a GitHub
issue URL and exit non-zero if it’s missing/invalid. This means the newly scaffolded Jira triage
workflow will fail unless the repo provides Jira-specific harness/script overrides.
Agent Prompt
### Issue description
The scaffolded `jira-triage.yml` sets `GITHUB_ISSUE_URL: ""`, but the default triage harness uses GitHub-issue-centric pre/post scripts that hard-fail when `GITHUB_ISSUE_URL` isn’t a GitHub issue URL. As a result, Jira triage will break out-of-the-box.

### Issue Context
`internal/scaffold/fullsend-repo/harness/triage.yaml` wires `pre_script`/`post_script` to `scripts/pre-triage.sh` and `scripts/post-triage.sh`, and both scripts enforce a GitHub issue URL regex.

### Fix Focus Areas
- internal/scaffold/fullsend-repo/templates/jira-triage.yml[79-168]
- internal/cli/jira.go[43-310] (optional: preflight validation before committing workflows)

### Suggested fix (choose one approach)
**A) Fail fast with a clear message (minimal + matches PR prerequisite):**
- Add a step in `jira-triage.yml` before “Run triage agent” that verifies required Jira overrides exist (e.g. `.fullsend/customized/harness/triage.yaml` and/or Jira-specific `scripts/pre-triage.sh` + `scripts/post-triage.sh`). If missing, emit `::error::` explaining that Jira triage requires customization overrides and exit 1.
- Remove `GITHUB_ISSUE_URL: ""` from the workflow env to avoid forcing an invalid value; let the customized harness/scripts decide what they need.

**B) Make defaults Jira-aware (more invasive):**
- Update `scripts/pre-triage.sh` and `scripts/post-triage.sh` to branch on `TRIAGE_SOURCE==jira` (or similar) and skip GitHub label mutations / GitHub URL validation in Jira mode, using `ISSUE_KEY` instead.

**C) CLI preflight (recommended in addition to A or B):**
- In `fullsend jira enroll`, before committing workflows, check the repo contains the required `.fullsend/customized/...` overrides; otherwise return a user-facing error telling them what to add.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +41 to +49
ENROLLED=$(PROJECT_KEY="${PROJECT_KEY}" python3 -c "
import os, yaml
key = os.environ['PROJECT_KEY']
with open('.jira.yml') as f:
cfg = yaml.safe_load(f)
projects = cfg.get('jira_projects', [])
keys = [p.get('project_key','') for p in projects]
print('true' if key in keys else 'false')
" 2>/dev/null || echo "false")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

5. Undeclared pyyaml workflow dependency 🐞 Bug ☼ Reliability

Both Jira workflow templates parse .jira.yml via python3 -c 'import yaml' but don’t install
PyYAML or otherwise ensure the yaml module is present, creating an undeclared runner dependency
that can break enrollment validation/config extraction. This makes the scaffolds less portable
across runner images.
Agent Prompt
### Issue description
The workflow templates rely on `python3` having the `yaml` module available (`import os, yaml`) to parse `.jira.yml`, but the templates don’t install PyYAML. This is an undeclared dependency and can cause runtime failures on environments where PyYAML isn’t preinstalled.

### Issue Context
The Jira dispatch and triage templates both use inline Python with `yaml.safe_load`.

### Fix Focus Areas
- internal/scaffold/fullsend-repo/templates/jira-dispatch.yml[21-57]
- internal/scaffold/fullsend-repo/templates/jira-triage.yml[79-135]

### Suggested fix
- Add an explicit setup step before the first `python3 -c` invocation:
  - `python3 -m pip install --user --quiet pyyaml`
- Or replace the Python YAML parsing with a tool already expected by the repo (e.g., `yq`) and include a presence check + install step if needed.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread internal/cli/jira.go
Comment on lines +116 to +119
githubPAT := resolveEnvOrFlag(cfg.githubPAT, "FULLSEND_JIRA_GITHUB_PAT")
if githubPAT == "" && !cfg.skipAutomationRules {
return fmt.Errorf("GitHub PAT is required for automation rules: use --github-pat or set FULLSEND_JIRA_GITHUB_PAT")
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

6. Dry-run unnecessarily requires pat 🐞 Bug ☼ Reliability

runJiraEnroll errors if FULLSEND_JIRA_GITHUB_PAT is missing whenever --skip-automation-rules
is false, even when --dry-run is enabled and the code will never call the Automation API. This
prevents “preview only” runs without supplying a PAT that won’t be used.
Agent Prompt
### Issue description
The CLI requires a GitHub PAT for automation rules even in `--dry-run` mode, despite the dry-run path not creating rules. This makes dry-run less useful as a low-friction preview.

### Issue Context
The PAT requirement is enforced before the dry-run short-circuit in the rule creation loop.

### Fix Focus Areas
- internal/cli/jira.go[116-120]
- internal/cli/jira.go[234-242]

### Suggested fix
- Gate the PAT requirement on actually creating rules:
  - `if githubPAT == "" && !cfg.skipAutomationRules && !cfg.dryRun { ... }`
- (Optional) Consider similarly relaxing Jira credential requirements under `--dry-run` if no Jira API calls will be made in that mode.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant