@codyswann/lisa 2.11.0 → 2.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +65 -14
- package/package.json +1 -1
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/skills/jira-read-ticket/SKILL.md +18 -2
- package/plugins/lisa/skills/jira-read-ticket/scripts/download-attachment.sh +110 -0
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/src/base/skills/jira-read-ticket/SKILL.md +18 -2
- package/plugins/src/base/skills/jira-read-ticket/scripts/download-attachment.sh +110 -0
package/README.md
CHANGED
|
@@ -12,9 +12,9 @@ A request to fix a bug routes to a different flow than a request to build a feat
|
|
|
12
12
|
|
|
13
13
|
### Flows and Agents
|
|
14
14
|
|
|
15
|
-
A flow is a pipeline. Each step in the pipeline is an **agent** — a scoped AI with specific tools and
|
|
15
|
+
A flow is a pipeline. Each step in the pipeline is an **agent** — a scoped AI with specific tools and instructions. One agent investigates git history, another reproduces bugs, another writes code, another verifies the result.
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
Behind the scenes, agents delegate domain-specific work to reusable instruction sets that are loaded automatically when a command runs. The same logic that triages a JIRA ticket interactively is the same logic invoked by the nightly triage workflow — you don't need to know which one is running.
|
|
18
18
|
|
|
19
19
|
Flows can nest. A build flow includes a verification sub-flow, which includes a ship sub-flow. This composition keeps each flow focused while enabling complex end-to-end workflows.
|
|
20
20
|
|
|
@@ -28,13 +28,13 @@ Lisa enforces quality through layered gates:
|
|
|
28
28
|
|
|
29
29
|
### Location Agnostic
|
|
30
30
|
|
|
31
|
-
The same rules,
|
|
31
|
+
The same rules, workflows, and quality gates apply everywhere:
|
|
32
32
|
|
|
33
33
|
- On a developer's workstation running Claude Code interactively
|
|
34
34
|
- In a GitHub Action running a nightly improvement job
|
|
35
35
|
- In a CI workflow responding to a PR review comment
|
|
36
36
|
|
|
37
|
-
The
|
|
37
|
+
The orchestration adapts to context — using MCP integrations locally and REST APIs in CI — but the standards don't change.
|
|
38
38
|
|
|
39
39
|
### Template Governance
|
|
40
40
|
|
|
@@ -44,7 +44,7 @@ Lisa distributes its standards to downstream projects as templates. When a proje
|
|
|
44
44
|
- Test and coverage infrastructure
|
|
45
45
|
- CI/CD workflows
|
|
46
46
|
- Git hooks
|
|
47
|
-
- AI agent definitions
|
|
47
|
+
- AI agent definitions and project rules
|
|
48
48
|
|
|
49
49
|
Templates follow governance rules: some files are overwritten on every update (enforced standards), some are created once and left alone (project customization), and some are merged (shared defaults with project additions).
|
|
50
50
|
|
|
@@ -58,15 +58,66 @@ curl -fsSL https://claude.ai/install.sh | bash
|
|
|
58
58
|
|
|
59
59
|
## Working With Lisa
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
Lisa exposes a small set of top-level commands that map to the work lifecycle. Run them in Claude Code; everything underneath — agents, sub-flows, and the supporting libraries that power each step — happens automatically.
|
|
62
62
|
|
|
63
|
-
|
|
63
|
+
### The Lifecycle
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
- `/build` — route through the feature build flow
|
|
67
|
-
- `/improve` — route through the improvement flow
|
|
68
|
-
- `/investigate` — route through the investigation flow
|
|
69
|
-
- `/jira:triage <TICKET-ID>` — analytical triage gate: detect ambiguities, edge cases, and verification methodology
|
|
70
|
-
- `/plan:improve-tests <target>` — improve test quality by analyzing and strengthening weak or brittle tests
|
|
65
|
+
A piece of work moves through five stages. Each stage has one command.
|
|
71
66
|
|
|
72
|
-
|
|
67
|
+
| Stage | Command | What it does |
|
|
68
|
+
| --- | --- | --- |
|
|
69
|
+
| Research | `/lisa:research <problem>` | Investigates the codebase and problem space, then produces a PRD ready for planning. |
|
|
70
|
+
| Plan | `/lisa:plan <PRD>` | Decomposes a PRD into ordered work items in your tracker (JIRA, GitHub Issues, or Linear). |
|
|
71
|
+
| Implement | `/lisa:implement <ticket>` | Takes one work item from spec to shipped: assembles an agent team, runs the build, opens a PR, handles review, merges. |
|
|
72
|
+
| Verify | `/lisa:verify` | Commits, pushes, opens a PR, monitors deploy, and verifies behavior in the target environment. Folded into `/lisa:implement` but available standalone. |
|
|
73
|
+
| Debrief | `/lisa:debrief <epic>` | After shipping, mines tickets and PRs to surface edge cases, gotchas, and friction. Produces a triage doc; `/lisa:debrief:apply` persists accepted learnings. |
|
|
74
|
+
|
|
75
|
+
Most users only ever call `/lisa:research`, `/lisa:plan`, and `/lisa:implement`. The rest run automatically as sub-flows.
|
|
76
|
+
|
|
77
|
+
### Batch and Scheduled Work
|
|
78
|
+
|
|
79
|
+
| Command | What it does |
|
|
80
|
+
| --- | --- |
|
|
81
|
+
| `/lisa:intake <queue-url>` | Scans a Ready queue (Notion PRD database, JIRA project, GitHub repo, Linear team, Confluence space) and dispatches each item through the right lifecycle command. Designed as the cron target for unattended runs. |
|
|
82
|
+
|
|
83
|
+
### Maintenance and Operations
|
|
84
|
+
|
|
85
|
+
| Command | What it does |
|
|
86
|
+
| --- | --- |
|
|
87
|
+
| `/lisa:monitor [environment]` | Checks application health, logs, error rates, and performance for the named environment. |
|
|
88
|
+
| `/lisa:product-walkthrough <route>` | Walks the live product through a real browser to ground PRD or ticket reasoning in current behavior. |
|
|
89
|
+
| `/lisa:codify-verification <type> <what>` | Converts a passing manual verification into a regression test in the appropriate framework (Playwright, integration test, benchmark). Runs automatically after `/lisa:verify`. |
|
|
90
|
+
| `/lisa:review:local` | Reviews local branch changes against `main`. |
|
|
91
|
+
| `/lisa:pull-request:review <pr-url>` | Pulls down review comments on a PR and implements the valid ones. |
|
|
92
|
+
| `/lisa:security:zap-scan` | Runs an OWASP ZAP baseline scan against the local app. |
|
|
93
|
+
|
|
94
|
+
### Targeted Improvements
|
|
95
|
+
|
|
96
|
+
These commands tighten a specific quality threshold and fix every violation in one pass — useful for incremental hardening or nightly jobs.
|
|
97
|
+
|
|
98
|
+
| Command | What it does |
|
|
99
|
+
| --- | --- |
|
|
100
|
+
| `/lisa:improve:test-coverage <pct>` | Raises coverage to the target percentage by adding tests for uncovered code. |
|
|
101
|
+
| `/lisa:improve:tests <target>` | Strengthens weak, brittle, or poorly-written tests. |
|
|
102
|
+
| `/lisa:improve:code-complexity` | Lowers the cognitive-complexity threshold by 2 and fixes resulting violations. |
|
|
103
|
+
| `/lisa:improve:max-lines <n>` | Reduces the max-file-lines threshold and fixes violations. |
|
|
104
|
+
| `/lisa:improve:max-lines-per-function <n>` | Reduces the max-lines-per-function threshold and fixes violations. |
|
|
105
|
+
| `/lisa:fix:linter-error <rule> [...]` | Fixes every violation of one or more ESLint rules across the codebase. |
|
|
106
|
+
|
|
107
|
+
### Git Helpers
|
|
108
|
+
|
|
109
|
+
| Command | What it does |
|
|
110
|
+
| --- | --- |
|
|
111
|
+
| `/lisa:git:commit [hint]` | Creates conventional commits from the current changes. |
|
|
112
|
+
| `/lisa:git:submit-pr [hint]` | Pushes and opens or updates a PR. |
|
|
113
|
+
| `/lisa:git:prune` | Prunes local branches whose remotes have been deleted. |
|
|
114
|
+
|
|
115
|
+
### Talking to Lisa in Plain English
|
|
116
|
+
|
|
117
|
+
You don't have to remember any of this. Tell Claude what you want and the right command will run:
|
|
118
|
+
|
|
119
|
+
> "I have JIRA ticket PROJ-1234. Research, plan, and implement it."
|
|
120
|
+
> "Walk through the checkout flow and tell me what's broken."
|
|
121
|
+
> "Get test coverage to 90%."
|
|
122
|
+
|
|
123
|
+
> Ask Claude: "What commands are available?" for the full list at any time.
|
package/package.json
CHANGED
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
"lodash": ">=4.18.1"
|
|
80
80
|
},
|
|
81
81
|
"name": "@codyswann/lisa",
|
|
82
|
-
"version": "2.
|
|
82
|
+
"version": "2.12.0",
|
|
83
83
|
"description": "Claude Code governance framework that applies guardrails, guidance, and automated enforcement to projects",
|
|
84
84
|
"main": "dist/index.js",
|
|
85
85
|
"exports": {
|
|
@@ -34,7 +34,23 @@ Call `mcp__atlassian__getJiraIssue` for the target ticket. Extract and preserve:
|
|
|
34
34
|
- Full description (preserve formatting)
|
|
35
35
|
- Acceptance criteria section if separately structured
|
|
36
36
|
- **Validation Journey** section if present (pass verbatim to downstream)
|
|
37
|
-
- Attachments list
|
|
37
|
+
- Attachments list — capture `id`, `filename`, `mimeType`, `size`, and `content` URL for each. Do not download unless a downstream task needs the bytes (see "Downloading attachments" below).
|
|
38
|
+
|
|
39
|
+
#### Downloading attachments (opt-in)
|
|
40
|
+
|
|
41
|
+
The Atlassian MCP exposes attachment metadata but no binary-fetch tool ([JRACLOUD-97830](https://jira.atlassian.com/browse/JRACLOUD-97830), [ECO-1265](https://jira.atlassian.com/browse/ECO-1265)). Fetch attachment bytes only when a downstream task explicitly needs them — e.g., a design-fidelity check on an image, log-file analysis, PDF text extraction. For everything else, keep the URL reference and move on.
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
bash .claude/skills/jira-read-ticket/scripts/download-attachment.sh <id-or-content-url> <output-path>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Requires `JIRA_SERVER`, `JIRA_LOGIN`, and `JIRA_API_TOKEN` in the environment (same contract as `jira-evidence`). If those are not set the helper exits with code 2 and a clear remediation message — record the URL only and continue.
|
|
48
|
+
|
|
49
|
+
After download, branch on `mimeType`:
|
|
50
|
+
- `image/*` — pass the local path to image-aware downstream tools
|
|
51
|
+
- `text/*`, `application/json`, `application/xml`, `application/x-yaml` — read inline as text
|
|
52
|
+
- `application/pdf` — extract text via downstream tooling if needed
|
|
53
|
+
- everything else — record path only; do not attempt to inline binary content
|
|
38
54
|
|
|
39
55
|
### Comments
|
|
40
56
|
|
|
@@ -121,7 +137,7 @@ Produce a single structured output that the caller can pass verbatim to downstre
|
|
|
121
137
|
<chronological comments, flagged items called out>
|
|
122
138
|
|
|
123
139
|
### Attachments
|
|
124
|
-
<list>
|
|
140
|
+
<list with id, filename, mimeType, size, content URL — note any that were downloaded and their local paths>
|
|
125
141
|
|
|
126
142
|
## Remote Links
|
|
127
143
|
### Pull Requests (<count>)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# download-attachment.sh — Download a JIRA attachment to a local file.
|
|
3
|
+
#
|
|
4
|
+
# Usage:
|
|
5
|
+
# bash download-attachment.sh <ATTACHMENT_ID_OR_URL> <OUTPUT_PATH>
|
|
6
|
+
#
|
|
7
|
+
# Why this helper exists:
|
|
8
|
+
# The Atlassian MCP server (mcp__atlassian__*) returns attachment metadata
|
|
9
|
+
# (id, filename, mimeType, size, content URL) on getJiraIssue but provides
|
|
10
|
+
# no tool to fetch the binary content. This script closes the gap by
|
|
11
|
+
# hitting the Jira REST API directly with Basic auth, mirroring the
|
|
12
|
+
# env-var contract already used by jira-evidence/scripts/post-evidence.sh.
|
|
13
|
+
#
|
|
14
|
+
# See https://jira.atlassian.com/browse/JRACLOUD-97830 for the upstream
|
|
15
|
+
# gap; remove this helper once Atlassian ships a download tool in the MCP.
|
|
16
|
+
#
|
|
17
|
+
# Required env vars:
|
|
18
|
+
# JIRA_SERVER - https://<your-tenant>.atlassian.net
|
|
19
|
+
# JIRA_LOGIN - login email
|
|
20
|
+
# JIRA_API_TOKEN - API token (https://id.atlassian.com/manage-profile/security/api-tokens)
|
|
21
|
+
#
|
|
22
|
+
# Exit codes:
|
|
23
|
+
# 0 success
|
|
24
|
+
# 1 download failed (HTTP error)
|
|
25
|
+
# 2 missing required env var
|
|
26
|
+
# 3 invalid arguments
|
|
27
|
+
|
|
28
|
+
set -euo pipefail
|
|
29
|
+
|
|
30
|
+
if [[ $# -lt 2 ]]; then
|
|
31
|
+
echo "Usage: download-attachment.sh <ATTACHMENT_ID_OR_URL> <OUTPUT_PATH>" >&2
|
|
32
|
+
exit 3
|
|
33
|
+
fi
|
|
34
|
+
ID_OR_URL="$1"
|
|
35
|
+
OUTPUT_PATH="$2"
|
|
36
|
+
|
|
37
|
+
# Resolve credentials: prefer env, fall back to jira-cli config for server/login.
|
|
38
|
+
JIRA_CONFIG="${HOME}/.config/.jira/.config.yml"
|
|
39
|
+
if [[ -z "${JIRA_SERVER:-}" && -f "$JIRA_CONFIG" ]]; then
|
|
40
|
+
JIRA_SERVER=$(grep '^server:' "$JIRA_CONFIG" | awk '{print $2}')
|
|
41
|
+
fi
|
|
42
|
+
if [[ -z "${JIRA_LOGIN:-}" && -f "$JIRA_CONFIG" ]]; then
|
|
43
|
+
JIRA_LOGIN=$(grep '^login:' "$JIRA_CONFIG" | awk '{print $2}')
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
for VAR in JIRA_SERVER JIRA_LOGIN JIRA_API_TOKEN; do
|
|
47
|
+
if [[ -z "${!VAR:-}" ]]; then
|
|
48
|
+
echo "ERROR: $VAR is not set." >&2
|
|
49
|
+
echo "Required env vars: JIRA_SERVER, JIRA_LOGIN, JIRA_API_TOKEN." >&2
|
|
50
|
+
echo "Generate an API token: https://id.atlassian.com/manage-profile/security/api-tokens" >&2
|
|
51
|
+
exit 2
|
|
52
|
+
fi
|
|
53
|
+
done
|
|
54
|
+
|
|
55
|
+
OUTPUT_DIR=$(dirname "$OUTPUT_PATH")
|
|
56
|
+
if [[ ! -d "$OUTPUT_DIR" ]]; then
|
|
57
|
+
echo "ERROR: Output directory does not exist: $OUTPUT_DIR" >&2
|
|
58
|
+
exit 3
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
if [[ "$ID_OR_URL" == http*://* ]]; then
|
|
62
|
+
ATTACHMENT_URL="$ID_OR_URL"
|
|
63
|
+
else
|
|
64
|
+
ATTACHMENT_URL="${JIRA_SERVER%/}/rest/api/3/attachment/content/$ID_OR_URL"
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
JIRA_AUTH=$(printf '%s' "$JIRA_LOGIN:$JIRA_API_TOKEN" | base64 | tr -d '\n')
|
|
68
|
+
|
|
69
|
+
# Atlassian responds 302 to a signed URL on media.atlassian.com that has its
|
|
70
|
+
# own auth and rejects Basic. Two-step: capture Location, then GET unauthed.
|
|
71
|
+
HEADERS_FILE=$(mktemp)
|
|
72
|
+
trap 'rm -f "$HEADERS_FILE"' EXIT
|
|
73
|
+
|
|
74
|
+
HTTP_CODE=$(curl -sS -o /dev/null -w '%{http_code}' \
|
|
75
|
+
--max-redirs 0 \
|
|
76
|
+
-D "$HEADERS_FILE" \
|
|
77
|
+
-H "Authorization: Basic $JIRA_AUTH" \
|
|
78
|
+
-H "Accept: */*" \
|
|
79
|
+
"$ATTACHMENT_URL" || true)
|
|
80
|
+
|
|
81
|
+
case "$HTTP_CODE" in
|
|
82
|
+
302|303|307)
|
|
83
|
+
SIGNED_URL=$(awk '/^[Ll]ocation:/{sub(/^[Ll]ocation:[ \t]*/,""); sub(/\r$/,""); print; exit}' "$HEADERS_FILE")
|
|
84
|
+
if [[ -z "$SIGNED_URL" ]]; then
|
|
85
|
+
echo "ERROR: Got HTTP $HTTP_CODE but no Location header in response." >&2
|
|
86
|
+
exit 1
|
|
87
|
+
fi
|
|
88
|
+
curl -sSf -o "$OUTPUT_PATH" "$SIGNED_URL" || { echo "ERROR: Download from signed URL failed." >&2; exit 1; }
|
|
89
|
+
;;
|
|
90
|
+
200)
|
|
91
|
+
curl -sSf -o "$OUTPUT_PATH" \
|
|
92
|
+
-H "Authorization: Basic $JIRA_AUTH" \
|
|
93
|
+
-H "Accept: */*" \
|
|
94
|
+
"$ATTACHMENT_URL" || { echo "ERROR: Direct download from $ATTACHMENT_URL failed." >&2; exit 1; }
|
|
95
|
+
;;
|
|
96
|
+
401|403)
|
|
97
|
+
echo "ERROR: Authentication failed (HTTP $HTTP_CODE). Verify JIRA_LOGIN and JIRA_API_TOKEN." >&2
|
|
98
|
+
exit 1
|
|
99
|
+
;;
|
|
100
|
+
404)
|
|
101
|
+
echo "ERROR: Attachment not found at $ATTACHMENT_URL (HTTP 404). Verify the ID and your access." >&2
|
|
102
|
+
exit 1
|
|
103
|
+
;;
|
|
104
|
+
*)
|
|
105
|
+
echo "ERROR: Unexpected HTTP $HTTP_CODE from $ATTACHMENT_URL" >&2
|
|
106
|
+
exit 1
|
|
107
|
+
;;
|
|
108
|
+
esac
|
|
109
|
+
|
|
110
|
+
echo " ✓ Downloaded -> $OUTPUT_PATH"
|
|
@@ -34,7 +34,23 @@ Call `mcp__atlassian__getJiraIssue` for the target ticket. Extract and preserve:
|
|
|
34
34
|
- Full description (preserve formatting)
|
|
35
35
|
- Acceptance criteria section if separately structured
|
|
36
36
|
- **Validation Journey** section if present (pass verbatim to downstream)
|
|
37
|
-
- Attachments list
|
|
37
|
+
- Attachments list — capture `id`, `filename`, `mimeType`, `size`, and `content` URL for each. Do not download unless a downstream task needs the bytes (see "Downloading attachments" below).
|
|
38
|
+
|
|
39
|
+
#### Downloading attachments (opt-in)
|
|
40
|
+
|
|
41
|
+
The Atlassian MCP exposes attachment metadata but no binary-fetch tool ([JRACLOUD-97830](https://jira.atlassian.com/browse/JRACLOUD-97830), [ECO-1265](https://jira.atlassian.com/browse/ECO-1265)). Fetch attachment bytes only when a downstream task explicitly needs them — e.g., a design-fidelity check on an image, log-file analysis, PDF text extraction. For everything else, keep the URL reference and move on.
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
bash .claude/skills/jira-read-ticket/scripts/download-attachment.sh <id-or-content-url> <output-path>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Requires `JIRA_SERVER`, `JIRA_LOGIN`, and `JIRA_API_TOKEN` in the environment (same contract as `jira-evidence`). If those are not set the helper exits with code 2 and a clear remediation message — record the URL only and continue.
|
|
48
|
+
|
|
49
|
+
After download, branch on `mimeType`:
|
|
50
|
+
- `image/*` — pass the local path to image-aware downstream tools
|
|
51
|
+
- `text/*`, `application/json`, `application/xml`, `application/x-yaml` — read inline as text
|
|
52
|
+
- `application/pdf` — extract text via downstream tooling if needed
|
|
53
|
+
- everything else — record path only; do not attempt to inline binary content
|
|
38
54
|
|
|
39
55
|
### Comments
|
|
40
56
|
|
|
@@ -121,7 +137,7 @@ Produce a single structured output that the caller can pass verbatim to downstre
|
|
|
121
137
|
<chronological comments, flagged items called out>
|
|
122
138
|
|
|
123
139
|
### Attachments
|
|
124
|
-
<list>
|
|
140
|
+
<list with id, filename, mimeType, size, content URL — note any that were downloaded and their local paths>
|
|
125
141
|
|
|
126
142
|
## Remote Links
|
|
127
143
|
### Pull Requests (<count>)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# download-attachment.sh — Download a JIRA attachment to a local file.
|
|
3
|
+
#
|
|
4
|
+
# Usage:
|
|
5
|
+
# bash download-attachment.sh <ATTACHMENT_ID_OR_URL> <OUTPUT_PATH>
|
|
6
|
+
#
|
|
7
|
+
# Why this helper exists:
|
|
8
|
+
# The Atlassian MCP server (mcp__atlassian__*) returns attachment metadata
|
|
9
|
+
# (id, filename, mimeType, size, content URL) on getJiraIssue but provides
|
|
10
|
+
# no tool to fetch the binary content. This script closes the gap by
|
|
11
|
+
# hitting the Jira REST API directly with Basic auth, mirroring the
|
|
12
|
+
# env-var contract already used by jira-evidence/scripts/post-evidence.sh.
|
|
13
|
+
#
|
|
14
|
+
# See https://jira.atlassian.com/browse/JRACLOUD-97830 for the upstream
|
|
15
|
+
# gap; remove this helper once Atlassian ships a download tool in the MCP.
|
|
16
|
+
#
|
|
17
|
+
# Required env vars:
|
|
18
|
+
# JIRA_SERVER - https://<your-tenant>.atlassian.net
|
|
19
|
+
# JIRA_LOGIN - login email
|
|
20
|
+
# JIRA_API_TOKEN - API token (https://id.atlassian.com/manage-profile/security/api-tokens)
|
|
21
|
+
#
|
|
22
|
+
# Exit codes:
|
|
23
|
+
# 0 success
|
|
24
|
+
# 1 download failed (HTTP error)
|
|
25
|
+
# 2 missing required env var
|
|
26
|
+
# 3 invalid arguments
|
|
27
|
+
|
|
28
|
+
set -euo pipefail
|
|
29
|
+
|
|
30
|
+
if [[ $# -lt 2 ]]; then
|
|
31
|
+
echo "Usage: download-attachment.sh <ATTACHMENT_ID_OR_URL> <OUTPUT_PATH>" >&2
|
|
32
|
+
exit 3
|
|
33
|
+
fi
|
|
34
|
+
ID_OR_URL="$1"
|
|
35
|
+
OUTPUT_PATH="$2"
|
|
36
|
+
|
|
37
|
+
# Resolve credentials: prefer env, fall back to jira-cli config for server/login.
|
|
38
|
+
JIRA_CONFIG="${HOME}/.config/.jira/.config.yml"
|
|
39
|
+
if [[ -z "${JIRA_SERVER:-}" && -f "$JIRA_CONFIG" ]]; then
|
|
40
|
+
JIRA_SERVER=$(grep '^server:' "$JIRA_CONFIG" | awk '{print $2}')
|
|
41
|
+
fi
|
|
42
|
+
if [[ -z "${JIRA_LOGIN:-}" && -f "$JIRA_CONFIG" ]]; then
|
|
43
|
+
JIRA_LOGIN=$(grep '^login:' "$JIRA_CONFIG" | awk '{print $2}')
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
for VAR in JIRA_SERVER JIRA_LOGIN JIRA_API_TOKEN; do
|
|
47
|
+
if [[ -z "${!VAR:-}" ]]; then
|
|
48
|
+
echo "ERROR: $VAR is not set." >&2
|
|
49
|
+
echo "Required env vars: JIRA_SERVER, JIRA_LOGIN, JIRA_API_TOKEN." >&2
|
|
50
|
+
echo "Generate an API token: https://id.atlassian.com/manage-profile/security/api-tokens" >&2
|
|
51
|
+
exit 2
|
|
52
|
+
fi
|
|
53
|
+
done
|
|
54
|
+
|
|
55
|
+
OUTPUT_DIR=$(dirname "$OUTPUT_PATH")
|
|
56
|
+
if [[ ! -d "$OUTPUT_DIR" ]]; then
|
|
57
|
+
echo "ERROR: Output directory does not exist: $OUTPUT_DIR" >&2
|
|
58
|
+
exit 3
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
if [[ "$ID_OR_URL" == http*://* ]]; then
|
|
62
|
+
ATTACHMENT_URL="$ID_OR_URL"
|
|
63
|
+
else
|
|
64
|
+
ATTACHMENT_URL="${JIRA_SERVER%/}/rest/api/3/attachment/content/$ID_OR_URL"
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
JIRA_AUTH=$(printf '%s' "$JIRA_LOGIN:$JIRA_API_TOKEN" | base64 | tr -d '\n')
|
|
68
|
+
|
|
69
|
+
# Atlassian responds 302 to a signed URL on media.atlassian.com that has its
|
|
70
|
+
# own auth and rejects Basic. Two-step: capture Location, then GET unauthed.
|
|
71
|
+
HEADERS_FILE=$(mktemp)
|
|
72
|
+
trap 'rm -f "$HEADERS_FILE"' EXIT
|
|
73
|
+
|
|
74
|
+
HTTP_CODE=$(curl -sS -o /dev/null -w '%{http_code}' \
|
|
75
|
+
--max-redirs 0 \
|
|
76
|
+
-D "$HEADERS_FILE" \
|
|
77
|
+
-H "Authorization: Basic $JIRA_AUTH" \
|
|
78
|
+
-H "Accept: */*" \
|
|
79
|
+
"$ATTACHMENT_URL" || true)
|
|
80
|
+
|
|
81
|
+
case "$HTTP_CODE" in
|
|
82
|
+
302|303|307)
|
|
83
|
+
SIGNED_URL=$(awk '/^[Ll]ocation:/{sub(/^[Ll]ocation:[ \t]*/,""); sub(/\r$/,""); print; exit}' "$HEADERS_FILE")
|
|
84
|
+
if [[ -z "$SIGNED_URL" ]]; then
|
|
85
|
+
echo "ERROR: Got HTTP $HTTP_CODE but no Location header in response." >&2
|
|
86
|
+
exit 1
|
|
87
|
+
fi
|
|
88
|
+
curl -sSf -o "$OUTPUT_PATH" "$SIGNED_URL" || { echo "ERROR: Download from signed URL failed." >&2; exit 1; }
|
|
89
|
+
;;
|
|
90
|
+
200)
|
|
91
|
+
curl -sSf -o "$OUTPUT_PATH" \
|
|
92
|
+
-H "Authorization: Basic $JIRA_AUTH" \
|
|
93
|
+
-H "Accept: */*" \
|
|
94
|
+
"$ATTACHMENT_URL" || { echo "ERROR: Direct download from $ATTACHMENT_URL failed." >&2; exit 1; }
|
|
95
|
+
;;
|
|
96
|
+
401|403)
|
|
97
|
+
echo "ERROR: Authentication failed (HTTP $HTTP_CODE). Verify JIRA_LOGIN and JIRA_API_TOKEN." >&2
|
|
98
|
+
exit 1
|
|
99
|
+
;;
|
|
100
|
+
404)
|
|
101
|
+
echo "ERROR: Attachment not found at $ATTACHMENT_URL (HTTP 404). Verify the ID and your access." >&2
|
|
102
|
+
exit 1
|
|
103
|
+
;;
|
|
104
|
+
*)
|
|
105
|
+
echo "ERROR: Unexpected HTTP $HTTP_CODE from $ATTACHMENT_URL" >&2
|
|
106
|
+
exit 1
|
|
107
|
+
;;
|
|
108
|
+
esac
|
|
109
|
+
|
|
110
|
+
echo " ✓ Downloaded -> $OUTPUT_PATH"
|