Skip to content
Open
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
139 changes: 139 additions & 0 deletions docs/ADRs/0079-jira-project-enrollment-via-cli.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
---
title: "79. Jira project setup via fullsend CLI"
status: Accepted
relates_to:
- agent-infrastructure
topics:
- jira
- enrollment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[MEDIUM] Filename and topic tag use "enrollment", the verb this ADR explicitly rejects in favor of "setup"

Option 2 explicitly argues for the verb setup over enroll "to avoid overloading" with fullsend github enroll, and the Decision names the command fullsend jira setup. The title/H1 were updated to "Jira project setup via fullsend CLI", but the filename 0079-jira-project-enrollment-via-cli.md and this topics: frontmatter entry (enrollment) still use the rejected term. ADR filenames are effectively permanent identifiers post-merge, so this leaves a lasting mismatch between what the file is named/tagged and what it decides.

Suggestion: rename the file to 0079-jira-project-setup-via-cli.md and swap the enrollment topic for setup before merge, since renaming after merge would violate this repo's ADR immutability convention.

- external-issue-trackers
- credentials
---

# 79. Jira project setup via fullsend CLI

Date: 2026-07-09

## Status

Accepted

## Context

Fullsend's dispatch model (see [agent-infrastructure](../problems/agent-infrastructure.md))
uses pluggable input drivers to normalize events from multiple sources
([ADR 0061](0061-harness-cel-dispatch.md)). For Jira, the poll driver
([ADR 0063](0063-polling-based-work-discovery.md)) discovers work items
via JQL and dispatches agents through the shared pipeline. ADR 0063
defers credential placement as an open question — Jira API tokens need
to reach the poll driver and agent pre-scripts, but no ADR specifies
how those credentials are provisioned or where enrollment metadata lives.

A proof-of-concept ([manish-jira](https://github.com/rh-hemartin-fullsendai/manish-jira))
validated end-to-end Jira-to-agent dispatch using classic (unscoped)
API tokens against `<tenant>.atlassian.net`. The PoC is GitHub-specific
and uses push-based `repository_dispatch`; this ADR intentionally
narrows scope to credential provisioning and poll-driver config,
deferring dispatch mechanics to
[ADR 0063](0063-polling-based-work-discovery.md). The enrollment steps
were entirely manual in the PoC; the CLI automates them.

Atlassian is deprecating unscoped API tokens. Scoped tokens require the
`api.atlassian.com` gateway with a Cloud ID in the URL
(`https://api.atlassian.com/ex/jira/{cloudId}/rest/api/3/...`) and
Basic auth (`email:token`). The Cloud ID is a stable site identifier

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[MEDIUM] Cloud ID resolution via _edge/tenant_info stated as a stable fact with no citation or fallback

This states as settled fact that the Cloud ID "is a stable site identifier resolvable from any tenant hostname via https://<host>/_edge/tenant_info" (repeated in the Decision as the mechanism the CLI depends on) with no citation. Checked against Atlassian's own community and support material: /_edge/tenant_info is not part of Atlassian's documented REST API surface — community threads explicitly warn "it shouldn't be relied on, even if it currently returns the cloudId" and report inconsistent behavior (e.g. 403s) depending on calling context. Atlassian's supported alternative for programmatic use is GET https://api.atlassian.com/oauth/token/accessible-resources, which requires an OAuth 2.0 (3LO) flow rather than the Basic-auth email+API-token model this ADR adopts — a real tradeoff never acknowledged. This is load-bearing for the whole credential/config design (it's the only way the CLI obtains the Cloud ID it stores) yet is presented with no fallback if the endpoint changes or returns errors.

Suggestion: note explicitly that /_edge/tenant_info is not part of Atlassian's supported REST API, and record a fallback (e.g. allow --cloud-id override, or document why the OAuth-based accessible-resources alternative wasn't used) rather than presenting it as a guaranteed-stable mechanism.

resolvable from any tenant hostname via
`https://<host>/_edge/tenant_info`; the CLI resolves it once at
enrollment time.

## Options

### Option 1: Manual credential setup per documentation

