Merge pull request 'fix: bug: dispatcher runner invokes formulas as bash scripts but formulas are TOML (#516)' (#519) from fix/issue-516 into main
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
This commit is contained in:
commit
471b0b053a
7 changed files with 339 additions and 38 deletions
|
|
@ -66,6 +66,27 @@ services:
|
|||
depends_on:
|
||||
- forgejo
|
||||
|
||||
runner:
|
||||
image: disinto/agents:latest
|
||||
profiles: ["runner"]
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- /usr/local/bin/claude:/usr/local/bin/claude:ro
|
||||
- ${HOME}/.claude:/home/agent/.claude
|
||||
- ${HOME}/.claude.json:/home/agent/.claude.json:ro
|
||||
entrypoint: ["bash", "/home/agent/disinto/docker/runner/entrypoint-runner.sh"]
|
||||
environment:
|
||||
- DISINTO_CONTAINER=1
|
||||
- FORGE_URL=${FORGE_URL:-}
|
||||
- FORGE_TOKEN=${FORGE_TOKEN:-}
|
||||
- FORGE_REPO=${FORGE_REPO:-disinto-admin/disinto}
|
||||
- FORGE_OPS_REPO=${FORGE_OPS_REPO:-}
|
||||
- PRIMARY_BRANCH=${PRIMARY_BRANCH:-main}
|
||||
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-}
|
||||
- CLAUDE_MODEL=${CLAUDE_MODEL:-}
|
||||
networks:
|
||||
- default
|
||||
|
||||
reproduce:
|
||||
build:
|
||||
context: .
|
||||
|
|
@ -95,6 +116,7 @@ services:
|
|||
- ${HOME}/.claude.json:/root/.claude.json:ro
|
||||
- ${HOME}/.claude:/root/.claude:ro
|
||||
- disinto-logs:/opt/disinto-logs
|
||||
- ./docker-compose.yml:/opt/docker-compose.yml:ro
|
||||
environment:
|
||||
- FORGE_SUPERVISOR_TOKEN=${FORGE_SUPERVISOR_TOKEN:-}
|
||||
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-}
|
||||
|
|
@ -102,6 +124,7 @@ services:
|
|||
- FORGE_TOKEN=${FORGE_TOKEN:-}
|
||||
- FORGE_URL=http://forgejo:3000
|
||||
- DISINTO_CONTAINER=1
|
||||
- HOST_PROJECT_DIR=${HOST_PROJECT_DIR:-.}
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
|
|
|
|||
|
|
@ -408,17 +408,16 @@ launch_runner() {
|
|||
local secrets_array
|
||||
secrets_array="${VAULT_ACTION_SECRETS:-}"
|
||||
|
||||
# Build command array (safe from shell injection)
|
||||
local -a cmd=(docker run --rm
|
||||
--name "vault-runner-${action_id}"
|
||||
--network disinto_disinto-net
|
||||
-e "FORGE_URL=${FORGE_URL}"
|
||||
-e "FORGE_TOKEN=${FORGE_TOKEN}"
|
||||
-e "FORGE_REPO=${FORGE_REPO}"
|
||||
-e "FORGE_OPS_REPO=${FORGE_OPS_REPO}"
|
||||
-e "PRIMARY_BRANCH=${PRIMARY_BRANCH}"
|
||||
-e DISINTO_CONTAINER=1
|
||||
)
|
||||
# Build docker compose run command (delegates to compose runner service)
|
||||
# The runner service definition handles image, network, volumes, and base env.
|
||||
# The dispatcher only adds declared secrets and the ops repo mount.
|
||||
#
|
||||
# The edge container has docker-compose.yml mounted at /opt/docker-compose.yml.
|
||||
# --project-directory tells docker compose to resolve relative paths (volumes,
|
||||
# env_file) against the HOST project root so the Docker daemon finds them.
|
||||
local compose_file="${COMPOSE_FILE:-/opt/docker-compose.yml}"
|
||||
local project_dir="${HOST_PROJECT_DIR:-.}"
|
||||
local -a cmd=(docker compose -f "$compose_file" --project-directory "$project_dir" run --rm)
|
||||
|
||||
# Add environment variables for secrets (if any declared)
|
||||
if [ -n "$secrets_array" ]; then
|
||||
|
|
@ -438,27 +437,13 @@ launch_runner() {
|
|||
log "Action ${action_id} has no secrets declared — runner will execute without extra env vars"
|
||||
fi
|
||||
|
||||
# Add formula and action id as arguments (safe from shell injection)
|
||||
local formula="${VAULT_ACTION_FORMULA:-}"
|
||||
cmd+=(disinto-agents:latest bash -c
|
||||
"cd /home/agent/disinto && bash formulas/${formula}.sh ${action_id}")
|
||||
# Mount the ops repo so the runner entrypoint can read the action TOML
|
||||
cmd+=(-v "${OPS_REPO_ROOT}:/home/agent/ops:ro")
|
||||
|
||||
# Log command skeleton (hide all -e flags for security)
|
||||
local -a log_cmd=()
|
||||
local skip_next=0
|
||||
for arg in "${cmd[@]}"; do
|
||||
if [[ $skip_next -eq 1 ]]; then
|
||||
skip_next=0
|
||||
continue
|
||||
fi
|
||||
if [[ "$arg" == "-e" ]]; then
|
||||
log_cmd+=("$arg" "<redacted>")
|
||||
skip_next=1
|
||||
else
|
||||
log_cmd+=("$arg")
|
||||
fi
|
||||
done
|
||||
log "Running: ${log_cmd[*]}"
|
||||
# Service name and action-id argument
|
||||
cmd+=(runner "$action_id")
|
||||
|
||||
log "Running: docker compose run --rm runner ${action_id} (secrets: ${secrets_array:-none})"
|
||||
|
||||
# Create temp file for logs
|
||||
local log_file
|
||||
|
|
|
|||
106
docker/runner/entrypoint-runner.sh
Normal file
106
docker/runner/entrypoint-runner.sh
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
#!/usr/bin/env bash
|
||||
# entrypoint-runner.sh — Vault runner entrypoint
|
||||
#
|
||||
# Receives an action-id, reads the vault action TOML to get the formula name,
|
||||
# then dispatches to the appropriate executor:
|
||||
# - formulas/<name>.sh → bash (mechanical operations like release)
|
||||
# - formulas/<name>.toml → claude -p (reasoning tasks like triage, architect)
|
||||
#
|
||||
# Usage: entrypoint-runner.sh <action-id>
|
||||
#
|
||||
# Expects:
|
||||
# OPS_REPO_ROOT — path to the ops repo (mounted by compose)
|
||||
# FACTORY_ROOT — path to disinto code (default: /home/agent/disinto)
|
||||
#
|
||||
# Part of #516.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
FACTORY_ROOT="${FACTORY_ROOT:-/home/agent/disinto}"
|
||||
OPS_REPO_ROOT="${OPS_REPO_ROOT:-/home/agent/ops}"
|
||||
|
||||
log() {
|
||||
printf '[%s] runner: %s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" "$*"
|
||||
}
|
||||
|
||||
# ── Argument parsing ─────────────────────────────────────────────────────
|
||||
|
||||
action_id="${1:-}"
|
||||
if [ -z "$action_id" ]; then
|
||||
log "ERROR: action-id argument required"
|
||||
echo "Usage: entrypoint-runner.sh <action-id>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── Read vault action TOML ───────────────────────────────────────────────
|
||||
|
||||
action_toml="${OPS_REPO_ROOT}/vault/actions/${action_id}.toml"
|
||||
if [ ! -f "$action_toml" ]; then
|
||||
log "ERROR: vault action TOML not found: ${action_toml}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract formula name from TOML
|
||||
formula=$(grep -E '^formula\s*=' "$action_toml" \
|
||||
| sed -E 's/^formula\s*=\s*"(.*)"/\1/' | tr -d '\r')
|
||||
|
||||
if [ -z "$formula" ]; then
|
||||
log "ERROR: no 'formula' field found in ${action_toml}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract context for logging
|
||||
context=$(grep -E '^context\s*=' "$action_toml" \
|
||||
| sed -E 's/^context\s*=\s*"(.*)"/\1/' | tr -d '\r')
|
||||
|
||||
log "Action: ${action_id}, formula: ${formula}, context: ${context:-<none>}"
|
||||
|
||||
# Export action TOML path so formula scripts can use it directly
|
||||
export VAULT_ACTION_TOML="$action_toml"
|
||||
|
||||
# ── Dispatch: .sh (mechanical) vs .toml (Claude reasoning) ──────────────
|
||||
|
||||
formula_sh="${FACTORY_ROOT}/formulas/${formula}.sh"
|
||||
formula_toml="${FACTORY_ROOT}/formulas/${formula}.toml"
|
||||
|
||||
if [ -f "$formula_sh" ]; then
|
||||
# Mechanical operation — run directly
|
||||
log "Dispatching to shell script: ${formula_sh}"
|
||||
exec bash "$formula_sh" "$action_id"
|
||||
|
||||
elif [ -f "$formula_toml" ]; then
|
||||
# Reasoning task — launch Claude with the formula as prompt
|
||||
log "Dispatching to Claude with formula: ${formula_toml}"
|
||||
|
||||
formula_content=$(cat "$formula_toml")
|
||||
action_context=$(cat "$action_toml")
|
||||
|
||||
prompt="You are a vault runner executing a formula-based operational task.
|
||||
|
||||
## Vault action
|
||||
\`\`\`toml
|
||||
${action_context}
|
||||
\`\`\`
|
||||
|
||||
## Formula
|
||||
\`\`\`toml
|
||||
${formula_content}
|
||||
\`\`\`
|
||||
|
||||
## Instructions
|
||||
Execute the steps defined in the formula above. The vault action context provides
|
||||
the specific parameters for this run. Execute each step in order, verifying
|
||||
success before proceeding to the next.
|
||||
|
||||
FACTORY_ROOT=${FACTORY_ROOT}
|
||||
OPS_REPO_ROOT=${OPS_REPO_ROOT}
|
||||
"
|
||||
|
||||
exec claude -p "$prompt" \
|
||||
--dangerously-skip-permissions \
|
||||
${CLAUDE_MODEL:+--model "$CLAUDE_MODEL"}
|
||||
|
||||
else
|
||||
log "ERROR: no formula found for '${formula}' — checked ${formula_sh} and ${formula_toml}"
|
||||
exit 1
|
||||
fi
|
||||
187
formulas/release.sh
Normal file
187
formulas/release.sh
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
#!/usr/bin/env bash
|
||||
# formulas/release.sh — Mechanical release script
|
||||
#
|
||||
# Implements the release workflow without Claude:
|
||||
# 1. Validate prerequisites
|
||||
# 2. Tag Forgejo main via API
|
||||
# 3. Push tag to mirrors (Codeberg, GitHub) via token auth
|
||||
# 4. Build and tag the agents Docker image
|
||||
# 5. Restart agent containers
|
||||
#
|
||||
# Usage: release.sh <action-id>
|
||||
#
|
||||
# Expects env vars:
|
||||
# FORGE_URL, FORGE_TOKEN, FORGE_REPO, PRIMARY_BRANCH
|
||||
# GITHUB_TOKEN — for pushing tags to GitHub mirror
|
||||
# CODEBERG_TOKEN — for pushing tags to Codeberg mirror
|
||||
#
|
||||
# The action TOML context field must contain the version, e.g.:
|
||||
# context = "Release v1.2.0"
|
||||
#
|
||||
# Part of #516.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
FACTORY_ROOT="${FACTORY_ROOT:-/home/agent/disinto}"
|
||||
OPS_REPO_ROOT="${OPS_REPO_ROOT:-/home/agent/ops}"
|
||||
|
||||
log() {
|
||||
printf '[%s] release: %s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" "$*"
|
||||
}
|
||||
|
||||
# ── Argument parsing ─────────────────────────────────────────────────────
|
||||
# VAULT_ACTION_TOML is exported by the runner entrypoint (entrypoint-runner.sh)
|
||||
|
||||
action_id="${1:-}"
|
||||
if [ -z "$action_id" ]; then
|
||||
log "ERROR: action-id argument required"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
action_toml="${VAULT_ACTION_TOML:-${OPS_REPO_ROOT}/vault/actions/${action_id}.toml}"
|
||||
if [ ! -f "$action_toml" ]; then
|
||||
log "ERROR: vault action TOML not found: ${action_toml}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract version from context field (e.g. "Release v1.2.0" → "v1.2.0")
|
||||
context=$(grep -E '^context\s*=' "$action_toml" \
|
||||
| sed -E 's/^context\s*=\s*"(.*)"/\1/' | tr -d '\r')
|
||||
RELEASE_VERSION=$(echo "$context" | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+') || true
|
||||
|
||||
if [ -z "${RELEASE_VERSION:-}" ]; then
|
||||
log "ERROR: could not extract version from context: '${context}'"
|
||||
log "Context must contain a version like v1.2.0"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "Starting release ${RELEASE_VERSION} (action: ${action_id})"
|
||||
|
||||
# ── Step 1: Preflight ────────────────────────────────────────────────────
|
||||
|
||||
log "Step 1/6: Preflight checks"
|
||||
|
||||
# Validate version format
|
||||
if ! echo "$RELEASE_VERSION" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+$'; then
|
||||
log "ERROR: invalid version format: ${RELEASE_VERSION}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Required env vars
|
||||
for var in FORGE_URL FORGE_TOKEN FORGE_REPO PRIMARY_BRANCH; do
|
||||
if [ -z "${!var:-}" ]; then
|
||||
log "ERROR: required env var not set: ${var}"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Check Docker access
|
||||
if ! docker info >/dev/null 2>&1; then
|
||||
log "ERROR: Docker not accessible"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check tag doesn't already exist on Forgejo
|
||||
if curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${FORGE_URL}/api/v1/repos/${FORGE_REPO}/tags/${RELEASE_VERSION}" >/dev/null 2>&1; then
|
||||
log "ERROR: tag ${RELEASE_VERSION} already exists on Forgejo"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "Preflight passed"
|
||||
|
||||
# ── Step 2: Tag main via Forgejo API ─────────────────────────────────────
|
||||
|
||||
log "Step 2/6: Creating tag ${RELEASE_VERSION} on Forgejo"
|
||||
|
||||
# Get HEAD SHA of primary branch
|
||||
head_sha=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${FORGE_URL}/api/v1/repos/${FORGE_REPO}/branches/${PRIMARY_BRANCH}" \
|
||||
| jq -r '.commit.id // empty')
|
||||
|
||||
if [ -z "$head_sha" ]; then
|
||||
log "ERROR: could not get HEAD SHA for ${PRIMARY_BRANCH}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create tag via API
|
||||
curl -sf -X POST \
|
||||
-H "Authorization: token ${FORGE_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${FORGE_URL}/api/v1/repos/${FORGE_REPO}/tags" \
|
||||
-d "{\"tag_name\":\"${RELEASE_VERSION}\",\"target\":\"${head_sha}\",\"message\":\"Release ${RELEASE_VERSION}\"}" \
|
||||
>/dev/null
|
||||
|
||||
log "Tag ${RELEASE_VERSION} created (SHA: ${head_sha})"
|
||||
|
||||
# ── Step 3: Push tag to mirrors ──────────────────────────────────────────
|
||||
|
||||
log "Step 3/6: Pushing tag to mirrors"
|
||||
|
||||
# Extract org/repo from FORGE_REPO (e.g. "disinto-admin/disinto" → "disinto")
|
||||
project_name="${FORGE_REPO##*/}"
|
||||
|
||||
# Push to GitHub mirror (if GITHUB_TOKEN is available)
|
||||
if [ -n "${GITHUB_TOKEN:-}" ]; then
|
||||
log "Pushing tag to GitHub mirror"
|
||||
# Create tag on GitHub via API
|
||||
if curl -sf -X POST \
|
||||
-H "Authorization: token ${GITHUB_TOKEN}" \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
"https://api.github.com/repos/Disinto/${project_name}/git/refs" \
|
||||
-d "{\"ref\":\"refs/tags/${RELEASE_VERSION}\",\"sha\":\"${head_sha}\"}" \
|
||||
>/dev/null 2>&1; then
|
||||
log "GitHub: tag pushed"
|
||||
else
|
||||
log "WARNING: GitHub tag push failed (may already exist)"
|
||||
fi
|
||||
else
|
||||
log "WARNING: GITHUB_TOKEN not set — skipping GitHub mirror"
|
||||
fi
|
||||
|
||||
# Push to Codeberg mirror (if CODEBERG_TOKEN is available)
|
||||
if [ -n "${CODEBERG_TOKEN:-}" ]; then
|
||||
log "Pushing tag to Codeberg mirror"
|
||||
# Codeberg uses Gitea-compatible API
|
||||
# Extract owner from FORGE_REPO for Codeberg (use same owner)
|
||||
codeberg_owner="${FORGE_REPO%%/*}"
|
||||
if curl -sf -X POST \
|
||||
-H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"https://codeberg.org/api/v1/repos/${codeberg_owner}/${project_name}/tags" \
|
||||
-d "{\"tag_name\":\"${RELEASE_VERSION}\",\"target\":\"${head_sha}\",\"message\":\"Release ${RELEASE_VERSION}\"}" \
|
||||
>/dev/null 2>&1; then
|
||||
log "Codeberg: tag pushed"
|
||||
else
|
||||
log "WARNING: Codeberg tag push failed (may already exist)"
|
||||
fi
|
||||
else
|
||||
log "WARNING: CODEBERG_TOKEN not set — skipping Codeberg mirror"
|
||||
fi
|
||||
|
||||
# ── Step 4: Build agents Docker image ────────────────────────────────────
|
||||
|
||||
log "Step 4/6: Building agents Docker image"
|
||||
|
||||
cd "$FACTORY_ROOT" || exit 1
|
||||
docker compose build --no-cache agents 2>&1 | tail -5
|
||||
log "Image built"
|
||||
|
||||
# ── Step 5: Tag image with version ───────────────────────────────────────
|
||||
|
||||
log "Step 5/6: Tagging image"
|
||||
|
||||
docker tag disinto/agents:latest "disinto/agents:${RELEASE_VERSION}"
|
||||
log "Tagged disinto/agents:${RELEASE_VERSION}"
|
||||
|
||||
# ── Step 6: Restart agent containers ─────────────────────────────────────
|
||||
|
||||
log "Step 6/6: Restarting agent containers"
|
||||
|
||||
docker compose stop agents agents-llama 2>/dev/null || true
|
||||
docker compose up -d agents agents-llama
|
||||
log "Agent containers restarted"
|
||||
|
||||
# ── Done ─────────────────────────────────────────────────────────────────
|
||||
|
||||
log "Release ${RELEASE_VERSION} completed successfully"
|
||||
|
|
@ -96,7 +96,7 @@ disinto_release() {
|
|||
id = "${id}"
|
||||
formula = "release"
|
||||
context = "Release ${version}"
|
||||
secrets = []
|
||||
secrets = ["GITHUB_TOKEN", "CODEBERG_TOKEN"]
|
||||
EOF
|
||||
|
||||
echo "Created vault item: ${vault_toml}"
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
# id = "release-v120"
|
||||
# formula = "release"
|
||||
# context = "Release v1.2.0"
|
||||
# secrets = []
|
||||
# secrets = ["GITHUB_TOKEN", "CODEBERG_TOKEN"]
|
||||
#
|
||||
# Steps executed by the release formula:
|
||||
# 1. preflight - Validate prerequisites (version, FORGE_TOKEN, Docker)
|
||||
|
|
@ -26,7 +26,7 @@
|
|||
id = "release-v120"
|
||||
formula = "release"
|
||||
context = "Release v1.2.0 — includes vault redesign, .profile system, architect agent"
|
||||
secrets = []
|
||||
secrets = ["GITHUB_TOKEN", "CODEBERG_TOKEN"]
|
||||
|
||||
# Optional: specify a larger model for complex release logic
|
||||
# model = "sonnet"
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ fi
|
|||
# =============================================================================
|
||||
|
||||
# Allowed secret names - must match keys in .env.vault.enc
|
||||
VAULT_ALLOWED_SECRETS="CLAWHUB_TOKEN GITHUB_TOKEN DEPLOY_KEY NPM_TOKEN DOCKER_HUB_TOKEN"
|
||||
VAULT_ALLOWED_SECRETS="CLAWHUB_TOKEN GITHUB_TOKEN CODEBERG_TOKEN DEPLOY_KEY NPM_TOKEN DOCKER_HUB_TOKEN"
|
||||
|
||||
# Validate a vault action TOML file
|
||||
# Usage: validate_vault_action <path-to-toml>
|
||||
|
|
@ -99,9 +99,9 @@ validate_vault_action() {
|
|||
return 1
|
||||
fi
|
||||
|
||||
# Validate formula exists in formulas/
|
||||
if [ ! -f "$formulas_dir/${formula}.toml" ]; then
|
||||
echo "ERROR: Formula not found: $formula" >&2
|
||||
# Validate formula exists in formulas/ (.toml for Claude reasoning, .sh for mechanical)
|
||||
if [ ! -f "$formulas_dir/${formula}.toml" ] && [ ! -f "$formulas_dir/${formula}.sh" ]; then
|
||||
echo "ERROR: Formula not found: $formula (checked .toml and .sh)" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue