Skip to content

feat(atlassian): add the Atlassian connector — Jira verify + Confluence publish [roadmap:atlassian]#11

Merged
tcballard merged 9 commits into
claude/repo-topology-convergence-2a1awbfrom
claude/atlassian-connector-plan-36nida
Jul 2, 2026
Merged

feat(atlassian): add the Atlassian connector — Jira verify + Confluence publish [roadmap:atlassian]#11
tcballard merged 9 commits into
claude/repo-topology-convergence-2a1awbfrom
claude/atlassian-connector-plan-36nida

Conversation

@tcballard

@tcballard tcballard commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements rac/roadmaps/atlassian-connector.md (epic #4; sub-issues #5#9).

Adds:

  • rac-connect atlassian verify — read-only Jira related_tickets existence/state checks over rac export --graph, exit 3 on findings (the CI gate)
  • rac-connect atlassian publish — idempotent managed Confluence pages over rac export --documents, keyed by a content property, never the title
  • The TicketVerifier/PagePublisher seams, VerifySummary, the [atlassian] extra (an internal httpx client, no Atlassian SDK), the additive GraphEdge.external/provider contract fields, 30 offline tests, the docs page with the release-gating smoke checklist, ADR-010/ADR-011, and the design + roadmap artifacts

Roadmap / ADR Trace

Roadmap:

Relevant ADRs:

  • rac/decisions/adr-010-atlassian-verify-publish-seams.md (new)
  • rac/decisions/adr-011-confluence-page-identity-and-idempotency.md (new)
  • rac/decisions/adr-002-connector-interface.md, adr-003-graph-connector-seam-neo4j.md (the sibling-seam precedent)
  • rac-core ADR-087 (delegates ticket state checks here), ADR-090 (suite placement), ADR-065 (untrusted corpus content, propose-only write-back), ADR-086 (air-gap)

Scope

Included

  • Verify: edge selection strictly by contract markers (external + provider == "jira"); key extraction from bare references and /browse/ URLs; dedupe with multi-artifact attribution; 100-key bulkfetch batches with fields=["status"]; exists/missing/forbidden classification incl. statusCategory; credential-free --dry-run
  • Publish: page identity via the lore.artifact_id content property + lore-managed label; sha256 body-hash skip (second publish over an unchanged corpus performs zero writes); version-checked updates, 409 surfaced as a skip and never forced; deterministic escape-first Markdown-to-storage rendering (hostile HTML/macros stay inert, CDATA break-out blocked, only http/https/mailto links become anchors)
  • Client: Basic auth from ATLASSIAN_BASE_URL/ATLASSIAN_EMAIL/ATLASSIAN_API_TOKEN; capped jittered backoff honouring Retry-After on 429/5xx; 401/403 fail fast without retry
  • CI now installs .[dev,atlassian] so the mock-transport wire tests run (they importorskip httpx, so a bare .[dev] install still passes)

Excluded

  • Inbound Confluence ingest (rac-core ADR-017/065-gated; future roadmap item)
  • Jira comment-mode; Data Center profile (Bearer PAT, v2 endpoints); OAuth 3LO
  • Jira remote-link backlinks from publish (client support is wired and tested; the CLI flag is deferred)
  • Table rendering (degrades to escaped text); --json verify output (exit code is the day-one machine contract)
  • Release tag — gated on the docs page's live smoke test (atlassian: live smoke test + first release tag #10)

Product / Architecture Decisions

  • Two sibling seams, not overloads (ADR-010): verify is read-shaped and returns a report, never records — the shape keeps the recall/re-rank surface ADR-002 rejects structurally impossible. This extends the seam family exactly the way ADR-003 did for graphs; ADR-002 is not reopened.
  • No Atlassian SDK: ~6 endpoints; an internal httpx client is a smaller supply chain than a wrapper library and the wire behaviour (backoff, conflict handling) is the part worth testing. Jira Cloud's classic /search endpoint no longer exists — the client uses bulkfetch; search, if ever needed, is POST /search/jql.
  • Page identity is a content property, never the title (ADR-011): title-keying orphans pages on rename; id write-back into the corpus would violate the propose-only trust boundary (rac-core ADR-065). The mapping lives in Confluence itself — no local state.
  • Change detection hashes our rendered output, not the read-back body — Confluence normalizes stored XHTML, so read-and-compare flaps.
  • Verify exits 3 on findings, distinct from 1 (malformed input) and 2 (missing credentials), so CI can gate on reference state without conflating it with operator error.
  • Nested CLI verbs (rac-connect atlassian verify|publish): the suite grows verbs (comment-mode, ingest) and a flat atlassian-verify would fork the backend namespace.

User-Facing Contract

CLI

rac export rac/ --graph     | rac-connect atlassian verify [--dry-run] [-i FILE] [-v]
rac export rac/ --documents | rac-connect atlassian publish [--dry-run] [--strict] [--space KEY] [-i FILE] [-v]

Human Output

Per-reference lines (exists PROJ-1: Done (done) <- RAC-…, missing PROJ-404: … <- RAC-…) — findings always print, even without --verbose; summary line to stderr (atlassian verify: 2 checked, 1 exist, 1 missing, 0 forbidden, 2 skipped). Publish action lines: created page … / updated page … / skip …: unchanged / version conflict … not overwritten.

JSON Output

None added. The graph input contract now surfaces edges[].external and edges[].provider in the reader (additive, tolerant defaults; emitted by rac-core since v0.25.0).

Exit Codes

  • 0: verify — all checked references exist; publish — done
  • 1: malformed --graph payload / malformed JSONL line under --strict
  • 2: credentials missing (each absent ATLASSIAN_* variable named); publish also: no space configured
  • 3: verify only — one or more references missing or forbidden

Verification

Ran

  • python -m pytest -q — 192 passed (was 162; +30: graph marker parsing, verify behaviour, publish behaviour, render goldens, client wire contract, CLI verbs)
  • python -m ruff check src/ tests/ scripts/, ruff format --check, python -m mypy src/ — clean
  • python scripts/sync_readme.py --check — in sync (8 connectors; new "Workspace & ticketing" group, no script change needed — the grouping is data-driven)
  • rac validate rac/, rac relationships rac/ --validate — exit 0; rac review rac/ — 100/100, nothing needs attention
  • End-to-end against this repo's own corpus: rac export rac/ --graph | rac-connect atlassian verify --dry-run — 7 github-provider tickets correctly skipped, exit 0; rac export rac/ --documents | rac-connect atlassian publish --dry-run — 15 pages planned, exit 0

Covered

  • Verify: github-provider and verified_by (provider-null) edges skipped by construction; resolved in-corpus edges ignored; key dedupe with multi-artifact attribution; the 101-keys-to-2-batches boundary; permission-hinted per-key errors and whole-batch auth failures classified forbidden; unparseable targets skipped not checked; empty graph exits 0; dry-run makes zero client calls
  • Publish: create path sets property + label; unchanged-hash second run performs zero writes; changed body updates version+1; 409 conflict recorded and the stream continues (and the stored hash does not advance past the human's edit)
  • Render: byte-exact golden; determinism = stable hash; script tags, injected ac:structured-macro text, and CDATA ]]> break-outs stay inert; javascript: links stay text
  • Client: Basic-auth header and bulkfetch body shape; Retry-After honoured with a capped 4-retry budget then surfaced; 401/403 fail fast with no retry; 409 raises VersionConflictError; every missing env var named
  • CLI: 0/1/2/3 exit contract, contract-major mismatch warning, --strict, --input, missing-space error

Review Path

  1. rac/decisions/adr-010…, adr-011…, rac/designs/atlassian-connector-shape.md — the contract
  2. src/rac_connectors/graph.py, base.py — the additive contract/seam layer
  3. src/rac_connectors/atlassian/client.py — auth, backoff, the six endpoints
  4. jira.py, render.py, confluence.py, connector.py — the two verbs
  5. cli.py — nested verb wiring and exit codes
  6. Tests (fakes for behaviour, MockTransport for the wire, goldens for render), then docs/connectors/atlassian.md + README region + CHANGELOG

Notes For Reviewer

  • render.py is the security-sensitive file: corpus content is untrusted input (rac-core ADR-065), and the escape-first discipline plus the CDATA and link-scheme guards are what keep a hostile artifact from smuggling markup into Confluence. The golden tests pin it.
  • The verify auth-failure path deliberately reports whole-batch 401/403 as forbidden findings (exit 3) rather than exit 2 — exit 2 is reserved for missing configuration; a rejected credential is instance state worth surfacing per reference.
  • Known limitation: a managed page manually stripped of its property/label becomes invisible and a fresh publish creates a sibling (recorded with mitigation in ADR-011; orphan adoption is a named follow-up).
  • The live path is unproven until the smoke test (atlassian: live smoke test + first release tag #10) runs — the docs page carries the checklist and gates the first tag.

Implementation Process

Implemented with AI assistance under the roadmap contract; final scope, review, and acceptance decisions rest with the maintainer.

tcballard added 9 commits July 2, 2026 04:45
…ADR-011) [roadmap:atlassian]

