Skip to main content

Ansible dependency graph CLI built on the untaped SDK.

Project description

untaped-ansible

untaped-ansible is a standalone Ansible dependency graph CLI built on the untaped SDK. It provides role/playbook dependency graphs, upstream impact analysis, and Git-backed source caches, plus the shared config, profile, and skills command groups every untaped tool ships.

Install

uv tool install untaped-ansible

untaped-ansible reads GitHub credentials from the shared github config section (provided by untaped-github, which it depends on for the public GitHub client API used by source refreshes):

untaped-ansible config set github.token ghp_xxx

untaped-ansible also ships the untaped-ansible agent skill, installed via its skills command group:

untaped-ansible skills install

Commands

untaped-ansible graph TARGET --downstream
untaped-ansible graph TARGET --org acme --team platform --upstream --refresh
untaped-ansible graph TARGET --source platform --upstream --cached
untaped-ansible graph TARGET --source platform --source ops --upstream --cached
untaped-ansible graph TARGET --source platform --upstream
untaped-ansible graph TARGET --source platform --downstream --live
untaped-ansible graph TARGET --source platform --both --refresh --concurrency 12
untaped-ansible graph TARGET --source platform --both --refresh --backend git
untaped-ansible graph TARGET --source platform --both --format mermaid --output deps.mmd
untaped-ansible source save platform --org acme --team platform
untaped-ansible source refresh platform --concurrency 12
untaped-ansible source refresh platform --backend graphql
untaped-ansible source status platform
untaped-ansible alias add common acme/common
untaped-ansible config|profile|skills ...

graph uses tree output by default and also supports mermaid and json. Local targets infer owner/repo from the checkout's GitHub remote, with --target-repo owner/name available as an override. --ref selects the branch, tag, or SHA used for live dependency reads and cached upstream lookups.

Graph JSON includes stable edge IDs and detected cycles. Each edge carries id: "edge:<digest>", where the digest is the first 16 hex characters of sha256(relation + NUL + source_id + NUL + target_id). That ID is topological: duplicate physical declarations for the same relation/source/target collapse to one representative graph edge. cycles contains records with kind, relation, node_ids, and edge_ids, detected separately for downstream requires and upstream impacts edges. kind: "cycle" records use a closed ordered node_ids path and matching path edge IDs; kind: "scc_group" records use a sorted open node set and sorted internal SCC edge IDs when a component has too many elementary cycles to enumerate deterministically. Cycle detection runs only on the graph that was emitted, so --depth can hide a longer cycle whose closing edge is outside the traversal horizon. Mermaid output emits every edge in the graph and includes cycle/group comments when cycles are known. Tree output keeps rendering cycles as path-local (cycle) markers.

Use relationship flags in user terms:

  • --downstream: dependencies used by the target.
  • --upstream: repos/roles/playbooks that depend on the target.
  • --both: both directions; this is the default when no direction flag is passed.

--upstream, --downstream, and --both are mutually exclusive, as are --refresh, --cached, and --live; combining flags from either group is a usage error caught at parse time.

Tree output renders each direction as a nested traversal. Each populated direction starts with the target node for that direction, then continues to its children. When the graph target omits --ref and cached or live data contains multiple matching refs, those refs are shown separately as target@ref roots. A repo/ref that appears in both directions is rendered in both sections. In tree output, refs are ordered for scanning: branches first with the repo's default branch first, then tags in newest-first semantic-version order, then refs without cached branch/tag metadata.

Downstream graphs do not require a source or cached data. Local targets are read from disk. GitHub URL and owner/repo targets read declared dependencies live from GitHub when no source is configured. When --source or inline source selectors are present, graph is cache-first: it reads only completed SQLite source data unless --refresh is passed. --cached is still accepted as an explicit cache-only mode, and --live opts downstream traversal back into live GitHub reads. ansible.freshness_ttl is deprecated and no longer affects graph defaults; use --refresh or untaped-ansible source refresh NAME when remote data should be checked.

Upstream graphs are source-backed because GitHub impact analysis needs a search boundary. Repeat --source to union multiple saved source caches in one graph. If a selected source has no completed baseline, graph fails with the exact refresh command instead of rendering partial upstream output. Use inline selectors for one-off work and pass --refresh when the inline cache needs to be created or updated:

untaped-ansible graph acme/base --org acme --team platform --upstream --refresh

