diff --git a/.buildkite/pipeline.poll-issues.yml b/.buildkite/pipeline.poll-issues.yml new file mode 100644 index 0000000..65255b8 --- /dev/null +++ b/.buildkite/pipeline.poll-issues.yml @@ -0,0 +1,22 @@ +# Optional scheduled pipeline: poll for new Radicle issues every 5 minutes. +# Enable in Buildkite UI → Pipeline → Settings → Schedules, or upload this file +# from a separate Buildkite pipeline pointed at the same Radicle Garden repo. +# +# Secrets / env: +# BUILDKITE_API_TOKEN — API token with build create permission +# BUILDKITE_ORG — default nandi (set in trigger-issue-build.sh) +# BUILDKITE_PIPELINE — default sleek +# +# Manual trigger for one issue: +# BUILDKITE_API_TOKEN=… ./scripts/buildkite/trigger-issue-build.sh + +agents: + queue: auto + +steps: + - label: ":radicle: Poll for new issues" + command: bash scripts/buildkite/poll-issues.sh + env: + BUILDKITE_API_TOKEN: ${BUILDKITE_API_TOKEN} + BUILDKITE_ORG: ${BUILDKITE_ORG} + BUILDKITE_PIPELINE: ${BUILDKITE_PIPELINE} diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index 0e462bb..fc75ea3 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -9,6 +9,8 @@ # Secrets (Buildkite cluster → Secrets, org nandi / Default cluster): # - NIXBUILD_TOKEN (preferred) — soft-loaded; skip artifacts if missing # - OPENBAO_TOKEN (optional) — fetch-openbao-env.sh → NIXBUILD_TOKEN +# - CURSOR_API_KEY (issue agent) — Cursor CLI for Radicle issue → patch +# - BUILDKITE_API_TOKEN (optional) — poll/trigger scripts for new issues # UI: https://buildkite.com/organizations/nandi/clusters → Default cluster → Secrets # Pipeline: https://buildkite.com/nandi/sleek # @@ -25,16 +27,25 @@ # github.com/codegod100/freeq, and Flathub when building Flatpak. # Hosted queue: auto (LINUX_AMD64_2X4). Use $$ so Buildkite does not interpolate # shell vars at pipeline-upload time. +# +# Radicle issue → cursor-agent → patch: +# Garden adapter skips issue COB commits (no pipeline.yml there). Trigger on +# main with RADICLE_ISSUE_ID / RADICLE_TRIGGER=issue instead: +# ./scripts/buildkite/trigger-issue-build.sh +# Optional schedule: .buildkite/pipeline.poll-issues.yml agents: queue: auto env: RUST_BACKTRACE: "1" + GIT_TERMINAL_PROMPT: "0" steps: - label: ":rust: Check" key: check + # Issue-triggered builds only need the agent step (see below). + if: build.env("RADICLE_TRIGGER") != "issue" timeout_in_minutes: 120 command: | set -euo pipefail @@ -98,6 +109,7 @@ steps: - label: ":android: APK (nixbuild.net)" key: apk + if: build.env("RADICLE_TRIGGER") != "issue" depends_on: check allow_dependency_failure: true timeout_in_minutes: 120 @@ -138,6 +150,7 @@ steps: - label: ":package: Flatpak (nixbuild.net)" key: flatpak + if: build.env("RADICLE_TRIGGER") != "issue" depends_on: check allow_dependency_failure: true timeout_in_minutes: 180 @@ -180,3 +193,36 @@ steps: ln -f "$$bundle" uk.nandi.sleek.flatpak 2>/dev/null \ || cp --reflink=auto "$$bundle" uk.nandi.sleek.flatpak buildkite-agent artifact upload uk.nandi.sleek.flatpak + + # --- Radicle issue agent (triggered via RADICLE_ISSUE_ID on main) --- + - label: ":radicle: Issue → :cursor: agent → patch" + key: radicle-issue-agent + if: build.env("RADICLE_TRIGGER") == "issue" || build.env("RADICLE_ISSUE_ID") != null + command: bash scripts/buildkite/run-issue-agent.sh + timeout_in_minutes: 90 + env: + CURSOR_API_KEY: ${CURSOR_API_KEY} + RADICLE_ISSUE_ID: ${RADICLE_ISSUE_ID} + + - label: ":mag: Verify patch (optional)" + key: verify-patch + if: build.env("RADICLE_TRIGGER") == "issue" || build.env("RADICLE_ISSUE_ID") != null + depends_on: radicle-issue-agent + allow_dependency_failure: true + command: | + set -euo pipefail + BRANCH="$$(buildkite-agent meta-data get radicle_issue_branch 2>/dev/null || true)" + if [[ -z "$$BRANCH" ]]; then + echo "No issue branch metadata — prior step likely skipped (not an issue event)." + exit 0 + fi + if git show-ref --verify --quiet "refs/heads/$$BRANCH"; then + echo "Branch $$BRANCH exists." + git log -1 --oneline "$$BRANCH" + else + echo "Branch $$BRANCH not found — agent may have declined or failed before branching." + exit 0 + fi + echo "" + echo "Open patches:" + rad patch list --open 2>/dev/null | sed -n '1,15p' || rad patch 2>/dev/null | sed -n '1,15p' || true diff --git a/.gitignore b/.gitignore index 65ea86a..c9d71fb 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ .direnv/ result result-* +.buildkite/.issue-poll-state diff --git a/scripts/buildkite/bootstrap.sh b/scripts/buildkite/bootstrap.sh new file mode 100755 index 0000000..af983aa --- /dev/null +++ b/scripts/buildkite/bootstrap.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +# Install cursor-agent (if missing) and build Radicle MCP for CI runs. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=lib.sh +source "$SCRIPT_DIR/lib.sh" + +REPO_ROOT="$(bk_repo_root)" +export PATH="${HOME}/.cursor/bin:${HOME}/.local/bin:/usr/local/bin:${PATH:-}" + +install_cursor_agent() { + if command -v cursor-agent >/dev/null 2>&1; then + return 0 + fi + if command -v agent >/dev/null 2>&1; then + return 0 + fi + + echo "[bootstrap] installing Cursor CLI..." + curl -fsSL https://cursor.com/install | bash + export PATH="${HOME}/.cursor/bin:${PATH:-}" +} + +cursor_agent_cmd() { + if command -v cursor-agent >/dev/null 2>&1; then + echo "cursor-agent" + return 0 + fi + if command -v agent >/dev/null 2>&1; then + echo "agent" + return 0 + fi + bk_die "cursor-agent / agent not found after install" +} + +build_radicle_mcp() { + local mcp_dir="$REPO_ROOT/mcp/radicle" + if [[ ! -f "$mcp_dir/package.json" ]]; then + echo "[bootstrap] no mcp/radicle in repo — skipping MCP build" + return 0 + fi + + if [[ -f "$mcp_dir/dist/index.js" ]]; then + echo "[bootstrap] radicle MCP already built" + return 0 + fi + + bk_require_cmd node + bk_require_cmd npm + echo "[bootstrap] building radicle MCP..." + npm ci --prefix "$mcp_dir" + npm run build --prefix "$mcp_dir" +} + +verify_auth() { + local cmd + cmd="$(cursor_agent_cmd)" + if [[ -z "${CURSOR_API_KEY:-}" ]]; then + bk_die "CURSOR_API_KEY is not set — add it as a Buildkite secret" + fi + if ! "$cmd" status >/dev/null 2>&1; then + bk_die "Cursor CLI auth failed — check CURSOR_API_KEY" + fi +} + +install_cursor_agent +build_radicle_mcp +verify_auth + +echo "[bootstrap] ready (repo=$REPO_ROOT)" diff --git a/scripts/buildkite/delegate.sh b/scripts/buildkite/delegate.sh new file mode 100755 index 0000000..9faa78b --- /dev/null +++ b/scripts/buildkite/delegate.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# delegate.sh — run a task via the cursor-agent CLI as a subagent. +# Usage: delegate.sh "task" [--workspace DIR] [--model ID] [--mode plan|ask] +# [--output-format json|text] [--timeout SECONDS] [--force] +set -euo pipefail + +task="" +workspace="" +model="" +mode="" +output_format="json" +timeout=1800 +force=0 +trust=1 + +while [[ $# -gt 0 ]]; do + case "$1" in + --workspace) workspace="$2"; shift 2 ;; + --model) model="$2"; shift 2 ;; + --mode) mode="$2"; shift 2 ;; + --output-format) output_format="$2"; shift 2 ;; + --timeout) timeout="$2"; shift 2 ;; + --force) force=1; shift ;; + --notrust) trust=0; shift ;; + -h|--help) echo "$usage_help"; exit 0 ;; + -*) echo "unknown flag: $1" >&2; exit 2 ;; + *) if [[ -z "$task" ]]; then task="$1"; else task="$task $1"; fi; shift ;; + esac +done + +if [[ -z "${task:-}" ]]; then + echo "error: provide a task/instruction to delegate" >&2 + exit 2 +fi + +AGENT_BIN="" +if command -v cursor-agent >/dev/null 2>&1; then + AGENT_BIN=cursor-agent +elif command -v agent >/dev/null 2>&1; then + AGENT_BIN=agent +fi +[[ -n "$AGENT_BIN" ]] || { echo "error: cursor-agent / agent not found on PATH" >&2; exit 127; } +"$AGENT_BIN" status >/dev/null 2>&1 || { echo "error: Cursor CLI not authenticated (set CURSOR_API_KEY or run: $AGENT_BIN login)" >&2; exit 1; } + +args=(--print --output-format "$output_format") +[[ -n "$model" ]] && args+=(--model "$model") +[[ -n "$mode" ]] && args+=(--mode "$mode") +[[ "$force" == 1 ]] && args+=(--force) +[[ "$trust" == 1 ]] && args+=(--trust) +[[ -n "$workspace" ]] && args+=(--workspace "$workspace") +args+=( "$task" ) + +out="$(mktemp)"; err="$(mktemp)"; rc=0 +timeout "$timeout" "$AGENT_BIN" "${args[@]}" >"$out" 2>"$err" || rc="$?" +if [[ "$rc" != 0 ]]; then + echo "error: $AGENT_BIN failed (exit $rc):" >&2 + sed -n '1,20p' "$err" >&2 + rm -f "$out" "$err" + exit "$rc" +fi +rm -f "$err" +cat "$out" +rm -f "$out" diff --git a/scripts/buildkite/detect-issue.sh b/scripts/buildkite/detect-issue.sh new file mode 100755 index 0000000..91b5d63 --- /dev/null +++ b/scripts/buildkite/detect-issue.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +# Detect whether BUILDKITE_COMMIT is a newly opened Radicle issue. +# On success, prints exportable shell assignments to stdout: +# RADICLE_ISSUE_ID, RADICLE_ISSUE_TITLE, RADICLE_ISSUE_BODY, RADICLE_ISSUE_BRANCH +# +# Exit 0 — issue detected (assignments printed) +# Exit 1 — error +# Exit 2 — not an issue event (skip agent) +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=lib.sh +source "$SCRIPT_DIR/lib.sh" + +COMMIT="${BUILDKITE_COMMIT:-}" +if [[ -z "$COMMIT" ]]; then + echo "BUILDKITE_COMMIT is not set" >&2 + exit 1 +fi + +bk_require_cmd rad +bk_require_rad_repo + +# Issue opens create a COB root commit that does not contain .buildkite/pipeline.yml, +# so the Radicle Buildkite adapter skips them. Trigger builds on main instead and +# pass the issue id explicitly (see scripts/buildkite/trigger-issue-build.sh). +if [[ -n "${RADICLE_ISSUE_ID:-}" ]]; then + ISSUE_ID="$RADICLE_ISSUE_ID" + if ! rad issue show "$ISSUE_ID" --header >/dev/null 2>&1; then + echo "RADICLE_ISSUE_ID=$ISSUE_ID is not a valid issue" >&2 + exit 1 + fi +elif bk_commit_is_new_issue "$COMMIT"; then + ISSUE_ID="$COMMIT" +else + echo "commit $COMMIT is not a new issue COB root (set RADICLE_ISSUE_ID to trigger from main)" >&2 + exit 2 +fi +mapfile -t _details < <(bk_issue_details "$ISSUE_ID") +TITLE="${_details[0]:-}" +BODY="${_details[1]:-}" + +if [[ -z "$TITLE" ]]; then + TITLE="(no title)" +fi + +SHORT="$(bk_short_id "$ISSUE_ID")" +BRANCH="issue/${SHORT}" + +printf 'RADICLE_ISSUE_ID=%q\n' "$ISSUE_ID" +printf 'RADICLE_ISSUE_TITLE=%q\n' "$TITLE" +printf 'RADICLE_ISSUE_BODY=%q\n' "$BODY" +printf 'RADICLE_ISSUE_BRANCH=%q\n' "$BRANCH" diff --git a/scripts/buildkite/lib.sh b/scripts/buildkite/lib.sh new file mode 100755 index 0000000..c21d5ff --- /dev/null +++ b/scripts/buildkite/lib.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash +# Shared helpers for Radicle issue → cursor-agent → patch Buildkite steps (sleek). +set -euo pipefail + +bk_die() { echo "radicle-issue-agent: $*" >&2; exit 1; } + +bk_require_cmd() { + command -v "$1" >/dev/null 2>&1 || bk_die "$1 not found on PATH" +} + +bk_repo_root() { + git rev-parse --show-toplevel 2>/dev/null || bk_die "not inside a git repository" +} + +bk_require_rad_repo() { + local root + root="$(bk_repo_root)" + git -C "$root" remote get-url rad >/dev/null 2>&1 \ + || bk_die "remote 'rad' missing — clone via rad clone or rad remote add rad " +} + +bk_rad_rid() { + git remote get-url rad +} + +# New issue COBs use the creation commit as the issue id (40-char hex). +bk_commit_is_new_issue() { + local commit=$1 rid + rid="$(bk_rad_rid)" + rad cob list --repo "$rid" --type xyz.radicle.issue 2>/dev/null \ + | grep -qxF "$commit" +} + +# Parse rad issue show --header output into title + description. +bk_issue_details() { + local issue_id=$1 + python3 - "$issue_id" <<'PY' +import re +import subprocess +import sys + +issue_id = sys.argv[1] +proc = subprocess.run( + ["rad", "issue", "show", issue_id, "--header"], + capture_output=True, + text=True, +) +if proc.returncode != 0: + sys.stderr.write(proc.stderr or proc.stdout) + sys.exit(proc.returncode) + +text = proc.stdout +title = "" +body = "" + +title_match = re.search(r"Title\s+(.+?)\s+Issue", text, re.DOTALL) +if title_match: + title = " ".join(title_match.group(1).split()) + +lines = [ln.strip() for ln in text.splitlines()] +for ln in lines: + if not ln or ln.startswith("╭") or ln.startswith("╰") or ln.startswith("│"): + continue + if re.match(r"^(Title|Issue|Author|Status)\b", ln): + continue + if re.match(r"^[●○]", ln): + continue + body = ln + break + +if not body: + for ln in reversed(lines): + cleaned = ln.strip("│ ").strip() + if cleaned and not re.match(r"^(Title|Issue|Author|Status)\b", cleaned): + body = cleaned + break + +print(title) +print(body) +PY +} + +bk_short_id() { + local id=$1 + echo "${id:0:7}" +} + +bk_annotate() { + local style=$1 + local message=$2 + if command -v buildkite-agent >/dev/null 2>&1; then + buildkite-agent annotate "$message" --style "$style" --context "radicle-issue-agent" || true + fi +} diff --git a/scripts/buildkite/poll-issues.sh b/scripts/buildkite/poll-issues.sh new file mode 100755 index 0000000..46183f7 --- /dev/null +++ b/scripts/buildkite/poll-issues.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +# Poll for new open Radicle issues and trigger Buildkite agent builds. +# Run from a scheduled Buildkite pipeline on main (e.g. every 5 minutes). +# +# Requires: +# BUILDKITE_ORG, BUILDKITE_PIPELINE, BUILDKITE_API_TOKEN +# Optional: +# RADICLE_ISSUE_POLL_STATE_FILE — path to last-seen issue ids (default: .buildkite/.issue-poll-state) +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +cd "$REPO_ROOT" + +# shellcheck source=lib.sh +source "$SCRIPT_DIR/lib.sh" + +bk_require_cmd rad +bk_require_rad_repo + +STATE_FILE="${RADICLE_ISSUE_POLL_STATE_FILE:-$REPO_ROOT/.buildkite/.issue-poll-state}" +mkdir -p "$(dirname "$STATE_FILE")" +touch "$STATE_FILE" + +mapfile -t known < "$STATE_FILE" + +current=() +while IFS= read -r line; do + [[ -n "$line" ]] && current+=("$line") +done < <(rad cob list --type xyz.radicle.issue 2>/dev/null || true) + +new_ids=() +for id in "${current[@]}"; do + found=0 + for old in "${known[@]}"; do + if [[ "$id" == "$old" ]]; then + found=1 + break + fi + done + if [[ "$found" -eq 0 ]]; then + new_ids+=("$id") + fi +done + +printf '%s\n' "${current[@]}" >"$STATE_FILE" + +if [[ "${#new_ids[@]}" -eq 0 ]]; then + echo "poll: no new issues" + exit 0 +fi + +echo "poll: ${#new_ids[@]} new issue(s)" +for id in "${new_ids[@]}"; do + echo " triggering build for $id" + "$SCRIPT_DIR/trigger-issue-build.sh" "$id" +done diff --git a/scripts/buildkite/run-issue-agent.sh b/scripts/buildkite/run-issue-agent.sh new file mode 100755 index 0000000..48f55f5 --- /dev/null +++ b/scripts/buildkite/run-issue-agent.sh @@ -0,0 +1,131 @@ +#!/usr/bin/env bash +# Buildkite entrypoint: Garden issue event → cursor-agent → Radicle patch. +# +# Required env (Buildkite secrets): +# CURSOR_API_KEY — Cursor CLI / service account key +# +# Optional: +# RADICLE_AGENT_MODEL — passed to cursor-agent --model +# RADICLE_AGENT_TIMEOUT — seconds (default 3600) +# RADICLE_AGENT_DRY_RUN — set to 1 to print prompt without running agent +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +cd "$REPO_ROOT" + +# shellcheck source=lib.sh +source "$SCRIPT_DIR/lib.sh" + +TIMEOUT="${RADICLE_AGENT_TIMEOUT:-3600}" +MODEL="${RADICLE_AGENT_MODEL:-}" +DRY_RUN="${RADICLE_AGENT_DRY_RUN:-0}" + +echo "=== Radicle issue agent ===" +echo "commit: ${BUILDKITE_COMMIT:-unknown}" +echo "branch: ${BUILDKITE_BRANCH:-unknown}" +echo "repo: $REPO_ROOT" + +# Detect before bootstrap so normal CI (no CURSOR_API_KEY) can soft-skip. +set +e +detect_out="$(bash "$SCRIPT_DIR/detect-issue.sh" 2>&1)" +detect_rc=$? +set -e + +if [[ "$detect_rc" -eq 2 ]]; then + echo "$detect_out" + bk_annotate info "Skipped: not a new Radicle issue event." + exit 0 +fi +if [[ "$detect_rc" -ne 0 ]]; then + echo "$detect_out" >&2 + bk_die "issue detection failed (exit $detect_rc)" +fi + +eval "$detect_out" + +echo "issue: $RADICLE_ISSUE_ID" +echo "title: $RADICLE_ISSUE_TITLE" +echo "branch: $RADICLE_ISSUE_BRANCH" + +# Idempotency: skip if we already started work on this issue. +if git show-ref --verify --quiet "refs/heads/$RADICLE_ISSUE_BRANCH"; then + msg="Branch \`$RADICLE_ISSUE_BRANCH\` already exists — skipping duplicate agent run." + echo "$msg" + bk_annotate warning "$msg" + exit 0 +fi + +bash "$SCRIPT_DIR/bootstrap.sh" + +if command -v buildkite-agent >/dev/null 2>&1; then + buildkite-agent meta-data set "radicle_issue_id" "$RADICLE_ISSUE_ID" || true + buildkite-agent meta-data set "radicle_issue_branch" "$RADICLE_ISSUE_BRANCH" || true +fi + +PROMPT=$(cat <&1)" +agent_rc=$? +set -e + +echo "$agent_out" + +if [[ "$agent_rc" -ne 0 ]]; then + bk_annotate error "cursor-agent failed (exit $agent_rc). See build logs." + exit "$agent_rc" +fi + +# Best-effort: surface patch id from agent output or recent patches. +PATCH_NOTE="" +if echo "$agent_out" | grep -qE 'patch_id|patches/[0-9a-f]{7,40}|Patch[[:space:]]+[0-9a-f]{7,40}'; then + PATCH_NOTE=$(echo "$agent_out" | grep -oE 'patches/[0-9a-f]{7,40}|Patch[[:space:]]+[0-9a-f]{7,40}|"patch_id"[[:space:]]*:[[:space:]]*"[0-9a-f]+"' | head -1 || true) +fi + +if [[ -n "$PATCH_NOTE" ]]; then + bk_annotate success "Agent finished for issue \`${RADICLE_ISSUE_ID:0:7}\`. Patch: ${PATCH_NOTE}" +else + bk_annotate success "Agent finished for issue \`${RADICLE_ISSUE_ID:0:7}\`. Check \`rad patch list\` for the new patch." +fi + +echo "=== Done ===" diff --git a/scripts/buildkite/trigger-issue-build.sh b/scripts/buildkite/trigger-issue-build.sh new file mode 100755 index 0000000..3412cbd --- /dev/null +++ b/scripts/buildkite/trigger-issue-build.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash +# Trigger a Buildkite build on main for a Radicle issue. +# +# The Garden Buildkite adapter only auto-triggers push/patch events and skips issue +# COB commits (no .buildkite/pipeline.yml at that commit). Call this from a Garden +# webhook, cron, or manually after opening an issue. +# +# Usage: +# BUILDKITE_API_TOKEN=... \ +# BUILDKITE_ORG=nandi \ +# BUILDKITE_PIPELINE=sleek \ +# ./scripts/buildkite/trigger-issue-build.sh 2ac9c51b5df8d3b67fb0a40c738e073e9ff301be +# +# Optional: +# BUILDKITE_BRANCH=main +# BUILDKITE_API_URL=https://api.buildkite.com/v2 +set -euo pipefail + +ISSUE_ID="${1:-}" +if [[ -z "$ISSUE_ID" ]]; then + echo "usage: $0 " >&2 + exit 2 +fi + +ORG="${BUILDKITE_ORG:-nandi}" +PIPELINE="${BUILDKITE_PIPELINE:-sleek}" +TOKEN="${BUILDKITE_API_TOKEN:-}" +BRANCH="${BUILDKITE_BRANCH:-main}" +API="${BUILDKITE_API_URL:-https://api.buildkite.com/v2}" + +[[ -n "$TOKEN" ]] || { echo "BUILDKITE_API_TOKEN required" >&2; exit 1; } + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=lib.sh +source "$SCRIPT_DIR/lib.sh" + +bk_require_cmd git +bk_require_cmd rad +bk_require_cmd curl +bk_require_cmd jq +bk_require_rad_repo + +if ! rad issue show "$ISSUE_ID" --header >/dev/null 2>&1; then + bk_die "not a valid issue: $ISSUE_ID" +fi + +COMMIT="$(git rev-parse "$BRANCH")" +MESSAGE="Radicle issue opened: ${ISSUE_ID:0:7}" + +payload="$(jq -n \ + --arg commit "$COMMIT" \ + --arg branch "$BRANCH" \ + --arg message "$MESSAGE" \ + --arg issue_id "$ISSUE_ID" \ + '{ + commit: $commit, + branch: $branch, + message: $message, + ignore_pipeline_branch_filters: true, + env: { + RADICLE_ISSUE_ID: $issue_id, + RADICLE_TRIGGER: "issue" + } + }')" + +url="${API%/}/organizations/${ORG}/pipelines/${PIPELINE}/builds" +echo "POST $url" +echo " branch=$BRANCH commit=${COMMIT:0:7} issue=${ISSUE_ID:0:7}" + +response="$(curl -fsSL -X POST "$url" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d "$payload")" + +build_url="$(echo "$response" | jq -r '.web_url // .url // empty')" +build_number="$(echo "$response" | jq -r '.number // empty')" + +if [[ -n "$build_url" ]]; then + echo "build #$build_number: $build_url" +else + echo "$response" | jq . +fi diff --git a/scripts/buildkite/webhook-to-buildkite.sh b/scripts/buildkite/webhook-to-buildkite.sh new file mode 100755 index 0000000..6a7ad72 --- /dev/null +++ b/scripts/buildkite/webhook-to-buildkite.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# Translate a Radicle CI broker push/patch webhook payload into a Buildkite build +# when the event commit is a new issue COB root. +# +# Intended for the Garden webhooks adapter: pipe broker/webhook JSON on stdin. +# Requires BUILDKITE_ORG, BUILDKITE_PIPELINE, BUILDKITE_API_TOKEN. +# +# Example (on Garden after an issue open): +# echo "$payload" | BUILDKITE_ORG=nandi BUILDKITE_PIPELINE=sleek \ +# BUILDKITE_API_TOKEN=... ./scripts/buildkite/webhook-to-buildkite.sh +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=lib.sh +source "$SCRIPT_DIR/lib.sh" + +bk_require_cmd jq + +payload="$(cat)" +commit="$(echo "$payload" | jq -r '.commit // .Commit // .head // empty')" +repo="$(echo "$payload" | jq -r '.repo // .repository // .repo_id // empty')" + +if [[ -z "$commit" ]]; then + echo "webhook: no commit in payload — ignoring" >&2 + exit 0 +fi + +echo "webhook: repo=${repo:-unknown} commit=${commit:0:7}" + +export RAD_HOME="${RAD_HOME:-$HOME/.radicle}" +bk_require_cmd rad + +# Resolve repo checkout if RAD_REPO_PATH is set (recommended on Garden). +if [[ -n "${RAD_REPO_PATH:-}" && -d "$RAD_REPO_PATH" ]]; then + cd "$RAD_REPO_PATH" +fi + +if ! bk_commit_is_new_issue "$commit" 2>/dev/null; then + echo "webhook: commit is not a new issue root — ignoring" >&2 + exit 0 +fi + +exec "$SCRIPT_DIR/trigger-issue-build.sh" "$commit"