diff --git a/.agents/skills/create-pr/SKILL.md b/.agents/skills/create-pr/SKILL.md index f0ce0b804..068264d21 100644 --- a/.agents/skills/create-pr/SKILL.md +++ b/.agents/skills/create-pr/SKILL.md @@ -40,7 +40,7 @@ Create a reviewable PR from the exact commits intended for publication. ``` Use `before: absent` for a new state and `after: removed` for a deleted state. A capture failure is blocking; record its reason instead of silently omitting the state. -9. Upload every local image as a GitHub user asset while composing the PR. Use the available GitHub/`gh` CLI workflow; do not commit screenshots to the source branch or another repository. +9. Upload every local image as a GitHub user asset by invoking `$upload-github-attachment` while composing the PR. 10. Put all pairs under `## Visual changes`, with an image row followed by its component or page name row: ```markdown @@ -50,7 +50,7 @@ Create a reviewable PR from the exact commits intended for publication. | Settings / Connection | Settings / Connection | ``` -11. Verify that every user-asset URL renders in the created PR. Remove temporary worktrees only after upload succeeds; clear ignored `.vishot` captures when they are no longer useful locally. +11. Verify that every user-asset URL matches the PR intent. Remove temporary worktrees only after upload succeeds; clear ignored `.vishot` captures when they are no longer useful locally. ## Visual Evidence Contract diff --git a/.agents/skills/upload-github-attachment/SKILL.md b/.agents/skills/upload-github-attachment/SKILL.md new file mode 100644 index 000000000..3793841be --- /dev/null +++ b/.agents/skills/upload-github-attachment/SKILL.md @@ -0,0 +1,41 @@ +--- +name: upload-github-attachment +description: Upload a local image or file to GitHub's user-attachments storage and return a URL suitable for issue, pull request, discussion, or comment Markdown. Use when Codex must embed local screenshots, videos, logs, or other evidence in GitHub content without committing the files to a repository. +--- + +# Upload GitHub Attachment + +Use the bundled script so authentication, repository resolution, MIME detection, and URL extraction remain consistent. + +## Requirements + +- Require the `gh` CLI and `curl`. +- Prefer an existing `gh auth login` session. +- When interactive `gh` authentication is unavailable, require `GH_TOKEN` or `GITHUB_TOKEN` in the environment. +- Stop and ask the user to configure one of those authentication methods if `gh auth token` cannot resolve a token. Never print, persist, or interpolate the token into diagnostic output. + +## Upload + +Run from a checkout of the destination repository: + +```bash +.agents/skills/upload-github-attachment/scripts/upload-github-attachment.sh /absolute/path/to/image.png +``` + +Specify the repository when the current directory cannot resolve it: + +```bash +.agents/skills/upload-github-attachment/scripts/upload-github-attachment.sh \ + --repo moeru-ai/airi \ + /absolute/path/to/image.png +``` + +The script prints only the final `https://github.com/user-attachments/assets/...` URL. Embed it with `![](URL)` for an image or `[label](URL)` for another file. + +## Verification + +1. Request the returned URL and follow redirects. +2. Require HTTP 200 and the expected content type. +3. After creating or editing the GitHub content, reopen it and verify that the attachment renders. + +The upload endpoint is currently undocumented by GitHub. Treat a rejected request as a blocking capability change instead of falling back to committing PR-only artifacts. Background and the observed Bearer-token flow: . diff --git a/.agents/skills/upload-github-attachment/agents/openai.yaml b/.agents/skills/upload-github-attachment/agents/openai.yaml new file mode 100644 index 000000000..5e9ad9eda --- /dev/null +++ b/.agents/skills/upload-github-attachment/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Upload GitHub Attachment" + short_description: "Upload files as GitHub user attachments" + default_prompt: "Use $upload-github-attachment to upload this file for a GitHub issue or pull request." diff --git a/.agents/skills/upload-github-attachment/scripts/upload-github-attachment.sh b/.agents/skills/upload-github-attachment/scripts/upload-github-attachment.sh new file mode 100755 index 000000000..37990011b --- /dev/null +++ b/.agents/skills/upload-github-attachment/scripts/upload-github-attachment.sh @@ -0,0 +1,105 @@ +#!/usr/bin/env bash + +set -euo pipefail + +usage() { + printf 'Usage: %s [--repo OWNER/REPO] [--content-type MIME] FILE\n' "$(basename "$0")" +} + +repository='' +content_type='' + +while (($# > 0)); do + case "$1" in + --repo) + repository="${2:-}" + shift 2 + ;; + --content-type) + content_type="${2:-}" + shift 2 + ;; + --help|-h) + usage + exit 0 + ;; + --*) + printf 'Unknown option: %s\n' "$1" >&2 + usage >&2 + exit 2 + ;; + *) + if [[ -n "${attachment_file:-}" ]]; then + printf 'Only one file can be uploaded per invocation.\n' >&2 + exit 2 + fi + attachment_file="$1" + shift + ;; + esac +done + +if ! command -v gh >/dev/null 2>&1; then + printf 'GitHub CLI (gh) is required. Install it, then run gh auth login or set GH_TOKEN/GITHUB_TOKEN.\n' >&2 + exit 127 +fi + +if ! command -v curl >/dev/null 2>&1; then + printf 'curl is required.\n' >&2 + exit 127 +fi + +if [[ -z "${attachment_file:-}" || ! -f "$attachment_file" ]]; then + printf 'Provide one existing local file.\n' >&2 + usage >&2 + exit 2 +fi + +if [[ -z "$repository" ]]; then + repository="$(gh repo view --json nameWithOwner --jq .nameWithOwner 2>/dev/null || true)" +fi + +if [[ -z "$repository" ]]; then + printf 'Could not resolve a GitHub repository. Run inside a checkout or pass --repo OWNER/REPO.\n' >&2 + exit 2 +fi + +attachment_token="${GH_TOKEN:-${GITHUB_TOKEN:-}}" +if [[ -z "$attachment_token" ]]; then + attachment_token="$(gh auth token 2>/dev/null || true)" +fi + +if [[ -z "$attachment_token" ]]; then + printf 'GitHub authentication is required. Run gh auth login or set GH_TOKEN/GITHUB_TOKEN.\n' >&2 + exit 1 +fi + +if [[ -z "$content_type" ]]; then + if ! command -v file >/dev/null 2>&1; then + printf 'The file utility is required for MIME detection; install it or pass --content-type.\n' >&2 + exit 127 + fi + content_type="$(file --brief --mime-type "$attachment_file")" +fi + +repository_id="$(GH_TOKEN="$attachment_token" gh api "repos/$repository" --jq .id)" +attachment_name="$(basename "$attachment_file")" + +response="$(curl --silent --show-error --fail-with-body \ + --request POST \ + --header "Authorization: Bearer $attachment_token" \ + --header 'Accept: application/json' \ + --url-query "name=$attachment_name" \ + --url-query "content_type=$content_type" \ + --url-query "repository_id=$repository_id" \ + --data-binary "@$attachment_file" \ + 'https://uploads.github.com/user-attachments/assets')" + +attachment_url="$(printf '%s' "$response" | sed -n 's/.*"url"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')" + +if [[ "$attachment_url" != https://github.com/user-attachments/* ]]; then + printf 'GitHub did not return a user-attachment URL.\n' >&2 + exit 1 +fi + +printf '%s\n' "$attachment_url"