Git-backed source indexing is the default. The first source-backed run creates bare repositories under ansible.repo_cache_path (~/.untaped/ansible-repositories by default), fetches only the selected refs, and reads dependency files from Git objects without checking out worktrees. Later runs fetch/check the same ref set and reparse only refs whose SHA, tag target, or dependency path configuration changed. Source-backed graphing uses that completed cache by default; run untaped-ansible source refresh NAME or pass graph --refresh when you want remote checks.

A refresh runs in repo-level batches. Org, team, and repo expansion is resolved through the public untaped-github inventory API, with explicit repo metadata winning over org/team listings. Ref probing is GraphQL-primary by default (ansible.source_refresh_backend: auto): GraphQL handles the happy path and Git ls-remote is used only for recoverable per-repo GraphQL probe failures or primary GraphQL rate-limit exhaustion. Set ansible.source_refresh_backend or pass --backend auto|graphql|git to source refresh; graph --refresh accepts the same --backend override, and graph --backend without --refresh is a usage error. The git backend still uses GitHub REST inventory expansion and still needs GitHub credentials for private sources; it only replaces the ref probe transport.

GraphQL probe results carry GraphQL cost, remaining budget, and reset metadata. All-ref GraphQL chunks use 50 repos, while default-branch-only GraphQL chunks use 100 repos. Git ls-remote probing runs one subprocess per repo, uses git ls-remote --symref (Git 2.8+), the same HTTPS auth-header redaction path as fetches, the normal 60-second Git timeout, and ansible.probe_concurrency for concurrency. Large Git fallback runs can be much slower than GraphQL batches; when fallback activates, a warning with the repo count and reason is printed to stderr. Git fetches still only repos whose selected refs changed, with per-repo concurrency bounded by ansible.git_fetch_concurrency (default 8, accepts 1..32). Refresh batches use ansible.source_refresh_repo_batch_size (default 100). untaped-ansible graph --refresh and untaped-ansible source refresh both accept --concurrency N as a per-run override for fetch/parse work. Refresh progress is reported on stderr with repo/ref counts, changed and unchanged refs, edge count, elapsed time, and the Git concurrency used.

Dependency files that cannot be parsed are skipped with an explicit warning instead of failing the whole repo. Empty files, whitespace-only files, and ----only YAML documents stay warning-free. Missing, null, and empty dependencies, roles, and collections sections also stay warning-free; present non-list values warn and are skipped. Malformed YAML, templated YAML that is not valid YAML, or recognized dependency files with an unsupported shape produce warning: skipped PATH: REASON in local graph output and warning: skipped REPO@REF PATH: REASON in live graph output when a ref is known. During source refresh, the same condition is printed to stderr with the same repo/ref form when a ref is present; skipped files are included in the refresh result for that run but are not persisted into SQLite, so later cached graph output does not invent past parse warnings.

In auto mode, primary GraphQL rate-limit exhaustion falls back to Git ls-remote for the whole active probe target set. Secondary rate limiting, auth failures, request-level forbidden responses, and unknown global GraphQL access failures still abort immediately with one error: ... message. They are not expanded into per-repo failures, and source-backed graph exits instead of rendering stale cached output. A known limitation remains: if GitHub returns 200 OK with a per-repo FORBIDDEN error for every alias, that still reports as per-repo missing/inaccessible rather than a global SSO or token-scope failure.

GraphQL and Git probes record the same SHA for branches, lightweight tags, annotated tags, and tags-of-tags. GraphQL currently peels annotated tags up to two levels; Git ls-remote exposes fully peeled ^{} targets for deeper annotated-tag chains. Rare 3+ level tag chains can therefore look changed if a run switches between GraphQL and Git fallback. Normalizing those deeper chains is future work.

Per-repo failures do not abort a refresh: successful repos are committed, each failure is listed on stderr, and source refresh exits 1. When every repo fails, nothing is committed and the index is left untouched. Transient GraphQL ref-probe failures in explicit graphql mode print an additional hint to rerun the same source refresh command; unchanged repos skip Git fetch and dependency scan work on the rerun. In auto mode, transient GraphQL per-repo failures first fall back to Git ls-remote; only unrecovered repos remain in the failure list.

