mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-15 02:20:38 +09:00
Add agent artifact upload workflow
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
75f88c588c
commit
0bd13c23a9
6 changed files with 502 additions and 0 deletions
368
scripts/paperclip-upload-artifact.sh
Executable file
368
scripts/paperclip-upload-artifact.sh
Executable file
|
|
@ -0,0 +1,368 @@
|
|||
#!/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 response_file
|
||||
local status_code
|
||||
|
||||
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=@${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"
|
||||
Loading…
Add table
Add a link
Reference in a new issue