Operators follow a guide to create forge secrets and edit
`.fullsend/config.yaml` by hand. Rejected — error-prone for multi-project
setups and inconsistent across forges.

### Option 2: `fullsend jira setup` CLI command

A CLI command provisions credentials and writes poll driver connection
config. The verb `setup` is chosen over `enroll` to avoid overloading:
`fullsend github enroll` is a lightweight config toggle that does not

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[HIGH] Naming rationale for jira setup compares against github enroll, a per-org-only command already slated for removal

This justifies setup over enroll by contrasting with fullsend github enroll: "is a lightweight config toggle that does not set secrets." Checked against docs/cli/github.md: github enroll <org> [repo...] only takes an org argument and updates config.yaml in the org's .fullsend config repo — it's a per-org-mode-only command with no per-repo equivalent. ADR 0044 (Accepted) deprecates the entire per-org installation model and explicitly lists github enroll/unenroll and the underlying EnrollmentLayer for removal in "Phase 2 — Removal (v2.0 release, breaking change)." Meanwhile ADR 0063, which this ADR builds on, is scoped exclusively to per-repo mode ("Polling is implemented only for per-repo mode... Per-org installations continue to rely on event-driven dispatch only"), where github enroll doesn't even apply. The comparison basis is both deprecated and out of scope for the context this ADR operates in.

Suggestion: drop the comparison to github enroll, or compare against github setup (the command that actually supports per-repo mode and also provisions secrets/variables) instead of a command tied to the deprecated per-org model.

set secrets, while `jira setup` provisions forge secrets alongside
config. Follows established CLI patterns (cobra subcommands,
`--dry-run`).

## Decision

Add a `fullsend jira setup <target-repo>` CLI command that operates on
a remote `owner/repo` target via the forge API (no local clone
required). The command accepts Jira credentials via environment
variables (`JIRA_HOST`, `JIRA_EMAIL`, `JIRA_API_TOKEN`) and supports

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[MEDIUM] Forge secret names JIRA_EMAIL/JIRA_API_TOKEN lack the FULLSEND_ prefix used elsewhere in the codebase

This specifies storing forge-level secrets literally named JIRA_EMAIL and JIRA_API_TOKEN. Every other credential in this codebase is namespaced under FULLSEND_: internal/layers/secrets.go generates FULLSEND_%s_APP_PRIVATE_KEY / FULLSEND_%s_CLIENT_ID, and ADR 0044 references FULLSEND_MINT_URL and (legacy) FULLSEND_DISPATCH_TOKEN. Bare names like JIRA_EMAIL/JIRA_API_TOKEN are generic enough that a repo could already have secrets with those exact names for an unrelated integration; since fullsend jira setup is described as idempotent by overwriting the forge secret, re-running it would silently clobber any pre-existing secret of the same name.

Suggestion: namespace the secrets consistently with the rest of the codebase, e.g. FULLSEND_JIRA_EMAIL / FULLSEND_JIRA_API_TOKEN, and record this as an explicit Decision point.

`--dry-run`. Credentials are never accepted via CLI flags to avoid
shell-history and process-list exposure.

The CLI resolves the Jira Cloud ID from the host URL at enrollment
time (`https://<host>/_edge/tenant_info`) and writes two non-secret
values into the poll driver's `poll.input_drivers[].connection` block
in `.fullsend/config.yaml`:

```yaml

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[HIGH] Sample connection config omits ADR 0063's mandatory queries field and invents an unmodeled project_key

ADR 0063 states each poll input driver entry must specify "at minimum: Driver type, Connection (base URL, credentials reference), Queries — one or more search expressions (JQL for Jira)" and its own YAML example shows connection: { ... } plus a mandatory queries: list (e.g. project = PROJ AND status != Done). This ADR's example YAML (lines 80-87) writes connection: { cloud_id, project_key } and has no queries block at all, and project_key does not exist anywhere in ADR 0063's model — project scoping there is expressed purely via JQL. An implementer following this example literally would produce a poll.input_drivers entry the ADR 0063 poll driver can't run (no JQL to execute).

Suggestion: either show project_key feeding into a generated default queries entry (e.g. project = <project_key> AND status != Done) and state that mapping explicitly, or drop project_key and have the CLI write a queries entry directly per ADR 0063's existing contract. Also state the credential-reference convention (fixed env-var names JIRA_EMAIL/JIRA_API_TOKEN) so the schema is a valid instance of ADR 0063's contract rather than a divergent one.

poll:
input_drivers:
- type: jira-poll
connection:
cloud_id: "<resolved-at-enrollment>"
project_key: EXAMPLE
```

Two credentials are stored as forge-level secrets (not checked into

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[MEDIUM] No mechanism specified for multiple Jira connections per repo sharing fixed-name secrets

poll.input_drivers[] is a list per ADR 0063, implying a repo could enroll more than one Jira project or site, but credentials here are stored under fixed secret names (JIRA_EMAIL, JIRA_API_TOKEN). This doesn't say what happens if fullsend jira setup is re-run for a second project_key or a different JIRA_HOST: does it append a new input_drivers entry or overwrite the existing one? If two entries pointed at different Jira sites, both would need distinct credentials, but only one JIRA_EMAIL/JIRA_API_TOKEN pair can exist under those fixed names — a real collision the Consequences section doesn't scope out or acknowledge.

Suggestion: either explicitly scope the decision to "one Jira connection per repo" in Consequences, or specify how multiple connections get distinct credential references (e.g. namespaced secret names or a credential-reference field in connection, matching ADR 0063's own "credentials reference" language).

the repository): `JIRA_EMAIL` (Atlassian account email) and
`JIRA_API_TOKEN` (scoped API token). Together these support Basic auth
against the `api.atlassian.com` gateway. This is compatible with
[ADR 0017](0017-credential-isolation-for-sandboxed-agents.md)'s
credential isolation model. Ensuring credentials stay outside the
agent sandbox is the harness author's responsibility (via `env.runner`
/ `env.sandbox` per [ADR 0055](0055-unified-env-var-delivery.md) and
pre/post scripts).

The command is designed to be idempotent — re-running with a new token
updates the forge secret. The scope is credentials and config only;
the dispatch mechanism is the poll driver's
responsibility ([ADR 0063](0063-polling-based-work-discovery.md)),
and agent-level Jira awareness (harness `pre_script` /
`post_script`) is the repo admin's responsibility. This is distinct
from the CLI's `EnrollmentLayer` ([ADR 0006](0006-ordered-layer-model.md)),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[MEDIUM] Decision anchors on EnrollmentLayer/ADR 0006, but the concrete type is defined by ADR 0044 and is slated for removal

This says the command "is distinct from the CLI's EnrollmentLayer (ADR 0006)". Checked ADR 0006 directly: it only names a generic enrollment stage in its layer-stack ordering ("config-repo → workflows → vendor-binary → secrets → inference → dispatch-token → enrollment") — the concrete Go type EnrollmentLayer is not defined there. It is ADR 0044 ("Deprecate per-org installation mode", Accepted) that names EnrollmentLayer explicitly and lists it under "Remove per-org layers (ConfigRepoLayer, DispatchTokenLayer, EnrollmentLayer)" in its Phase 2 removal plan. So this ADR both cites the wrong ADR for the concept and anchors its own naming distinction on a component that an already-Accepted ADR plans to delete, with no forward pointer for when that happens.

Suggestion: cite ADR 0044 alongside (or instead of) ADR 0006 for EnrollmentLayer, and add a short note that it is slated for removal per ADR 0044 so the reference doesn't silently go stale.

which manages forge-level installation scaffolding.

## Consequences