If the GraphQL budget drops below ansible.source_refresh_rate_limit_floor (default 500) during a large refresh, successful repos are committed, untouched repos and refs are preserved, the source-wide completed timestamp is not updated, and the command exits 1 with a resume hint. Re-running the same source refresh skips previously successful repos and retries failed or unprocessed repos. If a resumed queue is exhausted after prior success, the source can be marked complete even when the last batch has per-repo failures; the CLI still exits 1 and lists those failures. Removed repos/refs are pruned only after the expanded repo queue is exhausted without a budget stop.

Source refreshes scan all branches and all tags by default (ansible.ref_scan_default: all). Set ansible.ref_scan_default: default_branch when runtime matters more than broad upstream coverage, or set --ref-scan-default default_branch on an individual saved or inline source. The default-branch strategy uses a cheaper GitHub query that does not request the refs(...) connection. --ref-pattern narrows source refs, so --ref-pattern v3 scans matching branches and tags unless --ref-kind is also provided. --ref-kind tags without a pattern scans all tags. Patterns such as --ref-pattern 'release/*' filter the probed refs client-side; refs that changed are then fetched with exact Git refspecs. The same inline selector set is cached under a deterministic fingerprint, so later identical commands reuse the same SQLite source key.

Source-backed downstream traversal is strict about refs. If the graph needs repo@v1 and the source cache only has repo@main, graph stops at that node and warns instead of silently falling back. Available-ref warnings use the same branch/tag ordering as tree output. Scan the matching branch/tag, use --cached only when the existing SQLite index is known to be complete, or pass --live when you explicitly want downstream dependencies read from GitHub.

Save a reusable source for repeated work or CI:

untaped-ansible source save platform --org acme --team platform
untaped-ansible source refresh platform
untaped-ansible graph acme/base --source platform --upstream

Use ansible.git_clone_protocol: ssh when normal SSH keys are preferred over HTTPS token auth. HTTPS mode passes the configured GitHub token to Git as a transient auth header and does not store it in cached remotes.

The SQLite index enforces a schema version through PRAGMA user_version. There are no migrations: when an existing index was created by a different tool version, commands fail with an error naming the exact ansible.index_path file to delete and the untaped-ansible source refresh NAME command to run afterwards.

source status NAME reports whether a configured source has never been refreshed, is stale, or is fresh. Unknown source names return an error so typos do not look like missing cache data.

When a source has exactly one --org, --team accepts a bare team slug and stores it as org/slug. Use --team org/slug when a source has no org or multiple orgs.

Development

uv sync
uv run pytest
uv run mypy
uv run ruff check --fix
uv run ruff format
uv run untaped-ansible --help

See AGENTS.md for architecture rules and dependency graph contracts.

Security

Please report suspected vulnerabilities privately. See SECURITY.md.

Contributing

See CONTRIBUTING.md and AGENTS.md for the local workflow, architecture rules, and dependency graph contracts.

License

MIT. See LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

untaped_ansible-0.12.0.tar.gz (61.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

untaped_ansible-0.12.0-py3-none-any.whl (80.3 kB view details)

Uploaded Python 3

File details

Details for the file untaped_ansible-0.12.0.tar.gz.

File metadata

  • Download URL: untaped_ansible-0.12.0.tar.gz
  • Upload date:
  • Size: 61.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for untaped_ansible-0.12.0.tar.gz
Algorithm Hash digest
SHA256 762242d46ed26654817aa184cdbf422d4bafb579f441d007d1132e39f51a0863
MD5 991f27c98d970a2228edb79d860bee06
BLAKE2b-256 3ffcb47952f3f1567de19d980649e9cf910dd4781674a1c3933876aee880094a

See more details on using hashes here.

Provenance

The following attestation bundles were made for untaped_ansible-0.12.0.tar.gz:

Publisher: release.yml on alexisbeaulieu97/untaped-ansible

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file untaped_ansible-0.12.0-py3-none-any.whl.

File metadata

File hashes

Hashes for untaped_ansible-0.12.0-py3-none-any.whl
Algorithm Hash digest
SHA256 bc409f075ad9f02fc955f360906b01fa14caed097ce074ad95bb90f9f4ffa549
MD5 cdb3fc351d2b08af78f1f81989c38c42
BLAKE2b-256 f5c591aac94ffd607fa67ee39b32bf06d8b1e960559a86283373542f1bfb108b

See more details on using hashes here.

Provenance

The following attestation bundles were made for untaped_ansible-0.12.0-py3-none-any.whl:

Publisher: release.yml on alexisbeaulieu97/untaped-ansible

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page