From e22863eb6090cf96d76cca31ee7397b6cbb43925 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 6 Apr 2026 19:51:59 +0000 Subject: [PATCH 1/2] fix: refactor: extract disinto_release() from bin/disinto into lib/release.sh (#304) Co-Authored-By: Claude Sonnet 4.6 --- bin/disinto | 146 +---------------------------------------- lib/release.sh | 174 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 176 insertions(+), 144 deletions(-) create mode 100644 lib/release.sh diff --git a/bin/disinto b/bin/disinto index 4cd882f..38c4628 100755 --- a/bin/disinto +++ b/bin/disinto @@ -31,6 +31,7 @@ source "${FACTORY_ROOT}/lib/forge-setup.sh" source "${FACTORY_ROOT}/lib/generators.sh" source "${FACTORY_ROOT}/lib/forge-push.sh" source "${FACTORY_ROOT}/lib/ci-setup.sh" +source "${FACTORY_ROOT}/lib/release.sh" # ── Helpers ────────────────────────────────────────────────────────────────── @@ -1295,150 +1296,7 @@ disinto_shell() { # disinto_hire_an_agent() is sourced from lib/hire-agent.sh # ── release command ─────────────────────────────────────────────────────────── -# -# Creates a vault PR for the release. This is a convenience wrapper that -# creates the vault item TOML and submits it as a PR to the ops repo. -# -# Usage: disinto release -# Example: disinto release v1.2.0 - -disinto_release() { - local version="${1:-}" - local formula_path="${FACTORY_ROOT}/formulas/release.toml" - - if [ -z "$version" ]; then - echo "Error: version required" >&2 - echo "Usage: disinto release " >&2 - echo "Example: disinto release v1.2.0" >&2 - exit 1 - fi - - # Validate version format (must start with 'v' followed by semver) - if ! echo "$version" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+$'; then - echo "Error: version must be in format v1.2.3 (semver with 'v' prefix)" >&2 - exit 1 - fi - - # Load project config to get FORGE_OPS_REPO - if [ -z "${PROJECT_NAME:-}" ]; then - # PROJECT_NAME is unset - detect project TOML from projects/ directory - local found_toml - found_toml=$(find "${FACTORY_ROOT}/projects" -maxdepth 1 -name "*.toml" ! -name "*.example" 2>/dev/null | head -1) - if [ -n "$found_toml" ]; then - source "${FACTORY_ROOT}/lib/load-project.sh" "$found_toml" - fi - else - local project_toml="${FACTORY_ROOT}/projects/${PROJECT_NAME}.toml" - if [ -f "$project_toml" ]; then - source "${FACTORY_ROOT}/lib/load-project.sh" "$project_toml" - fi - fi - - # Check formula exists - if [ ! -f "$formula_path" ]; then - echo "Error: release formula not found at ${formula_path}" >&2 - exit 1 - fi - - # Get the ops repo root - local ops_root="${FACTORY_ROOT}/../disinto-ops" - if [ ! -d "${ops_root}/.git" ]; then - echo "Error: ops repo not found at ${ops_root}" >&2 - echo " Run 'disinto init' to set up the ops repo first" >&2 - exit 1 - fi - - # Generate a unique ID for the vault item - local id="release-${version//./}" - local vault_toml="${ops_root}/vault/actions/${id}.toml" - - # Create vault TOML with the specific version - cat > "$vault_toml" </dev/null || true - - # Push branch - git push -u origin "$branch_name" 2>/dev/null || { - echo "Error: failed to push branch" >&2 - exit 1 - } - - # Create PR - local pr_response - pr_response=$(curl -sf -X POST \ - -H "Authorization: token ${FORGE_TOKEN}" \ - -H "Content-Type: application/json" \ - "${FORGE_URL}/api/v1/repos/${FORGE_OPS_REPO}/pulls" \ - -d "{\"title\":\"${pr_title}\",\"head\":\"${branch_name}\",\"base\":\"main\",\"body\":\"$(echo "$pr_body" | sed ':a;N;$!ba;s/\n/\\n/g')\"}" 2>/dev/null) || { - echo "Error: failed to create PR" >&2 - echo "Response: ${pr_response}" >&2 - exit 1 - } - - local pr_number - pr_number=$(echo "$pr_response" | jq -r '.number') - - local pr_url="${FORGE_URL}/${FORGE_OPS_REPO}/pulls/${pr_number}" - - # Enable auto-merge on the PR — Forgejo will auto-merge after approval - _vault_log "Enabling auto-merge for PR #${pr_number}" - curl -sf -X POST \ - -H "Authorization: token ${FORGE_TOKEN}" \ - -H "Content-Type: application/json" \ - "${FORGE_URL}/api/v1/repos/${FORGE_OPS_REPO}/pulls/${pr_number}/merge" \ - -d '{"Do":"merge","merge_when_checks_succeed":true}' >/dev/null 2>&1 || { - echo "Warning: failed to enable auto-merge (may already be enabled or not supported)" >&2 - } - - echo "" - echo "Release PR created: ${pr_url}" - echo "" - echo "Next steps:" - echo " 1. Review the PR" - echo " 2. Approve the PR (auto-merge will trigger after approval)" - echo " 3. The vault runner will execute the release formula" - echo "" - echo "After merge, the release will:" - echo " 1. Tag Forgejo main with ${version}" - echo " 2. Push tag to mirrors (Codeberg, GitHub)" - echo " 3. Build and tag the agents Docker image" - echo " 4. Restart agent containers" -} +# disinto_release() is sourced from lib/release.sh # ── ci-logs command ────────────────────────────────────────────────────────── # Reads CI logs from the Woodpecker SQLite database. diff --git a/lib/release.sh b/lib/release.sh new file mode 100644 index 0000000..9910996 --- /dev/null +++ b/lib/release.sh @@ -0,0 +1,174 @@ +#!/usr/bin/env bash +# ============================================================================= +# release.sh — disinto_release() function +# +# Handles vault TOML creation, branch setup on ops repo, PR creation, +# and auto-merge request for a versioned release. +# +# Globals expected: +# FORGE_URL - Forge instance URL (e.g. http://localhost:3000) +# FORGE_TOKEN - API token for Forge operations +# FORGE_OPS_REPO - Ops repo slug (e.g. disinto-admin/myproject-ops) +# FACTORY_ROOT - Root of the disinto factory +# PRIMARY_BRANCH - Primary branch name (e.g. main) +# +# Usage: +# source "${FACTORY_ROOT}/lib/release.sh" +# disinto_release +# ============================================================================= +set -euo pipefail + +# Source vault.sh for _vault_log helper +source "${FACTORY_ROOT}/lib/vault.sh" + +# Assert required globals are set before using this module. +_assert_release_globals() { + local missing=() + [ -z "${FORGE_URL:-}" ] && missing+=("FORGE_URL") + [ -z "${FORGE_TOKEN:-}" ] && missing+=("FORGE_TOKEN") + [ -z "${FORGE_OPS_REPO:-}" ] && missing+=("FORGE_OPS_REPO") + [ -z "${FACTORY_ROOT:-}" ] && missing+=("FACTORY_ROOT") + [ -z "${PRIMARY_BRANCH:-}" ] && missing+=("PRIMARY_BRANCH") + if [ "${#missing[@]}" -gt 0 ]; then + echo "Error: release.sh requires these globals to be set: ${missing[*]}" >&2 + exit 1 + fi +} + +disinto_release() { + local version="${1:-}" + local formula_path="${FACTORY_ROOT}/formulas/release.toml" + + if [ -z "$version" ]; then + echo "Error: version required" >&2 + echo "Usage: disinto release " >&2 + echo "Example: disinto release v1.2.0" >&2 + exit 1 + fi + + # Validate version format (must start with 'v' followed by semver) + if ! echo "$version" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+$'; then + echo "Error: version must be in format v1.2.3 (semver with 'v' prefix)" >&2 + exit 1 + fi + + # Load project config to get FORGE_OPS_REPO + if [ -z "${PROJECT_NAME:-}" ]; then + # PROJECT_NAME is unset - detect project TOML from projects/ directory + local found_toml + found_toml=$(find "${FACTORY_ROOT}/projects" -maxdepth 1 -name "*.toml" ! -name "*.example" 2>/dev/null | head -1) + if [ -n "$found_toml" ]; then + source "${FACTORY_ROOT}/lib/load-project.sh" "$found_toml" + fi + else + local project_toml="${FACTORY_ROOT}/projects/${PROJECT_NAME}.toml" + if [ -f "$project_toml" ]; then + source "${FACTORY_ROOT}/lib/load-project.sh" "$project_toml" + fi + fi + + # Check formula exists + if [ ! -f "$formula_path" ]; then + echo "Error: release formula not found at ${formula_path}" >&2 + exit 1 + fi + + # Get the ops repo root + local ops_root="${FACTORY_ROOT}/../disinto-ops" + if [ ! -d "${ops_root}/.git" ]; then + echo "Error: ops repo not found at ${ops_root}" >&2 + echo " Run 'disinto init' to set up the ops repo first" >&2 + exit 1 + fi + + # Generate a unique ID for the vault item + local id="release-${version//./}" + local vault_toml="${ops_root}/vault/actions/${id}.toml" + + # Create vault TOML with the specific version + cat > "$vault_toml" </dev/null || true + + # Push branch + git push -u origin "$branch_name" 2>/dev/null || { + echo "Error: failed to push branch" >&2 + exit 1 + } + + # Create PR + local pr_response + pr_response=$(curl -sf -X POST \ + -H "Authorization: token ${FORGE_TOKEN}" \ + -H "Content-Type: application/json" \ + "${FORGE_URL}/api/v1/repos/${FORGE_OPS_REPO}/pulls" \ + -d "{\"title\":\"${pr_title}\",\"head\":\"${branch_name}\",\"base\":\"main\",\"body\":\"$(echo "$pr_body" | sed ':a;N;$!ba;s/\n/\\n/g')\"}" 2>/dev/null) || { + echo "Error: failed to create PR" >&2 + echo "Response: ${pr_response}" >&2 + exit 1 + } + + local pr_number + pr_number=$(echo "$pr_response" | jq -r '.number') + + local pr_url="${FORGE_URL}/${FORGE_OPS_REPO}/pulls/${pr_number}" + + # Enable auto-merge on the PR — Forgejo will auto-merge after approval + _vault_log "Enabling auto-merge for PR #${pr_number}" + curl -sf -X POST \ + -H "Authorization: token ${FORGE_TOKEN}" \ + -H "Content-Type: application/json" \ + "${FORGE_URL}/api/v1/repos/${FORGE_OPS_REPO}/pulls/${pr_number}/merge" \ + -d '{"Do":"merge","merge_when_checks_succeed":true}' >/dev/null 2>&1 || { + echo "Warning: failed to enable auto-merge (may already be enabled or not supported)" >&2 + } + + echo "" + echo "Release PR created: ${pr_url}" + echo "" + echo "Next steps:" + echo " 1. Review the PR" + echo " 2. Approve the PR (auto-merge will trigger after approval)" + echo " 3. The vault runner will execute the release formula" + echo "" + echo "After merge, the release will:" + echo " 1. Tag Forgejo main with ${version}" + echo " 2. Push tag to mirrors (Codeberg, GitHub)" + echo " 3. Build and tag the agents Docker image" + echo " 4. Restart agent containers" +} From 507e41a926f6126bf2a86dd82ce7abf0557014d9 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 6 Apr 2026 20:04:19 +0000 Subject: [PATCH 2/2] fix: use PRIMARY_BRANCH instead of hardcoded main in disinto_release The assert function declared PRIMARY_BRANCH as required but the implementation hardcoded 'main' in three places. Replace all three with $PRIMARY_BRANCH and call _assert_release_globals at entry. Co-Authored-By: Claude Sonnet 4.6 --- lib/release.sh | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/release.sh b/lib/release.sh index 9910996..33a01e2 100644 --- a/lib/release.sh +++ b/lib/release.sh @@ -36,6 +36,8 @@ _assert_release_globals() { } disinto_release() { + _assert_release_globals + local version="${1:-}" local formula_path="${FACTORY_ROOT}/formulas/release.toml" @@ -115,11 +117,11 @@ This PR creates a vault item for the release of version ${version}. 3. The vault runner will execute the release formula " - # Create branch from clean main + # Create branch from clean primary branch cd "$ops_root" - git checkout main - git pull origin main - git checkout -B "$branch_name" main + git checkout "$PRIMARY_BRANCH" + git pull origin "$PRIMARY_BRANCH" + git checkout -B "$branch_name" "$PRIMARY_BRANCH" # Add and commit only the vault TOML file git add "vault/actions/${id}.toml" @@ -137,7 +139,7 @@ This PR creates a vault item for the release of version ${version}. -H "Authorization: token ${FORGE_TOKEN}" \ -H "Content-Type: application/json" \ "${FORGE_URL}/api/v1/repos/${FORGE_OPS_REPO}/pulls" \ - -d "{\"title\":\"${pr_title}\",\"head\":\"${branch_name}\",\"base\":\"main\",\"body\":\"$(echo "$pr_body" | sed ':a;N;$!ba;s/\n/\\n/g')\"}" 2>/dev/null) || { + -d "{\"title\":\"${pr_title}\",\"head\":\"${branch_name}\",\"base\":\"${PRIMARY_BRANCH}\",\"body\":\"$(echo "$pr_body" | sed ':a;N;$!ba;s/\n/\\n/g')\"}" 2>/dev/null) || { echo "Error: failed to create PR" >&2 echo "Response: ${pr_response}" >&2 exit 1