taiga-cli

module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 1, 2026 License: Apache-2.0

README

taiga-cli

What it is

taiga is a command line client for the Taiga REST API v1, built for people and for coding agents. It resolves the Taiga URL and project from flags, environment, a .taiga.toml in the repository or the user config, keeps a session cache so agents never handle passwords, and reports every failure as a stable JSON error envelope with an exit code.

taiga api reaches the whole API v1, including version-checked writes and transparent pagination, so anything without a curated command is still one call away. Licensed under Apache-2.0.

Install

go install github.com/BasisTI/taiga-cli/cmd/taiga@latest

Or download the tarball for your platform from GitHub Releases, together with SHA256SUMS, and check it:

sha256sum -c SHA256SUMS --ignore-missing
tar -xzf taiga_*_linux_amd64.tar.gz taiga && sudo install taiga /usr/local/bin/

Quickstart

taiga auth login --url https://taiga.example.com --username me
echo 'url = "https://taiga.example.com"'  > .taiga.toml
echo 'project = "my-project"'           >> .taiga.toml
taiga api GET users/me
taiga api GET userstories --query project=37 --paginate
taiga api PATCH userstories/123 --field comment="Deployed to staging" --auto-version

Stories

taiga story works on the user stories of the selected project (--project, TAIGA_PROJECT, .taiga.toml or the config). A story is named by its reference (REF, the number shown in the web UI), never by its internal id; story get --id takes the id and still checks the project.

Command What it does
taiga story list [--ref N] [--status S] [--assignee USER|me] [--epic REF] [--tag T]... [--search TEXT] [--closed[=false]] Every matching story, without manual paging. Repeated --tag must all match
taiga story get REF / taiga story get --id ID One story, with its web url; JSON output also carries custom_attributes (values with their own version)
taiga story create --subject S [--description-file F|-] [--status S] [--tag T]... [--swimlane L] [--assignee USER]... Create a story
taiga story update REF [--subject S] [--description-file F|-] [--append-description TEXT] [--status S] [--tag T]... [--add-tag T]... [--remove-tag T]... [--milestone M] [--swimlane L] [--add-assignee USER]... [--remove-assignee USER]... [--owner-assignee USER|--clear-owner-assignee] [--block NOTE|--unblock] Send only the fields that change, with the story version
taiga story close REF [--status S] Move to a closed status; never archives or deletes
taiga story field list REF The story's custom field values next to their definitions
taiga story field set REF ["Name=value"]... [--unset NAME]... Merge custom field values: named fields change, the others stay
taiga story comment REF --body TEXT|--body-file F|- Publish a comment (Markdown); sent once, never repeated
taiga story comments REF [--include-system] The story's comments, newest first
  • Statuses, milestones and swimlanes take a name or an id of the project; users take an exact username, an id or me, and must be project members. A name used twice is ambiguous_name.
  • --tag replaces the tags; --add-tag/--remove-tag merge with the current ones. Taiga stores tags in lower case, so the CLI sends them that way.
  • --add-assignee/--remove-assignee merge with the current assignees (assigned_users) and never change the main assignee (assigned_to); that takes --owner-assignee or --clear-owner-assignee. Taiga always shows the main assignee among the assignees, so removing them needs one of those two flags in the same command. Changing the main assignee keeps the others.
  • --remove-assignee also takes users who left the project. A write that changes assignees is not retried after a version conflict (exit 4; run it again, or use --force-version).
  • Taiga's concurrency check does not cover the main assignee (assigned_to). The CLI re-reads the assignees right before writing (a change since the first read is version_conflict) and checks the story after writing; a mismatch is assignees_postcondition_failed (exit 4): the write was applied, so check the story instead of re-running. A concurrent change in the short window between that re-read and the write is detected, not prevented. See docs/api-notes.md.
  • --block NOTE sets is_blocked with the note (required); --unblock clears both.
  • close without --status uses the project's only closed status; with several (the default template has Done and Archived) it asks for one.
  • Every write accepts --dry-run (prints method, path and body) and, except create, --force-version.
  • comment sends the text exactly as given (quotes, accents, line breaks) and never touches the description. A blank comment is refused. Taiga accepts any past version for a comment, so the version protects nothing there and there is no --force-version: the comment is sent once. If the answer is lost (network error or 5xx), the CLI looks for it in the story history: found, the command succeeds; otherwise it exits with comment_unconfirmed (exit 1), because a request still running on the server can land later. Check story comments before publishing again. Exit 7 (network_error) means the connection never opened, so nothing was sent.
  • comments lists every comment, including edited and deleted ones (edit_comment_date, delete_comment_date). Comments written by Taiga's own GitLab integration (its inactive gitlab-<hash> user, with the push hook templates "This user story has been mentioned by …" and "… changed the status from [GitLab commit]…") are hidden unless --include-system; every other author, service accounts included, is always shown. Each entry carries is_system, story_ref and the story url.
  • --epic on create/update answers unsupported_operation: Taiga links epics through a separate, unversioned resource whose replacement needs DELETE (see docs/api-notes.md).
