mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-15 02:20:38 +09:00
371 lines
10 KiB
Bash
Executable file
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"
|