Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ jobs:
- name: Install package with dev extras
run: |
python -m pip install --upgrade pip
python -m pip install -e .[dev]
python -m pip install -e .[dev,atlassian]

- name: Run tests
run: python -m pytest -q
7 changes: 7 additions & 0 deletions .rac/config.yaml
Original file line number Diff line number Diff line change
@@ -1 +1,8 @@
repository_key: LCON

# External ticketing provider (ADR-087, ADR-093). Roadmap intent lives in the
# corpus; execution is tracked in GitHub issues, linked from a live artifact's
# `## Related Tickets` section. `rac validate` format-lints those references as
# `owner/repo#123` or a URL — offline; the engine never fetches issue state.
ticketing:
provider: github
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,32 @@ All notable changes to `rac-connectors` are recorded here. Versions are CalVer
(`YYYY.M.N`, ADR-008); the version is derived from the git tag by setuptools-scm,
and the published distribution is `rac-connectors`.

## Unreleased

The Atlassian suite connector — Jira + Confluence (rac-core ADR-090), the
first export-direction integration. Release gate: the docs page's live
smoke test against a real Cloud site (itsthelore/rac-connectors#10).

### Added

- The `atlassian` module and the nested CLI verbs: `rac-connect atlassian
verify` (read-only Jira `related_tickets` existence/state checks over
`rac export --graph`, exit 3 on findings — the CI gate; ADR-010) and
`rac-connect atlassian publish` (idempotent managed Confluence pages over
`rac export --documents`, keyed by the `lore.artifact_id` content
property with body-hash skip and conflict surfacing; ADR-011).
- The `[atlassian]` extra — an internal `httpx` client (no Atlassian SDK)
with Basic auth from `ATLASSIAN_*` environment variables and capped,
jittered backoff honouring `Retry-After`.
- The `TicketVerifier` / `PagePublisher` seams and `VerifySummary` beside
the existing `Connector` / `GraphConnector` shapes (ADR-010).

### Changed

- The graph reader now surfaces the export's `external` / `provider` edge
markers (rac-core ADR-087/096) — additive; existing connectors are
unaffected.

## 2026.6.1

First published release of **rac-connectors** — the integrations companion for
Expand Down
140 changes: 140 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,146 @@ Design + decision: [`rac/designs/`](rac/designs) (graph-connector-shape) and

</details>

### Workspace & ticketing

<details>
<summary><strong>Atlassian</strong> — Jira related_tickets verification + Confluence managed-page publish over the export contracts</summary>

The Atlassian suite connector (rac-core ADR-090): **verify** that every Jira
reference in the corpus still points at a real, reachable issue, and
**publish** the corpus into a Confluence space as managed pages. Both verbs
are thin consumers of the export contracts; the engine never talks to
Atlassian (rac-core ADR-087), and the connector only ever contacts the
instance you configure (rac-core ADR-086). No Atlassian SDK — an internal
`httpx` client, see [ADR-010](rac/decisions/adr-010-atlassian-verify-publish-seams.md).

```bash
pip install 'rac-connectors[atlassian]'
export ATLASSIAN_BASE_URL=https://yourorg.atlassian.net
export ATLASSIAN_EMAIL=you@example.com
export ATLASSIAN_API_TOKEN=... # id.atlassian.com API token
export ATLASSIAN_CONFLUENCE_SPACE=DOCS # publish only; or pass --space

rac export rac/ --graph | rac-connect atlassian verify # check Jira refs
rac export rac/ --graph | rac-connect atlassian verify --dry-run # list, no calls
rac export rac/ --documents | rac-connect atlassian publish # mirror pages
rac export rac/ --documents | rac-connect atlassian publish --dry-run # plan, no calls
```

## `verify` — read-only Jira reference checks

Selects the `--graph` projection's ticket edges by contract markers
(`external: true`, `provider: "jira"` — set from your repo's
`ticketing.provider`, rac-core ADR-087), dedupes the issue keys (bare
`PROJ-123` or full `/browse/` URLs), fetches them 100 at a time through
Jira's bulk-fetch endpoint with `fields=["status"]`, and reports each as
**exists** (with status and statusCategory), **missing**, or **forbidden**
— attributed back to the referencing artifacts. `verified_by` edges
(rac-core ADR-096) and other providers' tickets are counted as skipped,
never guessed at. It writes nothing, anywhere.

| Exit code | Meaning |
|---|---|
| 0 | Every checked reference exists. |
| 1 | The input was not a valid `--graph` payload. |
| 2 | Credentials missing from the environment. |
| 3 | One or more references are missing or forbidden — the CI gate. |

| Flag | Meaning |
|---|---|
| `--dry-run` | List the references and batches that would be checked; no client, no calls. |
| `--input`, `-i` | Read the `--graph` JSON from a file (default: stdin; `-` also means stdin). |
| `--verbose`, `-v` | Print per-reference results on a live verify too (findings always print). |

## `publish` — managed Confluence pages

Mirrors the `--documents` stream into one space, idempotent on the canonical
artifact id ([ADR-011](rac/decisions/adr-011-confluence-page-identity-and-idempotency.md)):

- **Page identity is a content property** (`lore.artifact_id`) plus a
`lore-managed` label — never the title, so artifact renames are ordinary
updates; and no page id is ever written back into the corpus (write-back
is propose-only via human PR, rac-core ADR-065).
- **Unchanged pages are skipped without a write.** The property stores a
sha256 of the rendered body; a second publish over an unchanged corpus
performs zero writes.
- **Conflicts are surfaced, never clobbered.** Updates send
`version + 1`; a 409 means a human edited the page — it is reported as a
skip and left alone.
- **Rendering is deterministic and escape-first.** A small Markdown subset
(headings, paragraphs, emphasis, code, fenced blocks, flat lists, links)
becomes storage format; corpus content is untrusted input, so hostile
HTML/macro text stays inert and only `http`/`https`/`mailto` links become
anchors. Tables are not yet rendered (they degrade to escaped text).

| Flag | Meaning |
|---|---|
| `--space` | Confluence space key (default: `ATLASSIAN_CONFLUENCE_SPACE`). |
| `--dry-run` | Print the pages that would be upserted; no client, no calls. |
| `--input`, `-i` | Read JSONL from a file (default: stdin; `-` also means stdin). |
| `--strict` | Fail on a malformed line instead of skipping it. |
| `--verbose`, `-v` | Print per-page actions on a live publish too. |

Exit codes are the standard 0 (done) / 1 (malformed input) / 2 (missing
credentials or space).

## Auth

API token + Basic auth against Atlassian Cloud — mint a token at
id.atlassian.com and set the three `ATLASSIAN_*` variables; one credential
serves Jira and Confluence. Tokens expire; rotate them like any secret.
Data Center (Bearer PAT, v2 Jira endpoints), OAuth, inbound Confluence
ingest, and Jira comment-mode are named deferrals on the
[`atlassian-connector`](rac/roadmaps/atlassian-connector.md) roadmap.

### Python API

```python
from rac_connectors import parse_documents, parse_graph
from rac_connectors.atlassian import (
AtlassianPublisher,
AtlassianVerifier,
client_from_env,
)

client = client_from_env()
report = AtlassianVerifier(client).verify(parse_graph(open("graph.json").read()))
summary = AtlassianPublisher(client, space_key="DOCS").publish(
parse_documents(open("corpus.jsonl"))
)
```

### Live smoke test

The connector is wired and unit-tested against fakes and a mock transport,
but the live path (a real Cloud site) is unproven until someone runs it —
this page is `drafted (live run pending)`. To validate end to end:

1. **Configure the environment** with a real site, account, and API token
(all four variables above; pick a scratch Confluence space).
2. **Verify, dry-run first:**
`rac export rac/ --graph | rac-connect atlassian verify --dry-run`, then
live. With a corpus referencing one known-good and one deleted issue,
confirm the exists/missing split and exit code 3.
3. **Publish twice into the scratch space:**
`rac export rac/ --documents | rac-connect atlassian publish`. First run
creates every page (property + `lore-managed` label set); the second run
must report all pages `unchanged` and perform **zero writes**.
4. **Rename check:** change one artifact's title, re-publish, and confirm
the same page updates in place (no duplicate).
5. **Conflict check:** hand-edit a managed page in Confluence, re-publish a
changed body for that artifact, and confirm the run reports a version
conflict and leaves the human edit alone.
6. **429 behaviour** (optional): run against a busy site and confirm
retries honour `Retry-After` rather than hammering.

Then flip this page's `status` to `shipped` — and only then consider a
release tag (the gate recorded on itsthelore/rac-connectors#10).

**Full page:** [`docs/connectors/atlassian.md`](docs/connectors/atlassian.md)

</details>

<!-- /GENERATED:CONNECTORS -->

## Run it in CI
Expand Down
140 changes: 140 additions & 0 deletions docs/connectors/atlassian.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
<!-- rac-connector
name: Atlassian
tagline: Jira related_tickets verification + Confluence managed-page publish over the export contracts
category: Workspace & ticketing
extra: atlassian
order: 70
status: drafted (live run pending)
-->
# Atlassian (Jira + Confluence)

The Atlassian suite connector (rac-core ADR-090): **verify** that every Jira
reference in the corpus still points at a real, reachable issue, and
**publish** the corpus into a Confluence space as managed pages. Both verbs
are thin consumers of the export contracts; the engine never talks to
Atlassian (rac-core ADR-087), and the connector only ever contacts the
instance you configure (rac-core ADR-086). No Atlassian SDK — an internal
`httpx` client, see [ADR-010](../../rac/decisions/adr-010-atlassian-verify-publish-seams.md).

```bash
pip install 'rac-connectors[atlassian]'
export ATLASSIAN_BASE_URL=https://yourorg.atlassian.net
export ATLASSIAN_EMAIL=you@example.com
export ATLASSIAN_API_TOKEN=... # id.atlassian.com API token
export ATLASSIAN_CONFLUENCE_SPACE=DOCS # publish only; or pass --space

rac export rac/ --graph | rac-connect atlassian verify # check Jira refs
rac export rac/ --graph | rac-connect atlassian verify --dry-run # list, no calls
rac export rac/ --documents | rac-connect atlassian publish # mirror pages
rac export rac/ --documents | rac-connect atlassian publish --dry-run # plan, no calls
```

## `verify` — read-only Jira reference checks

Selects the `--graph` projection's ticket edges by contract markers
(`external: true`, `provider: "jira"` — set from your repo's
`ticketing.provider`, rac-core ADR-087), dedupes the issue keys (bare
`PROJ-123` or full `/browse/` URLs), fetches them 100 at a time through
Jira's bulk-fetch endpoint with `fields=["status"]`, and reports each as
**exists** (with status and statusCategory), **missing**, or **forbidden**
— attributed back to the referencing artifacts. `verified_by` edges
(rac-core ADR-096) and other providers' tickets are counted as skipped,
never guessed at. It writes nothing, anywhere.

| Exit code | Meaning |
|---|---|
| 0 | Every checked reference exists. |
| 1 | The input was not a valid `--graph` payload. |
| 2 | Credentials missing from the environment. |
| 3 | One or more references are missing or forbidden — the CI gate. |

| Flag | Meaning |
|---|---|
| `--dry-run` | List the references and batches that would be checked; no client, no calls. |
| `--input`, `-i` | Read the `--graph` JSON from a file (default: stdin; `-` also means stdin). |
| `--verbose`, `-v` | Print per-reference results on a live verify too (findings always print). |

## `publish` — managed Confluence pages

Mirrors the `--documents` stream into one space, idempotent on the canonical
artifact id ([ADR-011](../../rac/decisions/adr-011-confluence-page-identity-and-idempotency.md)):

- **Page identity is a content property** (`lore.artifact_id`) plus a
`lore-managed` label — never the title, so artifact renames are ordinary
updates; and no page id is ever written back into the corpus (write-back
is propose-only via human PR, rac-core ADR-065).
- **Unchanged pages are skipped without a write.** The property stores a
sha256 of the rendered body; a second publish over an unchanged corpus
performs zero writes.
- **Conflicts are surfaced, never clobbered.** Updates send
`version + 1`; a 409 means a human edited the page — it is reported as a
skip and left alone.
- **Rendering is deterministic and escape-first.** A small Markdown subset
(headings, paragraphs, emphasis, code, fenced blocks, flat lists, links)
becomes storage format; corpus content is untrusted input, so hostile
HTML/macro text stays inert and only `http`/`https`/`mailto` links become
anchors. Tables are not yet rendered (they degrade to escaped text).

| Flag | Meaning |
|---|---|
| `--space` | Confluence space key (default: `ATLASSIAN_CONFLUENCE_SPACE`). |
| `--dry-run` | Print the pages that would be upserted; no client, no calls. |
| `--input`, `-i` | Read JSONL from a file (default: stdin; `-` also means stdin). |
| `--strict` | Fail on a malformed line instead of skipping it. |
| `--verbose`, `-v` | Print per-page actions on a live publish too. |

Exit codes are the standard 0 (done) / 1 (malformed input) / 2 (missing
credentials or space).

## Auth

API token + Basic auth against Atlassian Cloud — mint a token at
id.atlassian.com and set the three `ATLASSIAN_*` variables; one credential
serves Jira and Confluence. Tokens expire; rotate them like any secret.
Data Center (Bearer PAT, v2 Jira endpoints), OAuth, inbound Confluence
ingest, and Jira comment-mode are named deferrals on the
[`atlassian-connector`](../../rac/roadmaps/atlassian-connector.md) roadmap.

### Python API

```python
from rac_connectors import parse_documents, parse_graph
from rac_connectors.atlassian import (
AtlassianPublisher,
AtlassianVerifier,
client_from_env,
)

client = client_from_env()
report = AtlassianVerifier(client).verify(parse_graph(open("graph.json").read()))
summary = AtlassianPublisher(client, space_key="DOCS").publish(
parse_documents(open("corpus.jsonl"))
)
```

### Live smoke test

The connector is wired and unit-tested against fakes and a mock transport,
but the live path (a real Cloud site) is unproven until someone runs it —
this page is `drafted (live run pending)`. To validate end to end:

1. **Configure the environment** with a real site, account, and API token
(all four variables above; pick a scratch Confluence space).
2. **Verify, dry-run first:**
`rac export rac/ --graph | rac-connect atlassian verify --dry-run`, then
live. With a corpus referencing one known-good and one deleted issue,
confirm the exists/missing split and exit code 3.
3. **Publish twice into the scratch space:**
`rac export rac/ --documents | rac-connect atlassian publish`. First run
creates every page (property + `lore-managed` label set); the second run
must report all pages `unchanged` and perform **zero writes**.
4. **Rename check:** change one artifact's title, re-publish, and confirm
the same page updates in place (no duplicate).
5. **Conflict check:** hand-edit a managed page in Confluence, re-publish a
changed body for that artifact, and confirm the run reports a version
conflict and leaves the human edit alone.
6. **429 behaviour** (optional): run against a busy site and confirm
retries honour `Retry-After` rather than hammering.

Then flip this page's `status` to `shipped` — and only then consider a
release tag (the gate recorded on itsthelore/rac-connectors#10).
6 changes: 6 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ cognee = ["cognee>=1,<2"]
# fake-driven test-suite stay dependency-free. Embeddings come from an external
# endpoint over stdlib HTTP (no SDK), so only qdrant-client is required here.
qdrant = ["qdrant-client>=1.7,<2"]
# The live Atlassian verify/publish needs httpx (an internal client, no
# Atlassian SDK — ADR-010); the core install and the fake-driven test-suite
# stay dependency-free. CI installs this extra so the mock-transport wire
# tests run; they importorskip httpx and pass silently without it.
atlassian = ["httpx>=0.27,<1"]
dev = ["pytest>=7.0", "ruff", "mypy"]

[project.scripts]
Expand Down Expand Up @@ -96,5 +101,6 @@ module = [
"letta_client.*",
"cognee.*",
"qdrant_client.*",
"httpx.*",
]
ignore_missing_imports = true
Loading