Examples

Whole scripts to start from, in shell, Python and JavaScript.

Each example reads the workspace address and a personal token from the environment:

export ALBA_URL=https://your-workspace.albaticket.com
export ALBA_TOKEN=alba_pat_…

Make the token on your AI agents page with the least access the script needs: Read for the first and third, Read and change for the others. The scripts use the REST API, which an administrator switches on for the workspace under Modules and features; while it is off they are answered 403 feature_disabled.

My open tickets, for a stand-up

Shell, with curl and jq:

#!/bin/sh
set -eu

curl -sf "$ALBA_URL/api/v1/tickets?assignee=me&status=open&sort=priority&limit=100" \
  -H "Authorization: Bearer $ALBA_TOKEN" |
  jq -r '.tickets[] | "\(.key)\t\(.status.name)\t\(.priority // "-")\t\(.summary)"' |
  column -t -s "$(printf '\t')"

Raise a ticket from an alert, once

Python 3, standard library only. It looks for an open ticket with the alert's label before creating one, so an alert that keeps firing does not open a ticket each time:

import json, os, urllib.error, urllib.parse, urllib.request

BASE = os.environ["ALBA_URL"] + "/api/v1"
TOKEN = os.environ["ALBA_TOKEN"]


def call(method, path, body=None, **query):
    url = BASE + path + ("?" + urllib.parse.urlencode(query) if query else "")
    data = json.dumps(body).encode() if body is not None else None
    request = urllib.request.Request(url, data=data, method=method)
    request.add_header("Authorization", "Bearer " + TOKEN)
    request.add_header("Content-Type", "application/json")
    try:
        with urllib.request.urlopen(request) as response:
            return json.load(response)
    except urllib.error.HTTPError as error:
        message = json.load(error)["error"]["message"]
        raise SystemExit(f"{method} {path}: {error.code} {message}")


def raise_alert(name, detail):
    label = "alert-" + name
    found = call("GET", "/tickets", project="OPS", label=label, status="open", limit=1)

    if found["total"] > 0:
        key = found["tickets"][0]["key"]
        call("POST", f"/tickets/{key}/comments", {"body": f"Fired again:\n\n```\n{detail}\n```"})
        return key

    ticket = call("POST", "/tickets", {
        "project": "OPS",
        "summary": f"Alert: {name}",
        "description": f"```\n{detail}\n```",
        "type": "Bug",
        "priority": "High",
        "labels": [label],
    })
    return ticket["key"]


if __name__ == "__main__":
    print(raise_alert("disk-full", "/var is 97% full on web-3"))

Every ticket a search finds

Python 3, paging with limit and offset until total is reached:

import json, os, urllib.parse, urllib.request

BASE = os.environ["ALBA_URL"] + "/api/v1"
TOKEN = os.environ["ALBA_TOKEN"]


def tickets(**filters):
    offset = 0
    while True:
        query = urllib.parse.urlencode({**filters, "limit": 100, "offset": offset})
        request = urllib.request.Request(f"{BASE}/tickets?{query}")
        request.add_header("Authorization", "Bearer " + TOKEN)
        with urllib.request.urlopen(request) as response:
            page = json.load(response)
        yield from page["tickets"]
        offset += len(page["tickets"])
        if not page["tickets"] or offset >= page["total"]:
            return


for ticket in tickets(project="WEB", status="done", sort="key"):
    print(ticket["key"], ticket["summary"])

Resolve what a release shipped

JavaScript (Node.js 18 or later, which has fetch), for the end of a deployment pipeline. It resolves every ticket key mentioned in the release notes and says which release it went out in. A ticket that is already resolved, or that has no such transition, is reported and skipped:

const base = `${process.env.ALBA_URL}/api/v1`
const headers = {
  Authorization: `Bearer ${process.env.ALBA_TOKEN}`,
  "Content-Type": "application/json",
}

async function call(method, path, body) {
  const response = await fetch(base + path, {method, headers, body: body && JSON.stringify(body)})
  const data = await response.json()
  if (!response.ok) throw new Error(`${response.status} ${data.error.message}`)
  return data
}

const release = process.argv[2]
const notes = process.argv[3]
const keys = [...new Set(notes.match(/\b[A-Z][A-Z0-9_]+-\d+\b/g) ?? [])]

for (const key of keys) {
  try {
    const ticket = await call("POST", `/tickets/${key}/transitions`, {
      transition: "Resolved",
      resolution: "Done",
      comment: `Released in **${release}**.`,
    })
    console.log(`${key}: ${ticket.status.name}`)
  } catch (error) {
    console.log(`${key}: skipped (${error.message})`)
  }
}

Run it as node resolve.mjs 2.5.0 "$(cat RELEASE_NOTES.md)".

Move a card when a pull request opens

Shell, for a CI job. It moves the ticket named in the branch (WEB-142-fix-checkout) into the board's review column:

#!/bin/sh
set -eu

key=$(printf '%s' "$BRANCH_NAME" | grep -oE '^[A-Z][A-Z0-9_]+-[0-9]+' || true)
[ -n "$key" ] || exit 0

curl -sf "$ALBA_URL/api/v1/boards/3/moves" \
  -H "Authorization: Bearer $ALBA_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"key\": \"$key\", \"column\": \"Review\"}" |
  jq -r '"\(.key) is now \(.status.name)"'

A workspace with a GitHub or GitLab integration can do much of this without a token: see GitHub and GitLab in the user guide.

Close a sprint and open the next

Shell. Completes the active sprint, moving what is unfinished into the next one, then starts it for two weeks. The token's member must be able to manage the board's sprints:

#!/bin/sh
set -eu

board=3
api="$ALBA_URL/api/v1/boards/$board/sprints"
auth="Authorization: Bearer $ALBA_TOKEN"

sprints=$(curl -sf "$api" -H "$auth")
active=$(printf '%s' "$sprints" | jq -r '.sprints[] | select(.state == "active") | .id')
next=$(printf '%s' "$sprints" | jq -r '[.sprints[] | select(.state == "future")][0].id')
[ "$active" ] && [ "$next" != null ] || { echo "Needs an active sprint and a future one" >&2; exit 1; }

curl -sf -X POST "$api/$active/complete" -H "$auth" -H "Content-Type: application/json" \
  -d "{\"move_open_to\": \"$next\"}" | jq -r '"Closed \(.name), moved \(.moved | length)"'

end=$(date -u -d "+14 days" +%F 2>/dev/null || date -u -v+14d +%F)
curl -sf -X POST "$api/$next/start" -H "$auth" -H "Content-Type: application/json" \
  -d "{\"end_date\": \"$end\"}" | jq -r '"Started \(.name) until \(.end_at)"'