2026-04-06 19:51:59 +00:00
#!/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 <version>
# =============================================================================
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( ) {
2026-04-06 20:04:19 +00:00
_assert_release_globals
2026-04-06 19:51:59 +00:00
local version = " ${ 1 :- } "
local formula_path = " ${ FACTORY_ROOT } /formulas/release.toml "
if [ -z " $version " ] ; then
echo "Error: version required" >& 2
echo "Usage: disinto release <version>" >& 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 " <<EOF
# vault/actions/${id}.toml
# Release vault item for ${version}
# Auto-generated by disinto release
id = " ${ id } "
formula = "release"
context = " Release ${ version } "
secrets = [ ]
EOF
echo " Created vault item: ${ vault_toml } "
# Create a PR to submit the vault item to the ops repo
local branch_name = " release/ ${ version //./ } "
local pr_title = " release: ${ version } "
local pr_body = " Release ${ version }
This PR creates a vault item for the release of version ${ version } .
## Changes
- Added vault item: ${ id } .toml
## Next Steps
1. Review this PR
2. Approve and merge
3. The vault runner will execute the release formula
"
2026-04-06 20:04:19 +00:00
# Create branch from clean primary branch
2026-04-07 00:13:26 +00:00
(
cd " $ops_root "
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 "
git commit -m " $pr_title " -m " $pr_body " 2>/dev/null || true
# Push branch
git push -u origin " $branch_name " 2>/dev/null || {
echo "Error: failed to push branch" >& 2
exit 1
}
)
2026-04-06 19:51:59 +00:00
# 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 " \
2026-04-06 20:04:19 +00:00
-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) || {
2026-04-06 19:51:59 +00:00
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"
}