ADR-010 fixes the TicketVerifier/PagePublisher sibling seams (ADR-003
precedent), the no-SDK httpx client, Cloud-first auth, bulkfetch reads, the
nested CLI verbs, and the exit-code contract. ADR-011 fixes Confluence page
identity (content property, never title), hash-based idempotency, and
version-checked never-forced updates. The design
rac/designs/atlassian-connector-shape.md works the module and client shape.
…lassian]

Implements rac/roadmaps/atlassian-connector.md. Sets ticketing.provider:
github so the roadmap's ## Related Tickets references
(#4-#10) format-lint offline (ADR-087, ADR-093).
…roadmap:atlassian]

The --graph contract already emits external: true on external-target edges
(related_tickets, rac-core ADR-087; verified_by, rac-core ADR-096) and the
configured ticketing provider on ticket edges (ADR-074). Parse both
additively with tolerant defaults so the verify verb can select Jira edges
by contract markers instead of guessing from key shapes.
…:atlassian]

An internal client over httpx — no Atlassian SDK (ADR-010) — implementing
the JiraClient and ConfluenceClient Protocols against Cloud: Basic auth from
ATLASSIAN_BASE_URL/EMAIL/API_TOKEN, bulkfetch with narrow fields, globalId
remote-link upsert, property-keyed page lookup (ADR-011), version-checked
updates with 409 -> VersionConflictError, and capped jittered backoff
honouring Retry-After on 429/5xx. Ships as the [atlassian] extra; the wire
tests run through httpx.MockTransport and importorskip without it.
… edges [roadmap:atlassian]

The read-only verify verb (ADR-010): selects --graph edges marked
external with provider jira, dedupes issue keys from bare references and
browse URLs, batches 100 at a time through bulk fetch, and classifies each
as exists (with statusCategory), missing, or forbidden — attributed back to
the source artifacts. TicketVerifier/PagePublisher seams and VerifySummary
join base.py beside the existing seams; the CLI gains the nested
'atlassian verify' verb with exit 3 on findings.
…s [roadmap:atlassian]

Fake-client batteries: edge selection (github and verified_by skipped by
construction), key dedupe with multi-artifact attribution, the 100-key
batch boundary, permission-hinted and whole-batch auth failures as
forbidden, credential-free dry-run, and the 0/1/2/3 CLI exit-code
contract including the contract-major warning.
…p:atlassian]

A deliberately small deterministic subset (headings, paragraphs, emphasis,
inline code, fenced code as the code macro, flat lists, links), rendered
escape-first because corpus content is untrusted input (rac-core ADR-065):
hostile HTML and macro text stay inert, CDATA cannot break out of code
blocks, and only http/https/mailto links become anchors. body_hash over the
rendered bytes is the publish idempotency signal (ADR-011); golden tests
pin the output byte-for-byte.
…identity [roadmap:atlassian]

The publish verb (ADR-011): pages are keyed by the lore.artifact_id content
property plus the lore-managed label — never the title, never id write-back
into the corpus. Unchanged body hashes skip without a write (a second
publish over an unchanged corpus performs zero writes); changed pages
update version-checked, and a 409 conflict is surfaced and left alone. The
CLI gains 'atlassian publish' with --space / ATLASSIAN_CONFLUENCE_SPACE and
the standard malformed-line skip / --strict handling.
Quickstart, the verify exit-code table (including 3, the CI gate), the
page-identity and idempotency model, the escape-first rendering notes, the
named deferrals, and the live smoke-test checklist that gates any release
tag. README connectors region resynced (new 'Workspace & ticketing' kind
group); CHANGELOG gains the Unreleased entry.
@tcballard
tcballard merged commit a2dcfaf into claude/repo-topology-convergence-2a1awb Jul 2, 2026
4 checks passed
@tcballard
tcballard deleted the claude/atlassian-connector-plan-36nida branch July 2, 2026 05:20
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