- Jira credential provisioning is automated and consistent across
forges. Two forge secrets (`JIRA_EMAIL`, `JIRA_API_TOKEN`) and two
config values (`cloud_id`, `project_key`) fully describe a Jira
connection.
- Credentials are stored as forge secrets, compatible with
[ADR 0017](0017-credential-isolation-for-sandboxed-agents.md)'s
isolation model. Sandbox isolation is enforced downstream by harness
configuration (`env.runner` / `env.sandbox` per
[ADR 0055](0055-unified-env-var-delivery.md), pre/post scripts), not
by enrollment.
- The Jira-token portion of ADR 0063's open question on credential
placement is resolved for the enrollment path; the CLI writes
connection metadata directly into `poll.input_drivers[].connection`.
No separate `integrations.jira` config key is introduced — if a
push-based dispatch path (e.g. Jira Automation webhooks) is adopted
later, a future ADR can introduce integration-level config at that
point. Forge-native credentials (`GITHUB_TOKEN`, App creds) remain
unaddressed by this ADR.
- Credentials are accepted only via environment variables, not CLI
flags, to avoid shell-history and process-list exposure.
- The Cloud ID is resolved once at enrollment time from the Jira host
URL. Runtime API calls use the `api.atlassian.com` gateway with the
stored Cloud ID, avoiding per-poll lookups.
- Jira API token rotation is the repo admin's responsibility —
re-running `fullsend jira setup` with a new token is designed to
update the forge secret. Per-forge idempotency verification is
tracked as an implementation concern.
- Repo-to-issue association is out of scope — the poll driver

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[MEDIUM] Consequences misattributes repo-to-issue routing to ADR 0063, which defines no such mechanism

This states "Repo-to-issue association is out of scope — the poll driver (ADR 0063) handles which issues route to which repositories." Checked ADR 0063's actual scope: it explicitly limits itself to "A single target repository owns poll configuration, credential references, and the dispatch output path" (1:1 repo-to-Jira-project polling) and states polling is per-repo-mode only. ADR 0063 defines no cross-repo issue-routing mechanism at all, so there is no existing design for "repo-to-issue association" to actually defer to — the sentence implies a solved problem that isn't solved anywhere in the cited ADR.

Suggestion: rephrase to state plainly that multi-repo/cross-project issue routing is an unsolved problem deferred to a future ADR, rather than implying ADR 0063 already covers it.

([ADR 0063](0063-polling-based-work-discovery.md)) handles which
issues route to which repositories.
1 change: 1 addition & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ the dedicated org-level `<org>/.fullsend` config repo is deprecated
- Dispatch version-skew resolution: per-repo `reusable-dispatch.yml` inlines stage workflow jobs directly, eliminating `@v0` references to `reusable-{stage}.yml` ([ADR 0062](ADRs/0062-dispatch-version-skew.md)).
- Ready-made configuration presets: `fullsend github setup --config <path-or-url>` installs a vendor preset as `.fullsend/config.base.yaml` and a stub `.fullsend/config.yaml` overlay in the target repository; mint URL, inference backend, and related settings live in configuration files resolved through accessor methods, not CLI flags. Shared-infrastructure presets will reduce per-adopter enrollment (target state): mint via `job_workflow_ref` trust per [ADR 0059](ADRs/0059-public-mint-mode-with-wildcard-allowlists.md); inference authorization model undecided ([ADR 0069](ADRs/0069-ready-made-configuration-presets.md)); enrollment remains required until follow-on ADRs land.
- GitLab event dispatch: two-path model — native CI triggers (`merge_request_event`) for MR events, cron-based polling for issues/comments/labels. No external infrastructure (no webhook bridge). Bot PAT via OIDC/WIF from Secret Manager or protected CI/CD variable. Per-repo only ([ADR 0067](ADRs/0067-gitlab-cron-polling-event-dispatch.md)).
- Jira project connection: `fullsend jira setup` CLI command provisions Jira credentials (`JIRA_EMAIL`, `JIRA_API_TOKEN`) as forge secrets and writes `cloud_id` + `project_key` into the poll driver's `poll.input_drivers[].connection` block in `.fullsend/config.yaml` ([ADR 0079](ADRs/0079-jira-project-enrollment-via-cli.md)). This is distinct from the `EnrollmentLayer` ([ADR 0006](ADRs/0006-ordered-layer-model.md)) that manages forge-level installation scaffolding. Repo-to-issue association is handled by the poll driver design ([ADR 0063](ADRs/0063-polling-based-work-discovery.md)).

**Open questions:**

Expand Down