Contributing
Contributing
Section titled “Contributing”Guidelines for contributing to atlcli.
Development Setup
Section titled “Development Setup”Prerequisites
Section titled “Prerequisites”- Bun 1.3.14 (the version declared in
package.json) - Node.js 22.12+ (for documentation tooling)
- An Atlassian Cloud account for testing
Clone and Build
Section titled “Clone and Build”git clone https://github.com/BjoernSchotte/atlcli.gitcd atlclibun installbun run buildRun Tests
Section titled “Run Tests”bun run testAlways use the root script. It enables the development export condition so
workspace imports resolve to live source instead of stale dist/ output.
CI cadence
Section titled “CI cadence”CI classifies changed paths before starting expensive jobs. Documentation-only changes can stay lightweight; product, workflow, dependency, and unknown changes deliberately fail open to the complete product proof. The always-present required job aggregates the selected results and is the single check that branch protection should require.
The required test topology remains the legacy four-shard suite while three
duration-aware candidates collect same-SHA comparison evidence:
general-2x1, general-3x1, and general-2x2-workers. Worker count is fixed
at one or two; CI never uses global --concurrent. Real Typst/PDF and
package-contract tests are explicit serial lanes, and stateful isolation probes
remain serial.
| Cadence | Gates |
|---|---|
| Pull request | Selected product/platform gates, pinned consumer smoke, and documentation build |
Push to main |
Affected product/platform gates, security attestation, and documentation deployment when relevant |
| Daily | Blocking M1 cross-host acceptance, performance trend, and floating-Bun consumer canary |
| Weekly | Full unfiltered CI matrix, one rotating topology comparison, and a non-required system-Chrome compatibility signal |
| Release tag | Shared SHA-bound quality preflight and attestation before binary publication |
Superseded pull-request runs and Pages deployments are cancelled. Product CI
on main, nightly runs, and release evidence are never cancelled by a newer
commit.
Draft pull requests currently receive the same required product proof as Ready
pull requests. The proposed draft-fast mode is not active until its
live-state and affected-test promotion gates have passed. Before marking a PR
Ready, finish draft commits and synchronize main; do not toggle Ready merely
to restart CI.
Run the complete local suite with:
bun run testStart CI topology comparison manually in GitHub Actions to compare one
duration-aware candidate with all four legacy shards on the same SHA. The
workflow is non-required and cannot replace required. Timing JSON is
available from the ci-timing-<attempt>-<sha> artifact and the run summary.
The system Google Chrome job is also a compatibility canary only; required
packed MV3 proof continues to use Playwright-matched Chromium.
Use Re-run failed jobs only after a failure has been classified as infrastructure-related. Product failures and a second failure after the narrow Bun file-link retry require diagnosis.
README media
Section titled “README media”Store repository-owned screenshots and downloadable PDF references used by the
root README.md under assets/readme/. Keep each file below 10 MiB and the
referenced set below 25 MiB. PNG images must have non-zero dimensions no larger
than 4096×4096; PDFs may contain at most 20 detectable pages.
Run the lightweight validation before committing README presentation changes:
bun run check:readme-mediaThe check rejects missing or untracked files, unsupported local image formats,
invalid PNG/PDF headers, and media over the configured limits. Other assets/
subdirectories remain product or unknown surfaces and do not inherit this
documentation-only CI policy.
Project Structure
Section titled “Project Structure”atlcli/├── apps/│ └── cli/ # CLI application│ └── src/│ ├── commands/ # Command handlers│ └── index.ts # Entry point├── packages/│ ├── core/ # Shared utilities│ ├── confluence/ # Confluence API client│ └── jira/ # Jira API client├── docs/ # Documentation (this site)└── spec/ # Internal specs and roadmapsE2E Resources
Section titled “E2E Resources”Live end-to-end tests create real pages and issues in an Atlassian tenant.
These rules keep that tenant clean and make every resource’s ownership
decidable. Live tenant tests are operator-controlled and local-only; GitHub
Actions does not receive Atlassian credentials or run them remotely. They are
the concrete form of the “clean up test resources” rule in CLAUDE.md.
Helpers live in apps/cli/src/e2e/.
Run live cases only against an explicitly selected sandbox or test tenant. Keep its profile and fixture IDs in the local environment; do not add them as GitHub repository secrets or variables.
Naming convention
Section titled “Naming convention”Every live E2E resource is named atlcli-e2e-<feature>-<timestamp>, where
<feature> is a lowercase dash-separated slug and <timestamp> is epoch
seconds:
atlcli-e2e-scope-tree-1789000000Confluence pages go only in space DOCSY; Jira issues go only in
project ATLCLI (the summary carries the prefix). Build the name with
makeE2eTitle(feature) rather than by hand — it validates the slug, because an
off-convention name is one the sweeper can never recover.
The run-id ownership marker
Section titled “The run-id ownership marker”A title prefix is not proof of ownership. A real user page can share the name, and two E2E runs can race inside the same second. So at creation every page also gets a content property and every issue an issue property:
| Property key | Value |
|---|---|
atlcli-e2e-run-id |
The CI run ID (gha-<run>-<attempt>), or a local UUID |
That property, not the name, is what any deletion path checks. Anything without it is treated as someone else’s content and is never deleted.
Clean up in finally — every run, not every night
Section titled “Clean up in finally — every run, not every night”Each test records what it creates and deletes it in a finally block, so the
tenant is clean after every single run. Use withE2eResources, which does the
tracking, the marker stamping and the finally for you:
import { withE2eResources } from "../e2e/resources.js";import { createConfluencePort } from "../e2e/rest-ports.js";
await withE2eResources({ confluence: createConfluencePort(profile) }, async (t) => { const page = await t.createPage("scope-tree"); // named + marked + tracked await runCli(["wiki", "export", page.id]); // deleted on the way out, including when this body throws});Use t.trackPage(id) / t.trackIssue(key) for resources the CLI under test
created, so they are deleted too.
The sweeper is recovery, not cleanup
Section titled “The sweeper is recovery, not cleanup”apps/cli/src/e2e/cleanup.ts exists for the runs that could not clean up after
themselves — a crashed process, a cancelled CI job. It is not the primary
mechanism, and a test that relies on it is a broken test.
bun apps/cli/src/e2e/cleanup.ts # dry run: lists what it would deletebun apps/cli/src/e2e/cleanup.ts --force # actually deletesIt deletes a resource only when all four hold:
- It carries the
atlcli-e2e-run-idmarker. - Its name matches
atlcli-e2e-<feature>-<timestamp>. - It is older than 24 h — so a running E2E is never swept out from under itself.
- It lives in space
DOCSY/ projectATLCLI.
Every gate is re-checked immediately before each delete, not just when the plan is built.
Options
Section titled “Options”| Flag | Default | Constraint |
|---|---|---|
--force |
off | Without it, nothing is deleted |
--profile <name> |
mayflower |
Selects the tenant — see the warning below |
--ttl-hours <n> |
24 |
May only be raised. Values below 1 are rejected: the TTL gate cannot be switched off |
--max-deletes <n> |
50 |
May only be lowered. 50 is a hard ceiling, not a default |
The two directional limits are deliberate. --ttl-hours 0 would delete a page a
different, still-running E2E created seconds ago — the exact thing the TTL
exists to prevent. And raising --max-deletes is the obvious reflex right after
seeing an abort, which is precisely the moment a bad query is the likeliest
explanation.
Listings are fully paginated: a short result page carrying a live next-cursor is not the last page.
DOCSY also holds deliberately retained fixtures (the DOCX feature zoo, the
spec-005 logo/image page, the “M1 Abnahme …” set). What protects them is the
naming gate: their titles do not parse as atlcli-e2e-<feature>-<timestamp>,
so they are rejected even if something stamps a valid-looking marker on them.
That is a structural property of their names, not the contingent fact that they
happen to carry no marker today.
Remote CI policy
Section titled “Remote CI policy”GitHub Actions does not run live Atlassian E2E cases or the recovery sweeper.
The ordinary test suite keeps these cases disabled unless ATLCLI_E2E=1 is
set locally. This avoids storing a tenant profile in remote CI and prevents
accidental requests against a non-sandbox instance.
Coding Standards
Section titled “Coding Standards”TypeScript
Section titled “TypeScript”- Use strict TypeScript settings
- Prefer explicit types over
any - Use interfaces for public APIs
Formatting
Section titled “Formatting”The project uses Biome for linting and formatting:
bun run lintbun run formatCommits
Section titled “Commits”Follow Conventional Commits:
feat(jira): add worklog timer modefix(confluence): handle empty pagesdocs: update authentication guidePull Requests
Section titled “Pull Requests”- Fork the repository
- Create a feature branch (
git checkout -b feat/my-feature) - Make your changes
- Run tests (
bun test) - Commit with conventional commit message
- Push and open a PR
PR Guidelines
Section titled “PR Guidelines”- Keep PRs focused on a single change
- Include tests for new functionality
- Update documentation if needed
- Ensure CI passes
Architecture
Section titled “Architecture”Monorepo Structure
Section titled “Monorepo Structure”atlcli uses a monorepo with:
apps/cli- The CLI applicationpackages/*- Shared libraries
API Clients
Section titled “API Clients”Each Atlassian product has its own package:
@atlcli/confluence- Confluence REST API@atlcli/jira- Jira REST API
Clients handle authentication, request/response, and error handling.
Command Pattern
Section titled “Command Pattern”Commands follow a consistent pattern:
async function handleCommand(args: string[], flags: Flags, opts: Options): Promise<void> { // 1. Parse and validate input // 2. Call API client // 3. Format and output result}Releasing
Section titled “Releasing”Releases are automated via the release script:
bun scripts/release.ts patch # 0.16.0 → 0.16.1bun scripts/release.ts minor # 0.16.0 → 0.17.0bun scripts/release.ts major # 0.16.0 → 1.0.0What the Release Script Does
Section titled “What the Release Script Does”- Validates clean working directory and main branch
- Runs tests and type checking
- Bumps version in
package.json - Generates the changelog with git-cliff, crediting contributors and issue reporters
- Creates commit and tag
- Pushes to origin (triggers GitHub release workflow)
- Waits for the SHA-bound CI preflight and security attestation
- Waits for release artifacts
- Triggers Homebrew tap update
Options
Section titled “Options”--dry-run- Print the release plan and exit; makes no changes--preview- Render the changelog entry (including the Thanks section) and exit; makes no changes--skip-tests- Skip test step (use with caution)
Prerequisites
Section titled “Prerequisites”- GitHub CLI authenticated (
gh auth login) - On main branch with clean working directory
- Security review completed for this release — see below
Security Review Before Every Release
Section titled “Security Review Before Every Release”Run /security-review over the diff since the previous tag and confirm it is
clean before you cut a release. bun scripts/release.ts <type> --dry-run prints
this as a reminder checklist item; the script does not block on it, so the
confirmation is yours to make.
The review covers the untrusted-input surfaces plus anything new that talks to the network:
| Surface | What to check | Where it lives |
|---|---|---|
Raw .docx template upload |
Archive budget (entry count, declared uncompressed size), entry-name policy, active-content rejection | packages/docx/src/scan.ts |
.wiki-pdf-template container |
Path traversal, symlinks, per-file and cumulative size caps | packages/template-pack/src/unpack.ts |
| Embedded SVG | Script/foreignObject/on*/external-reference rejection |
packages/confluence/src/svg-safety.ts |
| Confluence storage parsing | Node-count, nesting-depth and text-length budget | packages/confluence/src/export-blocks.ts |
| Link targets | Scheme allowlist (http, https, mailto, relative only) |
packages/confluence/src/link-safety.ts |
| Fonts intake | sha256 manifest, sfnt magic bytes, per-font size cap | packages/pdf/scripts/ensure-fonts.ts |
| New network code | Any fetch added since the last tag: is the target host derived from user input? |
anywhere |
Example: Preview a Release
Section titled “Example: Preview a Release”Both --dry-run and --preview print and exit without changing anything, so no rollback is needed:
# Show the step-by-step release planbun scripts/release.ts minor --dry-run
# Render the changelog entry, including the Thanks sectionbun scripts/release.ts minor --previewReporting Issues
Section titled “Reporting Issues”Use GitHub Issues for:
- Bug reports
- Feature requests
- Questions
Include:
- atlcli version (
atlcli --version) - Steps to reproduce
- Expected vs actual behavior
- Relevant error messages
License
Section titled “License”atlcli is MIT licensed. By contributing, you agree your contributions will be under the same license.
Related Topics
Section titled “Related Topics”- Getting Started - Installation and setup
- Creating Plugins - Extend atlcli with plugins