taiga story list --assignee me --closed=false
taiga story update 246 --status "In progress" --add-tag cli --dry-run
taiga story update 246 --description-file notes.md
taiga story update 247 --add-assignee me --remove-assignee jdoe --block "waiting for the B6 review"
taiga story comment 246 --body-file note.md
taiga story comments 246 --output text

Custom fields

Command What it does
taiga field list --kind story|task The project's custom field definitions
taiga field create --kind story|task --name NAME --type text|date|checkbox [--description TEXT] [--dry-run] Create a definition unless one with that name exists
taiga story field list REF / taiga story field set REF ["Name=value"]... [--unset NAME]... [--dry-run] [--force-version] Read and merge a story's values
  • field create is idempotent: an existing definition with the same name (case-sensitive) and type is returned unchanged; one with another type, or another description when --description is given, is field_definition_conflict (exit 2). Definitions are never changed or deleted.
  • Only text, date and checkbox are supported so far. Values: text as is (null is text), checkbox true/false, date YYYY-MM-DD. The name ends at the first =.
  • --unset NAME (repeatable, name or id) clears a checkbox or date field: Taiga stores null under its key, which stays even when it is the last field (Taiga refuses an empty dictionary). A field without a value, or already null, writes nothing. Text fields cannot be unset (usage error); set them to empty with Name=. JSON output shows the cleared value as null; text output shows it empty, like a field without value. The same merge, check and --dry-run apply.
  • story field set reads the values, merges the named fields and writes the whole dictionary with the version of the values resource, not the story's. Unchanged values write nothing. Values of fields without a definition are kept.
  • Taiga never refuses an old version on custom field values, so it cannot stop a concurrent write from being overwritten. The CLI checks the answer instead: if another write landed next to ours, it exits with field_values_postcondition_failed (exit 4) and the write was applied; check with story field list instead of re-running. --force-version skips the check. See docs/api-notes.md.
taiga field create --kind story --name "Tested in staging" --type checkbox
taiga story field set 246 "Tested in staging=true" "Delivery=2026-10-15" "Notes=a=b is fine"
taiga story field set 246 --unset "Tested in staging" --unset "Delivery"

Project configuration

Command What it does
taiga status list [--kind story|task] The project's statuses in board order
taiga project plan -f FILE|- Compare the project with a TOML file; reads only
taiga project apply -f FILE|- [--dry-run] Create the statuses and story fields the file declares and the project lacks

The file declares story statuses and story custom fields (docs/examples/taiga-project.toml); unknown keys are errors:

[[story_status]]
name = "Waiting for deployment"
color = "#40A8E4"      # #RRGGBB, required
closed = false         # default false
after = "Ready for test"

[[story_field]]
name = "Tested in staging"
type = "checkbox"      # text, date or checkbox
description = ""       # default ""
  • Only what the file declares is managed and nothing is ever updated or deleted. Statuses and fields missing from the file are listed as unmanaged and kept. A declared one that exists with another color, closed, type or description — or a name that differs only by case — is drift: plan shows it and apply refuses (definition_drift, exit 2) before writing anything.
  • Names are case-sensitive. Two statuses or fields with the same name in Taiga are refused (ambiguous_name).
  • New statuses are created after the last status, in file order. after places a status behind another one, existing or declared; a cycle, a self-reference or an unknown status is a usage error. When after requires moving statuses, apply writes the whole order at the end, in one bulk_update_order request.
  • Reordering is checked, not protected. Taiga 6.7 has no optimistic concurrency for status order (no version). Right before the write, apply re-reads the order and refuses if anything moved since the plan (project_changed, exit 4, nothing sent); right after, it re-reads and requires the intended order, or exits with status_order_postcondition_failed (exit 4): the write was applied and someone else's change landed next to it, so check with taiga status list instead of re-running. This detects part of the races, it does not prevent them: a change between the last read and the write can be overwritten. The write is never repeated.
  • apply needs admin_project_values (a project admin) and checks it first, also with --dry-run (forbidden, exit 6); plan only reads. --dry-run prints the requests without sending them.
  • apply validates everything before the first write, never repeats a write and never undoes one. Its JSON result has plan, applied, remaining and complete; complete is true only when a new read finds nothing left. If it stops halfway, the result still goes to stdout with the error on stderr; run it again and it re-plans from Taiga without duplicating anything. If Taiga changed under it, it exits with project_changed (exit 4). Declarations whose names differ only by case are refused before anything is read; the catalogs are re-read before each creation.
taiga status list --output text
taiga project plan -f taiga-project.toml
taiga project apply -f taiga-project.toml --dry-run
taiga project apply -f taiga-project.toml

Output and exit codes

Without a terminal on stdout, taiga prints JSON; on a terminal it prints text. Force either with --output json or --output text. Errors go to stderr as {"error":{"code","source","stage","cause","recovery"}}; code is stable and cause never contains a secret.

Exit Meaning
0 OK
1 unexpected error
2 invalid usage
3 authentication
4 version conflict
5 not found
6 permission denied
7 network or server

Every error code is listed in docs/errors.md.

Authentication

