Repository navigation
feat(jira): add fullsend jira enroll command for Jira project enrollment - #3207
manish-jangra wants to merge 1 commit into
Conversation
…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>
|
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:
See CONTRIBUTING.md for details. |
E2E tests did not runE2E tests run automatically for org/repo members and collaborators on pull requests. For other contributors, a maintainer must add the See E2E testing guide for details. |
PR Summary by QodoAdd
AI Description
Diagram
High-Level Assessment
Files changed (12)
|
Code Review by Qodo
Context used✅ Compliance rules (platform):
61 rules✅ Skills:
writing-user-docs, writing-adrs 1. Swallowed .jira.yml read errors
|
| 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}, |
There was a problem hiding this comment.
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
| 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 | ||
| } |
There was a problem hiding this comment.
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
| - 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: |
There was a problem hiding this comment.
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
| 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") |
There was a problem hiding this comment.
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
| 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") | ||
| } |
There was a problem hiding this comment.
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
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
GET /rest/api/3/project/{key}).jira.ymlenrollment config in the GitHub repo/fs-triageslash command)jira-dispatch.ymlandjira-triage.ymlworkflow files to the repoJIRA_HOST,JIRA_EMAIL,JIRA_API_TOKENas GitHub repo secretsAutomation 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
internal/jira/config.go.jira.ymlparsing —JiraConfig,AddProject(idempotent),HasProjectinternal/jira/types.goCreateRuleRequest,RulePayload,RuleComponent,RuleActor)internal/jira/client.goGetCloudID,ValidateProject,GetCurrentUser,CreateAutomationRule,ListAutomationRules,RuleExistsByNameinternal/jira/rules.goAutoTriageRule,SlashCommandRule,ManualRuleInstructions. Component types reverse-engineered from existing rulesinternal/jira/rules_test.gointernal/jira/client_test.gohttptestmocks — success paths, error codes, malformed JSONinternal/jira/config_test.gointernal/cli/jira.gofullsend jira enrollcommand — flags, credential resolution, execution flow, 403 fallbackinternal/cli/root.gonewJiraCmd()internal/scaffold/scaffold.goJiraDispatchTemplate(),JiraTriageTemplate()helpersinternal/scaffold/fullsend-repo/templates/jira-dispatch.ymlinternal/scaffold/fullsend-repo/templates/jira-triage.ymlfullsend run triagewithTRIAGE_SOURCE=jiraDesign decisions
fullsend githubpattern —fullsend jirais a parallel command group with the same cobra structure,ui.Printeroutput, and credential resolution cascade (flag → env → prompt)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)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).jira.ymlskips existing projects, rule creation skips rules with matching names,CreateOrUpdateFilefor config,CreateRepoSecretoverwritesTest plan
go test ./internal/jira/...— 49 tests pass, 93.5% coveragego build ./cmd/fullsend/— clean buildfullsend jira enroll --help— displays correct usage and flags--dry-run— previews all steps without making changes--skip-workflows— tested against livestage-redhat.atlassian.netwith KFLUXINFRA projectOut of scope (future work)
fullsend jira unenroll/fullsend jira statussubcommands🤖 Generated with Claude Code