Attach documents, screenshots, PDFs, and files to Jira issues and Confluence pages via REST API. Use when uploading evidence, documentation, or media to Atlassian products.
Attach files to Jira issues and Confluence pages using Atlassian REST API. Use when uploading screenshots, documents, or evidence after creating bugs, documentation, or test reports.
/plugin marketplace add jpoutrin/product-forge/plugin install atlassian-integration@product-forge-marketplaceThis skill inherits all available tools. When active, it can use any tool Claude has access to.
Attach files, screenshots, and documents to Jira issues and Confluence pages using the Atlassian REST API.
Generate an API token at: https://id.atlassian.com/manage-profile/security/api-tokens
export ATLASSIAN_DOMAIN="your-domain"
export ATLASSIAN_EMAIL="your-email@example.com"
export ATLASSIAN_API_TOKEN="your-api-token"
If environment variables are not set, add them to .envrc in your project root for automatic loading with direnv:
# .envrc
export ATLASSIAN_DOMAIN="your-domain"
export ATLASSIAN_EMAIL="your-email@example.com"
export ATLASSIAN_API_TOKEN="your-api-token"
Then allow the file:
direnv allow
Security Note: Add .envrc to .gitignore to prevent committing credentials:
echo ".envrc" >> .gitignore
# Verify variables are set
echo "Domain: ${ATLASSIAN_DOMAIN:-NOT SET}"
echo "Email: ${ATLASSIAN_EMAIL:-NOT SET}"
echo "Token: ${ATLASSIAN_API_TOKEN:+SET}"
If any show "NOT SET", prompt the user to configure .envrc.
Jira: https://{domain}.atlassian.net/rest/api/3
Confluence: https://{domain}.atlassian.net/wiki/rest/api
POST https://{domain}.atlassian.net/rest/api/3/issue/{issueIdOrKey}/attachments
| Header | Value | Purpose |
|---|---|---|
X-Atlassian-Token | no-check | CSRF protection bypass (required) |
Content-Type | multipart/form-data | File upload format |
curl --location --request POST \
'https://your-domain.atlassian.net/rest/api/3/issue/PROJ-123/attachments' \
-u 'email@example.com:<api_token>' \
-H 'X-Atlassian-Token: no-check' \
--form 'file=@"./screenshots/bug-evidence.png"'
import requests
from requests.auth import HTTPBasicAuth
def upload_jira_attachment(domain, email, api_token, issue_key, file_path):
url = f"https://{domain}.atlassian.net/rest/api/3/issue/{issue_key}/attachments"
auth = HTTPBasicAuth(email, api_token)
headers = {
"X-Atlassian-Token": "no-check"
}
with open(file_path, 'rb') as file:
files = {'file': file}
response = requests.post(url, auth=auth, headers=headers, files=files)
if response.status_code == 200:
return response.json()
else:
raise Exception(f"Upload failed: {response.status_code} - {response.text}")
# Usage
result = upload_jira_attachment(
domain="your-domain",
email="your-email@example.com",
api_token="your-api-token",
issue_key="PROJ-123",
file_path="./screenshots/bug-evidence.png"
)
#!/bin/bash
DOMAIN="your-domain"
EMAIL="your-email@example.com"
API_TOKEN="your-api-token"
ISSUE_KEY="PROJ-123"
FILES_DIR="./qa-tests/screenshots/"
for file in "$FILES_DIR"*.png; do
echo "Uploading: $file"
curl --silent --location --request POST \
"https://${DOMAIN}.atlassian.net/rest/api/3/issue/${ISSUE_KEY}/attachments" \
-u "${EMAIL}:${API_TOKEN}" \
-H "X-Atlassian-Token: no-check" \
--form "file=@\"$file\""
echo " Done"
done
POST https://{domain}.atlassian.net/wiki/rest/api/content/{pageId}/child/attachment
| Header | Value | Purpose |
|---|---|---|
X-Atlassian-Token | nocheck | CSRF protection bypass (required) |
Content-Type | multipart/form-data | File upload format |
curl -u "${USER_EMAIL}:${API_TOKEN}" \
-X POST \
-H "X-Atlassian-Token: nocheck" \
-F "file=@./diagram.png" \
-F "comment=Uploaded via REST API" \
"https://${DOMAIN}.atlassian.net/wiki/rest/api/content/${PAGE_ID}/child/attachment"
curl -X POST \
-H "Authorization: Bearer ${PAT_TOKEN}" \
-H "X-Atlassian-Token: no-check" \
-H "Content-Type: multipart/form-data" \
-F "file=@./document.pdf" \
"https://${DOMAIN}.atlassian.net/wiki/rest/api/content/${PAGE_ID}/child/attachment"
import requests
from requests.auth import HTTPBasicAuth
def upload_confluence_attachment(domain, email, api_token, page_id, file_path, comment=""):
url = f"https://{domain}.atlassian.net/wiki/rest/api/content/{page_id}/child/attachment"
auth = HTTPBasicAuth(email, api_token)
headers = {
"X-Atlassian-Token": "nocheck"
}
with open(file_path, 'rb') as file:
files = {'file': file}
data = {'comment': comment} if comment else {}
response = requests.post(url, auth=auth, headers=headers, files=files, data=data)
if response.status_code in [200, 201]:
return response.json()
else:
raise Exception(f"Upload failed: {response.status_code} - {response.text}")
# Usage
result = upload_confluence_attachment(
domain="your-domain",
email="your-email@example.com",
api_token="your-api-token",
page_id="123456789",
file_path="./docs/architecture.png",
comment="Architecture diagram v2"
)
# Find page ID from title
curl -u "${EMAIL}:${API_TOKEN}" \
"https://${DOMAIN}.atlassian.net/wiki/rest/api/content?title=Page%20Title&spaceKey=SPACE" \
| jq '.results[0].id'
After uploading, the attachment is in the page's attachment list but NOT embedded in content. You must update the page body to display it.
| Format | Syntax | Works with API? |
|---|---|---|
| Wiki markup | !image.png! | NO - Not converted |
| Markdown |  | YES - Converted to storage format |
| Storage format | <ac:image>...</ac:image> | YES - Native format |
Key insight: Markdown image syntax gets automatically converted to Confluence storage format when using the API.
When using storage format directly, you MUST use the correct element for referencing attachments:
| Reference Type | Element | Use Case |
|---|---|---|
| Page attachment | <ri:attachment ri:filename="..."/> | Files uploaded to the page |
| External URL | <ri:url ri:value="..."/> | External image URLs |
WRONG - Using ri:url for attachments (images won't display):
<ac:image ac:src="screenshot.png">
<ri:url ri:value="screenshot.png" />
</ac:image>
CORRECT - Using ri:attachment for uploaded files:
<ac:image>
<ri:attachment ri:filename="screenshot.png" />
</ac:image>
The ri:url element is for external URLs only. For files uploaded as attachments to the page, you MUST use ri:attachment with ri:filename.
Use representation: "wiki" with Markdown syntax:
curl -u "${EMAIL}:${API_TOKEN}" \
-X PUT \
-H "Content-Type: application/json" \
"https://${DOMAIN}.atlassian.net/wiki/rest/api/content/${PAGE_ID}" \
-d '{
"version": {"number": NEW_VERSION},
"title": "Page Title",
"type": "page",
"body": {
"wiki": {
"value": "# My Page\n\nHere is the diagram:\n\n\n\nMore content here.",
"representation": "wiki"
}
}
}'
Use native Confluence storage format with representation: "storage":
curl -u "${EMAIL}:${API_TOKEN}" \
-X PUT \
-H "Content-Type: application/json" \
"https://${DOMAIN}.atlassian.net/wiki/rest/api/content/${PAGE_ID}" \
-d '{
"version": {"number": NEW_VERSION},
"title": "Page Title",
"type": "page",
"body": {
"storage": {
"value": "<p>Here is the diagram:</p><ac:image ac:align=\"center\" ac:width=\"800\"><ri:attachment ri:filename=\"architecture.png\"/></ac:image>",
"representation": "storage"
}
}
}'
IMPORTANT: Always use <ri:attachment ri:filename="..."/> for page attachments, never <ri:url>.
RECOMMENDED for QA screenshots: Use ac:width="600" to prevent images from being too wide and hindering readability:
<!-- RECOMMENDED: Centered image with controlled width (best for QA screenshots) -->
<ac:image ac:align="center" ac:layout="center" ac:width="600">
<ri:attachment ri:filename="screenshot.png"/>
</ac:image>
<!-- Basic image (attachment) - will be full width, may be too large -->
<ac:image>
<ri:attachment ri:filename="screenshot.png"/>
</ac:image>
<!-- Element screenshots (smaller width for UI elements) -->
<ac:image ac:align="center" ac:layout="center" ac:width="400">
<ri:attachment ri:filename="button-element.png"/>
</ac:image>
<!-- Full-width diagram (use sparingly) -->
<ac:image ac:align="center" ac:layout="center" ac:width="800">
<ri:attachment ri:filename="architecture-diagram.png"/>
</ac:image>
<!-- As thumbnail (clickable to expand) -->
<ac:image ac:thumbnail="true">
<ri:attachment ri:filename="photo.jpg"/>
</ac:image>
<!-- With border -->
<ac:image ac:border="true" ac:width="400">
<ri:attachment ri:filename="ui-mockup.png"/>
</ac:image>
<!-- External URL (only for images NOT uploaded as attachments) -->
<ac:image>
<ri:url ri:value="https://example.com/external-image.png"/>
</ac:image>
Width Guidelines:
| Image Type | Recommended Width | Notes |
|---|---|---|
| Full page screenshots | 600 | Readable without scrolling |
| Element screenshots | 300-400 | Small UI components |
| Architecture diagrams | 800 | Complex diagrams need more space |
| Thumbnails | Use ac:thumbnail="true" | Clickable to expand |
import requests
from requests.auth import HTTPBasicAuth
import json
def upload_and_embed_image(domain, email, api_token, page_id, file_path, alt_text=""):
base_url = f"https://{domain}.atlassian.net/wiki/rest/api"
auth = HTTPBasicAuth(email, api_token)
filename = file_path.split('/')[-1]
# 1. Upload attachment
upload_url = f"{base_url}/content/{page_id}/child/attachment"
headers = {"X-Atlassian-Token": "nocheck"}
with open(file_path, 'rb') as f:
files = {'file': f}
response = requests.post(upload_url, auth=auth, headers=headers, files=files)
if response.status_code not in [200, 201]:
raise Exception(f"Upload failed: {response.text}")
# 2. Get current page version
page_url = f"{base_url}/content/{page_id}?expand=body.wiki,version"
page = requests.get(page_url, auth=auth).json()
current_version = page['version']['number']
title = page['title']
# 3. Update page with embedded image using Markdown
update_url = f"{base_url}/content/{page_id}"
# Get existing content or start fresh
existing_content = page.get('body', {}).get('wiki', {}).get('value', '')
# Append image in Markdown format
new_content = f"{existing_content}\n\n"
payload = {
"version": {"number": current_version + 1},
"title": title,
"type": "page",
"body": {
"wiki": {
"value": new_content,
"representation": "wiki"
}
}
}
response = requests.put(
update_url,
auth=auth,
headers={"Content-Type": "application/json"},
data=json.dumps(payload)
)
if response.status_code == 200:
return response.json()
else:
raise Exception(f"Update failed: {response.text}")
# Usage
upload_and_embed_image(
domain="your-domain",
email="your-email@example.com",
api_token="your-api-token",
page_id="123456789",
file_path="./screenshots/feature-demo.png",
alt_text="Feature Demo Screenshot"
)
#!/bin/bash
# upload-and-embed.sh - Upload image and embed in Confluence page
DOMAIN="${ATLASSIAN_DOMAIN}"
EMAIL="${ATLASSIAN_EMAIL}"
API_TOKEN="${ATLASSIAN_API_TOKEN}"
PAGE_ID="$1"
FILE_PATH="$2"
FILENAME=$(basename "$FILE_PATH")
# 1. Upload the attachment
echo "Uploading ${FILENAME}..."
curl -s -u "${EMAIL}:${API_TOKEN}" \
-X POST \
-H "X-Atlassian-Token: nocheck" \
-F "file=@${FILE_PATH}" \
"https://${DOMAIN}.atlassian.net/wiki/rest/api/content/${PAGE_ID}/child/attachment" > /dev/null
# 2. Get current page version
VERSION=$(curl -s -u "${EMAIL}:${API_TOKEN}" \
"https://${DOMAIN}.atlassian.net/wiki/rest/api/content/${PAGE_ID}?expand=version" \
| jq '.version.number')
TITLE=$(curl -s -u "${EMAIL}:${API_TOKEN}" \
"https://${DOMAIN}.atlassian.net/wiki/rest/api/content/${PAGE_ID}" \
| jq -r '.title')
# 3. Update page with embedded image (Markdown format)
NEW_VERSION=$((VERSION + 1))
echo "Embedding image in page (version ${NEW_VERSION})..."
curl -s -u "${EMAIL}:${API_TOKEN}" \
-X PUT \
-H "Content-Type: application/json" \
"https://${DOMAIN}.atlassian.net/wiki/rest/api/content/${PAGE_ID}" \
-d "{
\"version\": {\"number\": ${NEW_VERSION}},
\"title\": \"${TITLE}\",
\"type\": \"page\",
\"body\": {
\"wiki\": {
\"value\": \"\",
\"representation\": \"wiki\"
}
}
}" > /dev/null
echo "Done! Image embedded in page."
| Category | Extensions | Max Size |
|---|---|---|
| Images | .png, .jpg, .jpeg, .gif, .svg, .webp | 10 MB |
| Documents | .pdf, .doc, .docx, .xls, .xlsx, .ppt, .pptx | 10 MB |
| Text | .txt, .md, .csv, .json, .xml, .yaml | 10 MB |
| Archives | .zip, .tar, .gz | 10 MB |
| Code | .js, .py, .java, .ts, .html, .css | 10 MB |
| Category | Extensions | Max Size |
|---|---|---|
| Images | .png, .jpg, .jpeg, .gif, .svg | 25 MB |
| Documents | .pdf, .doc, .docx, .xls, .xlsx, .ppt, .pptx | 100 MB |
| Media | .mp4, .mov, .mp3 | 100 MB |
| Design | .sketch, .fig, .psd, .ai | 100 MB |
# Single screenshot
curl --location --request POST \
"https://${DOMAIN}.atlassian.net/rest/api/3/issue/BUG-456/attachments" \
-u "${EMAIL}:${API_TOKEN}" \
-H "X-Atlassian-Token: no-check" \
--form "file=@./qa-tests/screenshots/QA-20250105-001/error-state.png"
#!/bin/bash
ISSUE_KEY="$1"
SCREENSHOT_DIR="$2"
for file in "$SCREENSHOT_DIR"/*; do
echo "Uploading: $(basename $file)"
curl --silent --location --request POST \
"https://${DOMAIN}.atlassian.net/rest/api/3/issue/${ISSUE_KEY}/attachments" \
-u "${EMAIL}:${API_TOKEN}" \
-H "X-Atlassian-Token: no-check" \
--form "file=@\"$file\"" > /dev/null
done
echo "All files uploaded to ${ISSUE_KEY}"
# Upload PDF to specific page
PAGE_ID="123456789"
curl -u "${EMAIL}:${API_TOKEN}" \
-X POST \
-H "X-Atlassian-Token: nocheck" \
-F "file=@./docs/api-specification.pdf" \
-F "comment=API Spec v3.0" \
"https://${DOMAIN}.atlassian.net/wiki/rest/api/content/${PAGE_ID}/child/attachment"
{test-id}-{description}-{timestamp}.{ext}
Examples:
- QA-20250105-001-login-error-20250105T143022.png
- QA-20250105-001-form-validation-20250105T143045.png
{issue-key}-{description}.{ext}
Examples:
- BUG-123-stack-trace.txt
- BUG-123-screenshot-before.png
- BUG-123-screenshot-after.png
{feature}-{version}-{type}.{ext}
Examples:
- auth-flow-v2-diagram.png
- api-v3-specification.pdf
- deployment-guide-v1.docx
The QA test screenshots use a specific local directory structure that needs mapping when uploading to Confluence.
qa-tests/
├── active/
│ └── QA-20250105-001-login.md # Test document
└── screenshots/
└── QA-20250105-001/ # Test ID folder
├── 01-initial-state.png
├── 02-form-filled.png
├── 03-success-state.png
└── elements/
├── login-button.png
├── email-field.png
└── password-field.png
When uploading to Confluence, flatten the structure with prefixed names:
| Local Path | Confluence Attachment Name |
|---|---|
QA-20250105-001/01-initial-state.png | QA-20250105-001-01-initial-state.png |
QA-20250105-001/02-form-filled.png | QA-20250105-001-02-form-filled.png |
QA-20250105-001/elements/login-button.png | QA-20250105-001-elem-login-button.png |
QA-20250105-001/elements/email-field.png | QA-20250105-001-elem-email-field.png |
{test-id}- prefixelements/ get -elem- infix01-, 02- numbering#!/bin/bash
# upload-qa-screenshots-confluence.sh
# Upload QA screenshots to Confluence with proper naming
TEST_ID="$1"
PAGE_ID="$2"
LOCAL_DIR="qa-tests/screenshots/${TEST_ID}"
# Upload main screenshots
for file in "$LOCAL_DIR"/*.png; do
filename=$(basename "$file")
# Prefix with test-id
new_name="${TEST_ID}-${filename}"
echo "Uploading: $new_name"
curl -s -u "${ATLASSIAN_EMAIL}:${ATLASSIAN_API_TOKEN}" \
-X POST \
-H "X-Atlassian-Token: nocheck" \
-F "file=@${file};filename=${new_name}" \
"https://${ATLASSIAN_DOMAIN}.atlassian.net/wiki/rest/api/content/${PAGE_ID}/child/attachment"
done
# Upload element screenshots with elem- infix
if [ -d "$LOCAL_DIR/elements" ]; then
for file in "$LOCAL_DIR/elements"/*.png; do
filename=$(basename "$file")
# Add elem- infix
new_name="${TEST_ID}-elem-${filename}"
echo "Uploading element: $new_name"
curl -s -u "${ATLASSIAN_EMAIL}:${ATLASSIAN_API_TOKEN}" \
-X POST \
-H "X-Atlassian-Token: nocheck" \
-F "file=@${file};filename=${new_name}" \
"https://${ATLASSIAN_DOMAIN}.atlassian.net/wiki/rest/api/content/${PAGE_ID}/child/attachment"
done
fi
echo "Done! All screenshots uploaded to page ${PAGE_ID}"
import os
import requests
from requests.auth import HTTPBasicAuth
from pathlib import Path
def upload_qa_screenshots_to_confluence(
domain: str,
email: str,
api_token: str,
page_id: str,
test_id: str,
local_dir: str = "qa-tests/screenshots"
):
"""Upload QA screenshots with Confluence-compatible naming."""
base_url = f"https://{domain}.atlassian.net/wiki/rest/api"
auth = HTTPBasicAuth(email, api_token)
headers = {"X-Atlassian-Token": "nocheck"}
screenshot_dir = Path(local_dir) / test_id
uploaded = []
# Upload main screenshots
for file in screenshot_dir.glob("*.png"):
confluence_name = f"{test_id}-{file.name}"
with open(file, 'rb') as f:
files = {'file': (confluence_name, f, 'image/png')}
response = requests.post(
f"{base_url}/content/{page_id}/child/attachment",
auth=auth, headers=headers, files=files
)
if response.ok:
uploaded.append({"local": str(file), "confluence": confluence_name})
# Upload element screenshots
elements_dir = screenshot_dir / "elements"
if elements_dir.exists():
for file in elements_dir.glob("*.png"):
confluence_name = f"{test_id}-elem-{file.name}"
with open(file, 'rb') as f:
files = {'file': (confluence_name, f, 'image/png')}
response = requests.post(
f"{base_url}/content/{page_id}/child/attachment",
auth=auth, headers=headers, files=files
)
if response.ok:
uploaded.append({"local": str(file), "confluence": confluence_name})
return uploaded
# Generate mapping table for documentation
def generate_mapping_table(uploaded: list) -> str:
"""Generate markdown table of local-to-confluence name mapping."""
lines = ["| Local Path | Confluence Name |", "|------------|-----------------|"]
for item in uploaded:
lines.append(f"| `{item['local']}` | `{item['confluence']}` |")
return "\n".join(lines)
After upload, update the QA test document to use Confluence attachment names:
Before (local references):


After (Confluence references):


import re
from pathlib import Path
def update_qa_doc_for_confluence(qa_doc_path: str, test_id: str) -> str:
"""Update QA doc image references for Confluence."""
content = Path(qa_doc_path).read_text()
# Pattern: ./screenshots/{test-id}/filename.png
# Replace with: {test-id}-filename.png
content = re.sub(
rf'\./screenshots/{test_id}/([^)]+\.png)',
rf'{test_id}-\1',
content
)
# Pattern: ./screenshots/{test-id}/elements/filename.png
# Replace with: {test-id}-elem-filename.png
content = re.sub(
rf'{test_id}-elements/([^)]+\.png)',
rf'{test_id}-elem-\1',
content
)
return content
If a page was created with ri:url instead of ri:attachment, images won't display. To fix:
#!/bin/bash
# fix-confluence-image-refs.sh - Fix broken image references in Confluence page
PAGE_ID="$1"
# 1. Get current page content
curl -s -u "${ATLASSIAN_EMAIL}:${ATLASSIAN_API_TOKEN}" \
"https://${ATLASSIAN_DOMAIN}.atlassian.net/wiki/rest/api/content/${PAGE_ID}?expand=body.storage,version" \
> /tmp/page.json
VERSION=$(jq '.version.number' /tmp/page.json)
TITLE=$(jq -r '.title' /tmp/page.json)
# 2. Extract and fix the content
jq -r '.body.storage.value' /tmp/page.json > /tmp/content.html
# Fix: Replace ri:url with ri:attachment for local filenames (not full URLs)
# This converts: <ri:url ri:value="filename.png" />
# To: <ri:attachment ri:filename="filename.png" />
sed -i '' 's/<ri:url ri:value="\([^"]*\)" \/>/<ri:attachment ri:filename="\1" \/>/g' /tmp/content.html
# Remove ac:src attribute (not needed with ri:attachment)
sed -i '' 's/ ac:src="[^"]*"//g' /tmp/content.html
# 3. Update the page
NEW_VERSION=$((VERSION + 1))
CONTENT=$(cat /tmp/content.html | jq -Rs .)
curl -s -u "${ATLASSIAN_EMAIL}:${ATLASSIAN_API_TOKEN}" \
-X PUT \
-H "Content-Type: application/json" \
"https://${ATLASSIAN_DOMAIN}.atlassian.net/wiki/rest/api/content/${PAGE_ID}" \
-d "{
\"version\": {\"number\": ${NEW_VERSION}},
\"title\": \"${TITLE}\",
\"type\": \"page\",
\"body\": {
\"storage\": {
\"value\": ${CONTENT},
\"representation\": \"storage\"
}
}
}"
echo "Fixed image references in page ${PAGE_ID}"
Common symptoms of broken image references:
blob:https://media.staging.atl-paas.net/...Action: Add attachment to Jira issue
Issue: PROJ-123
File: /path/to/screenshot.png
Expected behavior:
- File uploaded to issue attachments
- Visible in Attachments section
- Downloadable by team members
Action: List all attachments on PROJ-123
Response format:
- screenshot.png (234 KB) - Added by John on 2025-01-05
- error-log.txt (12 KB) - Added by Jane on 2025-01-04
Action: Remove old-screenshot.png from PROJ-123
Note: Requires appropriate permissions
Action: Attach file to Confluence page
Space: TEAM
Page: "Sprint Review"
File: /path/to/presentation.pdf
Action: Embed image in page content
Space: TEAM
Page: "Architecture Overview"
File: system-diagram.png
Position: After "System Components" heading
Width: 800px
Action: Update existing attachment
Space: TEAM
Page: "API Docs"
File: api-spec-v2.pdf
Replace: api-spec-v1.pdf
When a QA test is executed:
Capture screenshots during test
qa-tests/screenshots/QA-20250105-001/
├── 01-initial-state.png
├── 02-form-filled.png
└── 03-error-state.png
Create/update Jira issue
Create bug: "Login form validation not working"
Project: QA
Attach evidence
Attach all screenshots from qa-tests/screenshots/QA-20250105-001/
to the created issue
Add summary comment
"Test execution evidence attached:
- [^01-initial-state.png] - Before test
- [^02-form-filled.png] - Form with test data
- [^03-error-state.png] - Error encountered"
Action: Upload QA test procedure to Confluence
Steps:
1. Convert QA markdown to Confluence format
2. Create/update page in QA space
3. Attach all element screenshots
4. Embed screenshots in page content
Prompt: "Upload all files from ./release-assets/ to the 'v2.0 Release' Confluence page"
Behavior:
- Scan directory for supported files
- Upload each file as attachment
- Report progress and results
Prompt: "Sync screenshots from ./qa-tests/screenshots/PROJ-123/ to Jira issue PROJ-123"
Behavior:
- Compare local files with existing attachments
- Upload new files
- Optionally replace updated files
- Skip unchanged files
| Error | Cause | Resolution |
|---|---|---|
| File too large | Exceeds size limit | Compress or split file |
| Unsupported type | Extension not allowed | Convert to supported format |
| Permission denied | No attach permission | Request project/space access |
| Issue not found | Invalid issue key | Verify issue exists |
| Page not found | Invalid page title/space | Check space key and page title |
If file > max size:
1. For images: Compress or resize
2. For documents: Split into parts
3. For archives: Use cloud storage link instead
Alternative: Upload to cloud storage and link in description
!filename.png! # Basic
!filename.png|width=600! # With width
!filename.png|thumbnail! # As thumbnail
[^filename.pdf] # Download link
[View Document^filename.pdf] # Custom link text
{gallery:include=*.png} # All PNG attachments
{gallery:include=screenshot-*} # Matching pattern
{viewfile:filename.pdf} # Inline PDF viewer
{viewfile:filename.pdf|height=600}