The token for each call is resolved in this order:

  1. TAIGA_TOKEN, used as is, with the type from TAIGA_TOKEN_TYPE (default Bearer);
  2. the cached session, while it is valid;
  3. a refresh of the cached session, under a file lock so concurrent processes refresh once;
  4. a login with the configured secret: TAIGA_PASSWORD or TAIGA_PASSWORD_FILE, then secret_command, the keyring, or the --insecure-storage file.

TAIGA_TOKEN, TAIGA_PASSWORD and TAIGA_PASSWORD_FILE are only sent to a URL given by --url, TAIGA_URL or the user config. A URL that comes only from a repository's .taiga.toml is refused with auth_untrusted_url, and so is taiga auth login without --url in that case, so a cloned repository cannot redirect your credentials.

Variable Use
TAIGA_URL Taiga base URL (https://host; http only for localhost)
TAIGA_PROJECT project slug
TAIGA_TOKEN token used as is
TAIGA_TOKEN_TYPE Authorization scheme for TAIGA_TOKEN (default Bearer)
TAIGA_USERNAME username for the session and for password logins
TAIGA_PASSWORD password for a login without the keyring
TAIGA_PASSWORD_FILE file holding the password
TAIGA_CONFIG config file (default ~/.config/taiga/config.toml)
TAIGA_STATE_DIR state dir holding the session cache (default ~/.local/state/taiga)

taiga auth login stores the password in the Secret Service keyring. Use --secret-command "pass show taiga" to read it from a password manager instead (the command is split on spaces and runs without a shell), or --insecure-storage to keep it in a 0600 file. taiga auth refresh renews the session, taiga auth logout deletes the local session and secret, and taiga auth status --diagnose tests every source and the environment:

taiga auth status --diagnose --output text

Coding agents and sandboxes

  • A person runs taiga auth login once per machine; agents only read the session cache and never see the password.
  • An agent that gets session_expired inside a sandbox cannot renew the session there: run taiga auth refresh outside the sandbox and retry. taiga never spends the stored refresh token when it cannot save the new one, and with a read-only cache it logs in only with TAIGA_PASSWORD/TAIGA_PASSWORD_FILE (keeping that token in memory), never with the keyring, secret_command or the --insecure-storage file, whether or not a session exists. Without a session and without an env password it fails with session_cache_readonly: run taiga auth login outside the sandbox.
  • Codex: enable network_access = true and add the state dir to writable_roots:
[sandbox_workspace_write]
network_access = true
writable_roots = ["/home/you/.local/state/taiga"]   # absolute path to the state dir

Headless Linux keyring

On a server without a desktop session, run GNOME Keyring for the Secret Service only. The unlock must be what starts the daemon: a daemon that is already running (started by a unit or by D-Bus activation) stays without an unlocked login collection.

# Debian/Ubuntu
sudo apt-get install -y gnome-keyring libsecret-tools dbus-user-session
loginctl enable-linger "$USER"   # keep the user manager and its D-Bus running without an open session

# once per boot, before anything uses the keyring: unlock
# (the first unlock creates the "login" collection with this password)
read -rs KP && printf '%s' "$KP" | gnome-keyring-daemon --unlock --components=secrets >/dev/null; unset KP

# check
busctl --user list | grep org.freedesktop.secrets
secret-tool store --label=probe test probe <<<"ok" && secret-tool lookup test probe && secret-tool clear test probe
taiga auth status --diagnose

If something reached the keyring before the unlock, secret-tool fails with Object does not exist at path /org/freedesktop/secrets/collection/login and taiga reports keyring_no_default, keyring_locked or secret_missing: stop that daemon with pkill -x gnome-keyring-d and unlock again. Run these commands in bash, in the user's own login session (for example over SSH), so that DBUS_SESSION_BUS_ADDRESS points to the user bus.

The same keyring serves other tools that use libsecret, such as sonar auth login. KeePassXC and KWallet do not work without a graphical session. If unlocking at every boot is not acceptable, use --secret-command with a password manager.

Development

go test ./...
docker compose -f compose.test.yml up -d && scripts/taiga-seed && go test -tags integration -p 1 ./...

Integration tests only run against the local Taiga from compose.test.yml. Design notes and plans are in docs/superpowers/specs/ and docs/superpowers/plans/; Taiga API behaviour observed so far is in docs/api-notes.md.

Directories

Path Synopsis
cmd
taiga command
internal
app
Package app resolves the selected project and names into ids and computes minimal writes for the curated commands.
Package app resolves the selected project and names into ids and computes minimal writes for the curated commands.
auth
Package auth resolves Taiga credentials: env, session cache, refresh and secret sources.
Package auth resolves Taiga credentials: env, session cache, refresh and secret sources.
cli
Package cli wires the taiga command tree.
Package cli wires the taiga command tree.
config
Package config resolves file locations, the user config file and the Taiga URL/project context.
Package config resolves file locations, the user config file and the Taiga URL/project context.
output
Package output renders results and errors as text or JSON and defines exit codes.
Package output renders results and errors as text or JSON and defines exit codes.
taiga
Package taiga is an HTTP client for the Taiga REST API v1.
Package taiga is an HTTP client for the Taiga REST API v1.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL