From ce96524586d13245dc76c0403e978e63ab904857 Mon Sep 17 00:00:00 2001 From: openhands Date: Sat, 16 May 2026 03:55:13 +0000 Subject: [PATCH] Add github-event-poller skill Adds a new keyword-triggered skill that streamlines creating an OpenHands automation which polls a GitHub repository on a cron schedule for new events (issues, PRs, releases, comments, push, etc.) and runs user-defined handler instructions against each event. Key design points: - Complements (does not replace) the existing webhook-based 'source: github' event trigger in 'openhands-automation'. Polling fills the gap for repos where the user doesn't own webhook config (e.g. monitoring a third-party public repo). - Uses the prompt-preset endpoint only; no custom SDK / tarball code. - Auth strategy is decided at runtime inside the sandbox: prefer any 'mcp__github' tools if a GitHub MCP server is configured, else fall back to the GITHUB_TOKEN env var via curl. If neither is available the run fails fast with a clear error. - Deduplication via a lookback window (defaults to 2x cron interval) with a soft handler-level dedup hint for write actions. - Bundled one-shot wrapper at scripts/create_poller.sh that builds the prompt from references/runtime-prompt-template.md and POSTs to /api/automation/v1/preset/prompt. - Activation via keyword triggers (poll/watch/monitor github repo, github polling, cron github, etc.) plus a commands/github-poller:create.md slash entry (modern convention; no deprecated slash trigger in frontmatter). End-to-end validated against OpenHands/OpenHands on a local automation stack: preset POST -> 201, manual dispatch -> run transitioned PENDING -> RUNNING. README catalog and marketplace JSON regenerated via scripts/sync_extensions.py. Co-authored-by: openhands --- README.md | 5 +- marketplaces/openhands-extensions.json | 14 ++ skills/github-event-poller/SKILL.md | 160 ++++++++++++++++++ .../commands/github-poller:create.md | 7 + .../references/runtime-prompt-template.md | 110 ++++++++++++ .../scripts/create_poller.sh | 130 ++++++++++++++ 6 files changed, 424 insertions(+), 2 deletions(-) create mode 100644 skills/github-event-poller/SKILL.md create mode 100644 skills/github-event-poller/commands/github-poller:create.md create mode 100644 skills/github-event-poller/references/runtime-prompt-template.md create mode 100755 skills/github-event-poller/scripts/create_poller.sh diff --git a/README.md b/README.md index 73923964..c5dd74d4 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ Browse available plugins in [`plugins/`](plugins/). ## Extensions Catalog -This repository contains **2 marketplace(s)** with **48 extensions** (38 skills, 10 plugins). +This repository contains **2 marketplace(s)** with **49 extensions** (39 skills, 10 plugins). ### large-codebase @@ -51,7 +51,7 @@ OpenHands skills for interacting, improving, and refactoring large codebases Official skills and plugins for OpenHands — the open-source AI software engineer. -**44 extensions** (36 skills, 8 plugins) +**45 extensions** (37 skills, 8 plugins) | Name | Type | Description | Commands | |------|------|-------------|----------| @@ -71,6 +71,7 @@ Official skills and plugins for OpenHands — the open-source AI software engine | flarglebargle | skill | A test skill that responds to the magic word 'flarglebargle' with a compliment. Use for testing skill activation and ... | — | | frontend-design | skill | Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks ... | — | | github | skill | Interact with GitHub repositories, pull requests, issues, and workflows using the GITHUB_TOKEN environment variable a... | — | +| github-event-poller | skill | Create an OpenHands automation that polls a GitHub repository on a cron schedule for new events (issues, PRs, release... | — | | github-pr-review | skill | Post structured PR reviews to GitHub with inline comments/suggestions in a single API call. | `/github-pr-review` | | gitlab | skill | Interact with GitLab repositories, merge requests, and APIs using the GITLAB_TOKEN environment variable. Use when wor... | — | | iterate | skill | Iterate on a GitHub pull request — drive it through CI, code review, and QA until merge-ready. Monitors state, fixes ... | `/iterate`, `/verify`, `/babysit` | diff --git a/marketplaces/openhands-extensions.json b/marketplaces/openhands-extensions.json index 6bc1e6a3..1a1a9f14 100644 --- a/marketplaces/openhands-extensions.json +++ b/marketplaces/openhands-extensions.json @@ -559,6 +559,20 @@ "pull-request", "iterate" ] + }, + { + "name": "github-event-poller", + "source": "./skills/github-event-poller", + "description": "Create an OpenHands automation that polls a GitHub repository on a cron schedule for new events (issues, PRs, releases, comments, push, etc.) and runs user-defined handler instructions. Prefers a configured GitHub MCP server in the sandbox, falls back to a GITHUB_TOKEN environment variable.", + "category": "integration", + "keywords": [ + "github", + "automation", + "cron", + "polling", + "events", + "monitoring" + ] } ] } diff --git a/skills/github-event-poller/SKILL.md b/skills/github-event-poller/SKILL.md new file mode 100644 index 00000000..687c0d87 --- /dev/null +++ b/skills/github-event-poller/SKILL.md @@ -0,0 +1,160 @@ +--- +name: github-event-poller +description: This skill should be used when the user wants to create an OpenHands automation that polls a GitHub repository for new events on a schedule (e.g. "watch a repo for new issues / PRs / releases / comments", "poll GitHub for events", "set up a recurring GitHub activity check"). It scaffolds and registers a cron-triggered OpenHands automation that reads the GitHub Events / REST API (preferring a GitHub MCP server if configured, falling back to a `GITHUB_TOKEN` env var) and runs user-defined handler instructions against each new event. +triggers: + - poll github + - poll a github repo + - poll repo events + - poll repository events + - github event poller + - github events automation + - github activity automation + - watch github repo + - watch a github repository + - monitor github repo + - monitor a github repository + - github polling + - schedule github poll + - cron github + - github cron job +--- + +# GitHub Event Poller + +Creates a **cron-triggered OpenHands automation** that periodically polls a GitHub repository for new events and runs user-defined handler instructions on them. + +This is different from the built-in GitHub *webhook* trigger: + +| Capability | Webhook trigger (`source: github`) | This skill (cron + REST polling) | +| ---------------------- | ----------------------------------------------- | ------------------------------------------- | +| Needs webhook access | Yes (must own the repo / org or App install) | **No** — any repo readable by your token | +| Latency | Real-time | As frequent as your cron (e.g. every 5 min) | +| Public-repo monitoring | Hard (you don't own webhook config) | **Easy** — just read public events | +| State / deduplication | Handled by webhook delivery | Lookback window (managed by this skill) | + +Use this skill when the user **doesn't own** the target repo's webhook config, or when they explicitly want a polling model. + +## Prerequisites — confirm before creating + +Before calling the API, verify each of these. If anything is missing, ask the user. + +1. **Target repo** — `owner/repo` (e.g. `OpenHands/OpenHands`). +2. **Event types of interest** — any combination of: + `issues`, `pull_request`, `issue_comment`, `pull_request_review`, `pull_request_review_comment`, `push`, `release`, `create`, `delete`, `fork`, `watch`, `member`, `public`. + GitHub's `/repos/{owner}/{repo}/events` returns these via the unified Events API. +3. **Cron schedule** — default `*/15 * * * *` (every 15 minutes). Don't go below 5 minutes against the GitHub API. +4. **Handler instructions** — natural-language description of what to do for each new event. Should be parameterizable by event type if needed. +5. **Auth strategy for the automation sandbox**: + - **Preferred:** a GitHub MCP server is configured in the user's OpenHands Cloud account. Check by asking the user (or with the API listed below). The automation's runtime prompt instructs the agent to *prefer MCP tools* when present. + - **Fallback:** the automation sandbox has a `GITHUB_TOKEN` secret. The runtime prompt falls back to `curl https://api.github.com/...` with that token. + - At least one of these **must** be available, or the automation will be unable to read GitHub. If neither is present, tell the user and stop — do not create the automation. + +### Checking for a configured GitHub MCP server + +Ask the user, or inspect their account settings via the OpenHands API (host comes from `` if provided, else `https://app.all-hands.dev`): + +```bash +curl -s "${OPENHANDS_HOST}/api/conversations/settings" \ + -H "Authorization: Bearer ${OPENHANDS_API_KEY}" \ + | python3 -c 'import sys,json; d=json.load(sys.stdin); print(json.dumps(d.get("mcp_config",{}),indent=2))' +``` + +Look for an entry whose name or `command`/`url` references `github`, `mcp-github`, or `github-mcp-server`. If none, fall back to `GITHUB_TOKEN`. + +### Checking that GITHUB_TOKEN is available + +```bash +curl -s "${OPENHANDS_HOST}/api/automation/v1" \ + -H "Authorization: Bearer ${OPENHANDS_API_KEY}" | head -c 200 >/dev/null # smoke-test API key +``` + +`GITHUB_TOKEN` is an OpenHands Cloud secret and is injected automatically into automation sandboxes when registered. If the user isn't sure, have them add it under **Settings → Secrets**. + +## How to create the automation + +Always use the **prompt preset** (`POST /api/automation/v1/preset/prompt`). Never write a custom SDK script for this skill. + +The body has three parameterised pieces: + +1. **Cron trigger** — `trigger.schedule`, `trigger.timezone`. +2. **Repos** — clone the target repo into the sandbox so its skills (`AGENTS.md`, etc.) are auto-loaded and the agent can run repo-aware actions. +3. **Prompt** — built from the template in `references/runtime-prompt-template.md`. Substitute these placeholders: + - `{{OWNER_REPO}}` — e.g. `OpenHands/OpenHands` + - `{{EVENT_TYPES}}` — JSON array of event types + - `{{LOOKBACK_MINUTES}}` — typically `cron_interval_minutes * 2` (safety overlap) + - `{{HANDLER_INSTRUCTIONS}}` — user-supplied instructions block + - `{{MAX_EVENTS}}` — cap (default `50`) so runs stay bounded + +### One-shot create (recommended) + +The bundled script `scripts/create_poller.sh` does the substitution and POSTs in one step: + +```bash +.agents/skills/github-event-poller/scripts/create_poller.sh \ + --repo OpenHands/OpenHands \ + --events "issues,pull_request,release" \ + --schedule "*/15 * * * *" \ + --name "OpenHands repo activity watcher" \ + --handler "For each new issue or PR, summarise it in 1 sentence and print it. For each release, print version + URL." \ + --host "${OPENHANDS_HOST:-https://app.all-hands.dev}" +``` + +Required env: `OPENHANDS_API_KEY` (or the local `$OPENHANDS_AUTOMATION_API_KEY` for the in-sandbox automation service — see "Local dev stack" below). + +The script prints the new automation's `id` on success. + +### Manual create (when the script can't be used) + +```bash +PROMPT=$(sed \ + -e 's|{{OWNER_REPO}}|OpenHands/OpenHands|g' \ + -e 's|{{EVENT_TYPES}}|["issues","pull_request","release"]|g' \ + -e 's|{{LOOKBACK_MINUTES}}|30|g' \ + -e 's|{{MAX_EVENTS}}|50|g' \ + -e 's|{{HANDLER_INSTRUCTIONS}}|For each new issue or PR, summarise it in one sentence.|g' \ + .agents/skills/github-event-poller/references/runtime-prompt-template.md) + +jq -n --arg name "OpenHands repo activity watcher" --arg prompt "$PROMPT" '{ + name: $name, + prompt: $prompt, + trigger: {type:"cron", schedule:"*/15 * * * *", timezone:"UTC"}, + timeout: 600, + repos: [{url: "https://github.com/OpenHands/OpenHands", ref: "main"}] +}' | curl -s -X POST "${OPENHANDS_HOST}/api/automation/v1/preset/prompt" \ + -H "Authorization: Bearer ${OPENHANDS_API_KEY}" \ + -H "Content-Type: application/json" \ + --data @- +``` + +## After creating + +1. **Verify** the automation appears in `GET /api/automation/v1`. +2. **Dispatch once** to confirm it runs end-to-end: + `POST /api/automation/v1/{id}/dispatch`. +3. **Inspect a run** with `GET /api/automation/v1/{id}/runs?limit=5` and look for `status: COMPLETED`. +4. Show the user the automation `id`, the chosen schedule, and a link to the run in the UI. + +## Local dev stack (this sandbox) + +If `$OPENHANDS_AUTOMATION_API_KEY` is set, the automation backend is running locally at +`http://host.docker.internal:18001`. Use: + +- `Host`: `http://host.docker.internal:18001` +- `Auth header`: `Authorization: Bearer $OPENHANDS_AUTOMATION_API_KEY` + (the `/v1` routes on the local stack do **not** accept `X-API-Key` — only Bearer) + +The OpenAPI spec is at `${HOST}/api/automation/openapi.json`. This is useful for testing the skill end-to-end without touching production OpenHands Cloud. + +## Files in this skill + +- `SKILL.md` — this file. The decision flow / API recipe. +- `references/runtime-prompt-template.md` — the prompt that the **automation's sandbox agent** runs every cron tick. Contains the MCP-vs-token decision logic and the polling/dedup loop. +- `scripts/create_poller.sh` — one-shot wrapper that substitutes the template, builds the JSON body and POSTs it to the preset endpoint. + +## Tips & gotchas + +- **Rate limits.** Unauthenticated GitHub API = 60 req/h. Authenticated = 5,000 req/h. Always run through MCP or `GITHUB_TOKEN`. +- **Events API window.** `/repos/{owner}/{repo}/events` returns up to ~300 events over ~90 days and is cached for ~60 s. Schedules tighter than 1 min are wasteful. +- **Deduplication.** Polling can re-see the same event when runs overlap. The runtime prompt filters events to `created_at >= now - LOOKBACK_MINUTES`; if you need stronger dedup, instruct the handler to check whether it has already acted (e.g. "before commenting on a PR, search for an existing comment authored by you"). +- **Push events** can be huge. If watching `push`, cap the handler with `MAX_EVENTS` and consider filtering to a specific branch in the handler instructions. +- **Don't include secrets in the prompt.** The runtime prompt references `$GITHUB_TOKEN`, which is resolved inside the sandbox — never inline the token value into the automation prompt or JSON body. diff --git a/skills/github-event-poller/commands/github-poller:create.md b/skills/github-event-poller/commands/github-poller:create.md new file mode 100644 index 00000000..040f0531 --- /dev/null +++ b/skills/github-event-poller/commands/github-poller:create.md @@ -0,0 +1,7 @@ +--- +description: Create an OpenHands automation that polls a GitHub repository on a cron schedule for new events (issues, PRs, releases, comments, push, etc.) and runs user-defined handler instructions on each event. Prefers a GitHub MCP server in the sandbox, falls back to GITHUB_TOKEN. +--- + +Read and follow the complete instructions in the SKILL.md file located in this skill's directory. + +$ARGUMENTS diff --git a/skills/github-event-poller/references/runtime-prompt-template.md b/skills/github-event-poller/references/runtime-prompt-template.md new file mode 100644 index 00000000..cffb9159 --- /dev/null +++ b/skills/github-event-poller/references/runtime-prompt-template.md @@ -0,0 +1,110 @@ +You are running as a scheduled OpenHands automation that polls a single GitHub +repository for new events and acts on them. + +## Configuration (filled in at automation-creation time) + +- Target repository: `{{OWNER_REPO}}` +- Event types of interest: `{{EVENT_TYPES}}` (subset of: + issues, pull_request, issue_comment, pull_request_review, + pull_request_review_comment, push, release, create, delete, fork, watch, + member, public) +- Lookback window: `{{LOOKBACK_MINUTES}}` minutes (events older than this are ignored) +- Max events to process this run: `{{MAX_EVENTS}}` + +## Step 1 — Choose the GitHub access path + +Decide ONCE at the start of the run, then use the same path for every call: + +1. Inspect the tools available in this conversation. If you have any tool whose + name begins with `mcp__github` (e.g. `mcp__github__list_issues`, + `mcp__github__list_pulls`, `mcp__github__list_repository_events`), prefer + those — they handle auth, pagination and rate limits for you. + +2. Otherwise, check that the environment variable `GITHUB_TOKEN` is set: + + test -n "$GITHUB_TOKEN" && echo "GITHUB_TOKEN available" || echo "missing" + + If it IS set, use it via curl: + + curl -sS \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + -H "Authorization: Bearer $GITHUB_TOKEN" \ + "https://api.github.com/repos/{{OWNER_REPO}}/events?per_page=100" + +3. If neither is available, STOP the run with a clear error message + ("No GitHub MCP server and no GITHUB_TOKEN — cannot poll {{OWNER_REPO}}.") + and do not attempt further work. + +Record at the top of your output which path you chose. + +## Step 2 — Fetch recent events + +Fetch the repo's public event feed: + +- MCP path: call the events / activity tool on `{{OWNER_REPO}}`, requesting + the most recent page (per_page=100). +- Token path: curl `GET /repos/{{OWNER_REPO}}/events?per_page=100` as above. + +The response is a JSON array of event objects, each with at least: +`id`, `type`, `created_at`, `actor.login`, `repo.name`, `payload`. + +## Step 3 — Filter to new + relevant events + +Apply these filters, in order: + +1. Drop events whose `type` is not in `{{EVENT_TYPES}}` (the GitHub event + type names, e.g. `IssuesEvent`, `PullRequestEvent`, `PushEvent`, + `ReleaseEvent`, `IssueCommentEvent`, `PullRequestReviewEvent`, + `PullRequestReviewCommentEvent`). + Mapping of webhook-style names → REST event-feed names: + issues → IssuesEvent + pull_request → PullRequestEvent + issue_comment → IssueCommentEvent + pull_request_review → PullRequestReviewEvent + pull_request_review_comment → PullRequestReviewCommentEvent + push → PushEvent + release → ReleaseEvent + create → CreateEvent + delete → DeleteEvent + fork → ForkEvent + watch → WatchEvent + member → MemberEvent + public → PublicEvent + +2. Drop events older than `now() - {{LOOKBACK_MINUTES}} minutes` based on + `created_at` (UTC ISO-8601). + +3. Sort ascending by `created_at` so you process oldest-first. + +4. Truncate to the first `{{MAX_EVENTS}}` events. + +If zero events survive filtering, log "No new events." and exit 0. + +## Step 4 — Per-event handler + +For each surviving event, follow the handler instructions below. Print one +clearly-delimited block per event with: + +- the event `id`, `type`, `actor.login`, `created_at` +- a short summary derived from `payload` +- any actions taken / output produced + +### Handler instructions (user-supplied) + +{{HANDLER_INSTRUCTIONS}} + +If the handler instructions ask you to comment on or modify GitHub resources, +use the same access path you chose in Step 1 (MCP write tool, or `curl -X +POST/PATCH …` with `$GITHUB_TOKEN`). Before posting any comment, search for an +existing comment by the same automation actor on the same issue/PR and skip if +one already exists (best-effort dedup). + +## Step 5 — Wrap up + +Print a final one-line summary: + + PROCESSED {{OWNER_REPO}} events_seen= events_processed= path= + +That single line lets the user grep through run logs to confirm the poller is +healthy. diff --git a/skills/github-event-poller/scripts/create_poller.sh b/skills/github-event-poller/scripts/create_poller.sh new file mode 100755 index 00000000..c6623b37 --- /dev/null +++ b/skills/github-event-poller/scripts/create_poller.sh @@ -0,0 +1,130 @@ +#!/usr/bin/env bash +# Create an OpenHands automation that polls a GitHub repo for new events. +# +# Required env (one of): +# OPENHANDS_API_KEY -> production / Cloud (Authorization: Bearer) +# OPENHANDS_AUTOMATION_API_KEY -> local dev stack (X-API-Key) +# +# Usage: +# create_poller.sh \ +# --repo OpenHands/OpenHands \ +# --events "issues,pull_request,release" \ +# --schedule "*/15 * * * *" \ +# --name "OpenHands activity watcher" \ +# --handler "For each event, print a one-line summary." \ +# [--timezone UTC] [--lookback 30] [--max-events 50] [--timeout 600] \ +# [--host https://app.all-hands.dev] [--ref main] [--dry-run] +set -euo pipefail + +repo=""; events=""; schedule="*/15 * * * *"; name=""; handler="" +timezone="UTC"; lookback=""; max_events=50; timeout=600 +host="${OPENHANDS_HOST:-https://app.all-hands.dev}"; ref="main"; dry_run=0 + +usage() { sed -n '1,30p' "$0"; exit 1; } + +while [[ $# -gt 0 ]]; do + case "$1" in + --repo) repo="$2"; shift 2;; + --events) events="$2"; shift 2;; + --schedule) schedule="$2"; shift 2;; + --name) name="$2"; shift 2;; + --handler) handler="$2"; shift 2;; + --timezone) timezone="$2"; shift 2;; + --lookback) lookback="$2"; shift 2;; + --max-events) max_events="$2"; shift 2;; + --timeout) timeout="$2"; shift 2;; + --host) host="$2"; shift 2;; + --ref) ref="$2"; shift 2;; + --dry-run) dry_run=1; shift;; + -h|--help) usage;; + *) echo "Unknown arg: $1" >&2; usage;; + esac +done + +[[ -z "$repo" || -z "$events" || -z "$name" || -z "$handler" ]] && { echo "Missing required args" >&2; usage; } + +# Default lookback = 2x cron interval (in minutes) when on a "*/N" schedule, else 30. +if [[ -z "$lookback" ]]; then + if [[ "$schedule" =~ ^\*/([0-9]+)\ \*\ \*\ \*\ \*$ ]]; then + lookback=$(( ${BASH_REMATCH[1]} * 2 )) + else + lookback=30 + fi +fi + +# Auth header selection. Both keys are passed as Bearer tokens; the local dev +# stack accepts the automation key as a Bearer too (the X-API-Key header is +# only honoured by certain non-/v1 ingress routes). +if [[ -n "${OPENHANDS_API_KEY:-}" ]]; then + auth_header="Authorization: Bearer ${OPENHANDS_API_KEY}" +elif [[ -n "${OPENHANDS_AUTOMATION_API_KEY:-}" ]]; then + auth_header="Authorization: Bearer ${OPENHANDS_AUTOMATION_API_KEY}" +else + echo "ERROR: set OPENHANDS_API_KEY or OPENHANDS_AUTOMATION_API_KEY" >&2; exit 2 +fi + +# Build events JSON array from CSV. +events_json=$(python3 -c ' +import json, sys +print(json.dumps([e.strip() for e in sys.argv[1].split(",") if e.strip()])) +' "$events") + +# Load template. +tpl_path="$(cd "$(dirname "$0")/.." && pwd)/references/runtime-prompt-template.md" +[[ -f "$tpl_path" ]] || { echo "ERROR: template not found at $tpl_path" >&2; exit 3; } + +# Substitute placeholders -- use python to avoid sed escaping pain. +prompt=$(python3 - "$tpl_path" "$repo" "$events_json" "$lookback" "$max_events" "$handler" <<'PY' +import sys, pathlib +tpl, repo, events_json, lookback, max_events, handler = sys.argv[1:7] +text = pathlib.Path(tpl).read_text() +for k, v in [ + ("{{OWNER_REPO}}", repo), + ("{{EVENT_TYPES}}", events_json), + ("{{LOOKBACK_MINUTES}}", str(lookback)), + ("{{MAX_EVENTS}}", str(max_events)), + ("{{HANDLER_INSTRUCTIONS}}", handler), +]: + text = text.replace(k, v) +sys.stdout.write(text) +PY +) + +# Build request body. +body=$(python3 -c ' +import json, sys +name, prompt, schedule, tz, timeout, repo, ref = sys.argv[1:8] +print(json.dumps({ + "name": name, + "prompt": prompt, + "trigger": {"type":"cron","schedule":schedule,"timezone":tz}, + "timeout": int(timeout), + "repos": [{"url": f"https://github.com/{repo}", "ref": ref}], +})) +' "$name" "$prompt" "$schedule" "$timezone" "$timeout" "$repo" "$ref") + +endpoint="${host%/}/api/automation/v1/preset/prompt" + +if [[ "$dry_run" -eq 1 ]]; then + echo "Would POST to: $endpoint" + echo "Header: $auth_header" | sed 's/Bearer .*/Bearer /; s/X-API-Key: .*/X-API-Key: /' + echo "Body:"; echo "$body" | python3 -m json.tool + exit 0 +fi + +echo "POST $endpoint" >&2 +resp=$(curl -sS -X POST "$endpoint" \ + -H "$auth_header" -H "Content-Type: application/json" \ + --data "$body" -w "\n__HTTP_CODE__=%{http_code}") +code=$(echo "$resp" | sed -n 's/^__HTTP_CODE__=//p') +payload=$(echo "$resp" | sed '/^__HTTP_CODE__=/d') + +if [[ "$code" != "200" && "$code" != "201" ]]; then + echo "FAILED ($code):" >&2 + echo "$payload" >&2 + exit 4 +fi + +echo "$payload" | python3 -m json.tool +id=$(echo "$payload" | python3 -c 'import json,sys; print(json.load(sys.stdin).get("id",""))') +[[ -n "$id" ]] && echo "automation_id=$id"