mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-14 01:50:39 +09:00
Bundle artifact upload helper with Paperclip skill
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
e7cdd0f8c5
commit
c4bb68c14b
8 changed files with 18 additions and 14 deletions
|
|
@ -98,7 +98,7 @@ If `currentParticipant` does not match you, do not try to advance the stage —
|
|||
|
||||
When work produces a user-inspectable file, upload it to the current issue before final disposition. Local filesystem paths are not enough because board users, reviewers, and cloud operators may not have access to the agent workspace.
|
||||
|
||||
Use the repo helper when it exists in the active workspace:
|
||||
Use the helper bundled with this skill. From an installed `paperclip` skill directory, the helper lives at `scripts/paperclip-upload-artifact.sh`:
|
||||
|
||||
```bash
|
||||
scripts/paperclip-upload-artifact.sh path/to/output.webm \
|
||||
|
|
|
|||
371
skills/paperclip/scripts/paperclip-upload-artifact.sh
Executable file
371
skills/paperclip/scripts/paperclip-upload-artifact.sh
Executable file
|
|
@ -0,0 +1,371 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
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"
|
||||
Loading…
Add table
Add a link
Reference in a new issue