paperclip/scripts/paperclip-upload-artifact.sh
2026-05-30 20:51:46 +00:00

371 lines
10 KiB
Bash
Executable file

#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage:
scripts/paperclip-upload-artifact.sh FILE [options]
Uploads a generated file from the current workspace to the current Paperclip
issue, then creates an attachment-backed artifact work product by default.
Required environment for live uploads:
PAPERCLIP_API_URL, PAPERCLIP_API_KEY, PAPERCLIP_COMPANY_ID, PAPERCLIP_TASK_ID, PAPERCLIP_RUN_ID
Options:
--issue-id ID Issue id to attach to (default: PAPERCLIP_TASK_ID)
--company-id ID Company id (default: PAPERCLIP_COMPANY_ID)
--title TEXT Work product title (default: file basename)
--summary TEXT Work product summary
--content-type TYPE Override detected upload content type
--status STATUS Work product status (default: ready_for_review)
--no-work-product Only upload the issue attachment
--no-primary Do not mark the artifact work product primary for its type
--output FORMAT markdown or json (default: markdown)
--dry-run Print resolved upload settings without calling the API
--help, -h Show this help
Examples:
scripts/paperclip-upload-artifact.sh dist/demo.mp4 \
--title "Demo video render" \
--summary "MP4 render for board review"
scripts/paperclip-upload-artifact.sh out/walkthrough.webm \
--title "Walkthrough video" \
--content-type video/webm
EOF
}
require_command() {
if ! command -v "$1" >/dev/null 2>&1; then
printf 'Missing required command: %s\n' "$1" >&2
exit 1
fi
}
json_bool() {
if [[ "${1:-0}" == "1" ]]; then
printf 'true'
else
printf 'false'
fi
}
detect_content_type() {
local path="$1"
local lower
lower="$(printf '%s' "$path" | tr '[:upper:]' '[:lower:]')"
case "$lower" in
*.mp4|*.m4v) printf 'video/mp4' ;;
*.webm) printf 'video/webm' ;;
*.mov|*.qt) printf 'video/quicktime' ;;
*.png) printf 'image/png' ;;
*.jpg|*.jpeg) printf 'image/jpeg' ;;
*.gif) printf 'image/gif' ;;
*.webp) printf 'image/webp' ;;
*.svg) printf 'image/svg+xml' ;;
*.pdf) printf 'application/pdf' ;;
*.txt|*.log) printf 'text/plain' ;;
*.md|*.markdown) printf 'text/markdown' ;;
*.json) printf 'application/json' ;;
*.csv) printf 'text/csv' ;;
*.html|*.htm) printf 'text/html' ;;
*.zip) printf 'application/zip' ;;
*)
if command -v file >/dev/null 2>&1; then
file --brief --mime-type "$path"
else
printf 'application/octet-stream'
fi
;;
esac
}
request_json() {
local method="$1"
local url="$2"
local body="${3:-}"
local response_file
local status_code
response_file="$(mktemp)"
if [[ -n "$body" ]]; then
status_code="$(
curl -sS -X "$method" -w '%{http_code}' -o "$response_file" \
"$url" \
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
-H "X-Paperclip-Run-Id: $PAPERCLIP_RUN_ID" \
-H 'Content-Type: application/json' \
--data-binary "$body"
)"
else
status_code="$(
curl -sS -X "$method" -w '%{http_code}' -o "$response_file" \
"$url" \
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
-H "X-Paperclip-Run-Id: $PAPERCLIP_RUN_ID"
)"
fi
if [[ "$status_code" -lt 200 || "$status_code" -ge 300 ]]; then
printf 'Request failed (%s): %s\n' "$status_code" "$url" >&2
cat "$response_file" >&2
printf '\n' >&2
rm -f "$response_file"
exit 1
fi
cat "$response_file"
rm -f "$response_file"
}
upload_file() {
local url="$1"
local path="$2"
local content_type="$3"
local escaped_path
local response_file
local status_code
escaped_path="${path//\\/\\\\}"
escaped_path="${escaped_path//\"/\\\"}"
response_file="$(mktemp)"
status_code="$(
curl -sS -X POST -w '%{http_code}' -o "$response_file" \
"$url" \
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
-H "X-Paperclip-Run-Id: $PAPERCLIP_RUN_ID" \
-F "file=@\"${escaped_path}\";type=${content_type}"
)"
if [[ "$status_code" -lt 200 || "$status_code" -ge 300 ]]; then
printf 'Upload failed (%s): %s\n' "$status_code" "$url" >&2
cat "$response_file" >&2
printf '\n' >&2
rm -f "$response_file"
exit 1
fi
cat "$response_file"
rm -f "$response_file"
}
file_path=""
issue_id="${PAPERCLIP_TASK_ID:-}"
company_id="${PAPERCLIP_COMPANY_ID:-}"
title=""
summary=""
content_type=""
status="ready_for_review"
create_work_product=1
is_primary=1
output_format="markdown"
dry_run=0
while [[ $# -gt 0 ]]; do
case "$1" in
--issue-id)
issue_id="${2:-}"
shift 2
;;
--company-id)
company_id="${2:-}"
shift 2
;;
--title)
title="${2:-}"
shift 2
;;
--summary)
summary="${2:-}"
shift 2
;;
--content-type)
content_type="${2:-}"
shift 2
;;
--status)
status="${2:-}"
shift 2
;;
--no-work-product)
create_work_product=0
shift
;;
--no-primary)
is_primary=0
shift
;;
--output)
output_format="${2:-}"
shift 2
;;
--dry-run)
dry_run=1
shift
;;
--help|-h)
usage
exit 0
;;
--*)
printf 'Unknown argument: %s\n' "$1" >&2
usage >&2
exit 1
;;
*)
if [[ -n "$file_path" ]]; then
printf 'Unexpected positional argument: %s\n' "$1" >&2
usage >&2
exit 1
fi
file_path="$1"
shift
;;
esac
done
if [[ -z "$file_path" ]]; then
printf 'Missing file path.\n' >&2
usage >&2
exit 1
fi
if [[ ! -f "$file_path" ]]; then
printf 'Artifact file does not exist: %s\n' "$file_path" >&2
exit 1
fi
if [[ "$output_format" != "markdown" && "$output_format" != "json" ]]; then
printf 'Unsupported output format: %s\n' "$output_format" >&2
exit 1
fi
require_command curl
require_command jq
if [[ -z "$title" ]]; then
title="$(basename "$file_path")"
fi
if [[ -z "$content_type" ]]; then
content_type="$(detect_content_type "$file_path")"
fi
if [[ "$dry_run" == "1" ]]; then
create_work_product_json="$(json_bool "$create_work_product")"
is_primary_json="$(json_bool "$is_primary")"
jq -n \
--arg file "$file_path" \
--arg issueId "$issue_id" \
--arg companyId "$company_id" \
--arg title "$title" \
--arg summary "$summary" \
--arg contentType "$content_type" \
--arg status "$status" \
--argjson createWorkProduct "$create_work_product_json" \
--argjson isPrimary "$is_primary_json" \
'{file: $file, issueId: $issueId, companyId: $companyId, title: $title, summary: $summary, contentType: $contentType, status: $status, createWorkProduct: $createWorkProduct, isPrimary: $isPrimary}'
exit 0
fi
if [[ -z "${PAPERCLIP_API_URL:-}" || -z "${PAPERCLIP_API_KEY:-}" || -z "${PAPERCLIP_RUN_ID:-}" ]]; then
printf 'Missing PAPERCLIP_API_URL, PAPERCLIP_API_KEY, or PAPERCLIP_RUN_ID.\n' >&2
exit 1
fi
if [[ -z "$issue_id" || -z "$company_id" ]]; then
printf 'Missing issue or company id. Pass --issue-id/--company-id or set PAPERCLIP_TASK_ID/PAPERCLIP_COMPANY_ID.\n' >&2
exit 1
fi
api_base="${PAPERCLIP_API_URL%/}/api"
attachment="$(
upload_file \
"$api_base/companies/$company_id/issues/$issue_id/attachments" \
"$file_path" \
"$content_type"
)"
work_product="null"
if [[ "$create_work_product" == "1" ]]; then
is_primary_json="$(json_bool "$is_primary")"
attachment_id="$(jq -r '.id // empty' <<<"$attachment")"
byte_size="$(jq -r '.byteSize // 0' <<<"$attachment")"
content_path="$(jq -r '.contentPath // empty' <<<"$attachment")"
open_path="$(jq -r '.openPath // .contentPath // empty' <<<"$attachment")"
download_path="$(jq -r '.downloadPath // (if .contentPath then (.contentPath + "?download=1") else "" end)' <<<"$attachment")"
original_filename="$(jq -r '.originalFilename // empty' <<<"$attachment")"
if [[ -z "$attachment_id" || -z "$content_path" || -z "$download_path" ]]; then
printf 'Upload response did not include attachment path metadata.\n' >&2
printf '%s\n' "$attachment" >&2
exit 1
fi
work_product_payload="$(
jq -nc \
--arg title "$title" \
--arg summary "$summary" \
--arg status "$status" \
--arg runId "$PAPERCLIP_RUN_ID" \
--arg attachmentId "$attachment_id" \
--arg contentType "$content_type" \
--argjson byteSize "$byte_size" \
--arg contentPath "$content_path" \
--arg openPath "$open_path" \
--arg downloadPath "$download_path" \
--arg originalFilename "$original_filename" \
--argjson isPrimary "$is_primary_json" \
'{
type: "artifact",
provider: "paperclip",
title: $title,
status: $status,
reviewState: "none",
isPrimary: $isPrimary,
healthStatus: "unknown",
summary: (if $summary == "" then null else $summary end),
createdByRunId: $runId,
metadata: {
attachmentId: $attachmentId,
contentType: $contentType,
byteSize: $byteSize,
contentPath: $contentPath,
openPath: $openPath,
downloadPath: $downloadPath,
originalFilename: (if $originalFilename == "" then null else $originalFilename end)
}
}'
)"
work_product="$(
request_json \
POST \
"$api_base/issues/$issue_id/work-products" \
"$work_product_payload"
)"
fi
if [[ "$output_format" == "json" ]]; then
jq -n --argjson attachment "$attachment" --argjson workProduct "$work_product" \
'{attachment: $attachment, workProduct: $workProduct}'
exit 0
fi
content_path="$(jq -r '.contentPath // empty' <<<"$attachment")"
download_path="$(jq -r '.downloadPath // (if .contentPath then (.contentPath + "?download=1") else "" end)' <<<"$attachment")"
attachment_id="$(jq -r '.id // empty' <<<"$attachment")"
work_product_id="$(jq -r '.id // empty' <<<"$work_product")"
printf 'Uploaded artifact\n\n'
printf -- '- Attachment: [%s](%s)\n' "$title" "$content_path"
printf -- '- Download: [%s](%s)\n' "$title" "$download_path"
printf -- '- Attachment ID: `%s`\n' "$attachment_id"
if [[ -n "$work_product_id" ]]; then
printf -- '- Work product ID: `%s`\n' "$work_product_id"
fi
printf '\nFinal comment snippet:\n\n'
printf -- '- Artifact: [%s](%s)\n' "$title" "$content_path"