| title | Code Generation |
|---|---|
| description | How atla generates Rust API clients from Atlassian OpenAPI specs using progenitor. |
atla generates type-safe Rust API clients at compile time using
progenitor, a Rust-native OpenAPI code generator.
Normal Cargo builds consume checked-in partial specs and require no Java, Node.js, or Python
code-generation runtime. Refreshing those specs separately requires curl, Node.js, and Python 3.
Atlassian CDN (upstream specs)
|
v
update-specs.sh Download upstream OpenAPI JSON
|
v
JS filter scripts Filter/patch specs to partial subsets
|
v
specs/*.json Checked-in spec files
|
v
build.rs (progenitor) Generate Rust code at compile time
|
v
$OUT_DIR/codegen.rs Included via include!() in lib.rs
| Crate | Spec file | Upstream source | Filter script |
|---|---|---|---|
atla-jira-api |
specs/jira-v3-partial.json |
Jira Cloud v3 | scripts/jira-v3-partial-spec.js |
atla-confluence-api |
specs/confluence-v2-partial.json |
Confluence Cloud v2 | scripts/confluence-v2-partial-spec.js |
atla-confluence-v1-api |
specs/confluence-v1-partial.json |
Confluence Cloud v1 | scripts/confluence-v1-partial-spec.js |
Each crate contains only three hand-maintained files:
Cargo.toml— declares build-dependencies on progenitor and runtime dependencies on progenitor-clientbuild.rs— reads the spec and invokes progenitor to generate$OUT_DIR/codegen.rssrc/lib.rs— includescodegen.rsfor normal builds; skips the generated documentation examples undercfg(doctest)because Progenitor emits raw URL/JSON fragments that are not valid Rust doctests.
All API client code is generated at compile time. There are no hand-maintained API modules.
The generated clients' transport behavior is exercised through atla-core contract tests, so
cargo test --doc --workspace remains a valid workspace-wide gate without editing OUT_DIR.
All three crates follow the same pattern:
use progenitor::{Generator, GenerationSettings, InterfaceStyle};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let src = "../../specs/<spec-file>.json";
println!("cargo:rerun-if-changed={src}");
let file = std::fs::File::open(src)?;
let spec: openapiv3::OpenAPI = serde_json::from_reader(file)?;
let mut settings = GenerationSettings::default();
settings
.with_interface(InterfaceStyle::Builder)
.with_derive("PartialEq");
let mut generator = Generator::new(&settings);
let tokens = generator.generate_tokens(&spec)
.map_err(|error| std::io::Error::other(format!("generation failed: {error:?}")))?;
let content = prettyplease::unparse(&syn::parse2(tokens)?);
let out_path = std::path::Path::new(&std::env::var("OUT_DIR")?).join("codegen.rs");
std::fs::write(out_path, content)?;
Ok(())
}Key points:
cargo:rerun-if-changedensures the crate only rebuilds when the spec file changesInterfaceStyle::Buildergenerates builder-pattern methods:client.create_issue().body(body).send().awaitprettypleaseformats the generated code for readable compiler errors
[build-dependencies]
progenitor = "0.14"
serde_json = "1"
openapiv3 = "2"
syn = { version = "2", features = ["full"] }
prettyplease = "0.2"[dependencies]
progenitor-client = "0.14"
reqwest.workspace = true
serde.workspace = true
serde_json.workspace = trueSome crates require additional dependencies (chrono, uuid, serde_repr,
etc.) depending on the spec's schema types.
Cargo already fingerprints each partial spec through cargo:rerun-if-changed,
so generated clients are reused until a spec or generator input changes. To
reuse that build output across worktrees and fresh checkouts, use the opt-in
shared target cache:
scripts/check-fast.shThis runs cargo check -p atla and defaults CARGO_TARGET_DIR to
$XDG_CACHE_HOME/atla/cargo-target (or ~/.cache/atla/cargo-target). Set
ATLA_BUILD_CACHE_DIR to choose another cache root, or set
CARGO_TARGET_DIR directly. Extra Cargo arguments are forwarded, for example
scripts/check-fast.sh --all-targets.
The cache never replaces release validation: run the full workspace fmt, Clippy, and test commands before opening a PR. Delete the cache directory if disk use or a suspected stale local artifact needs to be ruled out.
A local reference measurement on 2026-07-20 used cargo check -p atla, Rust
1.97.0, macOS arm64, and an Apple M2 Max:
| Target state | Wall time |
|---|---|
| Empty isolated target directory | 35.66 s |
| Same target, no source changes | 0.60 s |
These numbers are a baseline, not a performance guarantee. Reproduce the clean and warm checks with:
target_dir="$(mktemp -d)"
CARGO_TARGET_DIR="$target_dir" /usr/bin/time -p cargo check -p atla
CARGO_TARGET_DIR="$target_dir" /usr/bin/time -p cargo check -p atla
rm -rf "$target_dir"Full Atlassian specs are very large. The Jira v3 spec alone produces around 1,000 files and 12 MB of Rust code. Filtering to only the endpoints atla uses keeps compile times manageable.
Filters the full Jira Cloud v3 spec to include only:
- Issues: create, search, get, update, delete, transitions
- Comments: list, create, get, update, delete
- Projects: search, get
- Issue types, attachments, issue links
Also provides simplified schemas for complex types (CreatedIssue, IssueUpdateDetails,
Transitions, etc.) to avoid pulling in the entire Jira type graph. The simplified Project
schema deliberately keeps projectTypeKey open-ended because Atlassian returns values missing from
its published enum; the filter applies this invariant automatically.
Jira Software Agile endpoints (boards, sprints) are not part of the Jira platform v3 spec. atla calls those endpoints directly via raw reqwest calls in atla-core.
Filters the Confluence v1 spec and applies compatibility patches:
- Paths included: content search, general search, user search, space info, and content labels.
- Unsupported query parameters are removed.
- Simplified schemas are provided for
Content,SearchResult,Space, and related response models.
Attachment uploads are deliberately excluded from the generated client because
progenitor does not support multipart/form-data. atla sends them through its
raw reqwest multipart path while reusing the generated v1 response model. Page
label mutation also uses Confluence v1 raw REST calls because the v2 API does
not expose label add/remove endpoints.
Filters the upstream v2 spec to the operations used by atla, then follows their
$ref closure so all required schemas remain available. The filter also
applies the documented enum and malformed-upstream-schema repairs from
specs/PATCHES.md. When core starts calling a new generated v2 operation, add
its snake_case operation name to usedOperations and refresh the specs.
.github/workflows/spec-refresh.yml runs weekly and on manual dispatch. It
executes the update script, verifies fmt/check/workspace tests, and opens a
review PR containing only specs/** changes; it never pushes directly to
main. scripts/spec-diff-summary.py adds per-spec line/size/hash totals, operation-ID/schema-count
deltas, and normalized parameter/request/response/schema contract facts to the PR body and
workflow summary. This exposes nested field, requiredness, enum, type, and default changes that
counts alone miss. Review every invariant in specs/PATCHES.md and all contract diffs before
merging.
The local refresh requires curl, Node.js, and Python 3 (mise install provisions the pinned
Node.js and Python versions). scripts/update-specs.sh handles the full refresh cycle:
- Downloads upstream specs from Atlassian CDN
- Runs JS filters that produce partial specs and apply every invariant in
specs/PATCHES.md - Updates
specs/manifest.jsonwith SHA-256 hashes and metadata
scripts/update-specs.sh
python3 -m unittest discover -s scripts/tests -p 'test_*.py'
cargo check --workspaceTracks integrity and provenance for each spec:
- Source file path and SHA256 hash
- Upstream URL and SHA256 hash
- Filter script path (if applicable)
- Generator tool and version metadata
| Spec | URL |
|---|---|
| Jira v3 | https://dac-static.atlassian.com/cloud/jira/platform/swagger-v3.v3.json |
| Confluence v2 | https://dac-static.atlassian.com/cloud/confluence/openapi-v2.v3.json |
| Confluence v1 | https://dac-static.atlassian.com/cloud/confluence/swagger.v3.json |
progenitor generates a Client struct with builder-pattern methods. Core constructs it with the
shared AtlassianClient::authed_http_client() so credentials, timeouts, redirect protection, and
retry ownership stay centralized. The generated client is immediately sealed inside
GeneratedTransport; domain adapters can execute a request but cannot retrieve the client:
let client = atla_jira_api::Client::new_with_client(
&base_url,
raw_client.authed_http_client(),
);
let transport = GeneratedTransport::new(client);
let result = transport
.execute(reqwest::Method::POST, move |generated| {
let body = body.clone();
async move { generated.create_issue().body(body).send().await }
})
.await?;
let issue = result.into_inner();Do not expose the generated client or call a builder outside GeneratedTransport::execute. The
transport applies bounded, method-aware retry with exponential backoff and Retry-After, reads
final API error bodies, retries an explicit 429 rejection for any method, and otherwise never
repeats a non-idempotent mutation. Progenitor itself has no auth fields; Basic auth remains in the
shared reqwest client.
To expose a new Jira or Confluence endpoint in atla:
- Identify the endpoint in the upstream spec (check the downloaded spec in
specs/) - Add the operation to the relevant JS filter script (for Confluence v2, add its snake_case
operation name to
usedOperations; the Jira/v1 scripts select paths) - Re-run filtering:
scripts/update-specs.sh - Verify:
cargo check --workspace - Consume the new generated method in
atla-core
For endpoints not covered by any upstream spec (e.g. Jira Software Agile REST API), implement them directly in atla-core using raw reqwest calls.
- Getting Started — installation and first-time setup
- Agent Reference — complete command reference for automation