fix: feat: versioned releases — vault-gated tag, image build, and deploy (#112)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful

This commit is contained in:
Agent 2026-04-01 13:51:19 +00:00
parent 0455040d02
commit a7ad6eb32a
4 changed files with 406 additions and 5 deletions

View file

@ -40,6 +40,7 @@ Usage:
disinto status Show factory status
disinto secrets <subcommand> Manage encrypted secrets
disinto run <action-id> Run action in ephemeral runner container
disinto release <version> Create vault PR for release (e.g., v1.2.0)
disinto hire-an-agent <agent-name> <role> [--formula <path>]
Hire a new agent (create user + .profile repo)
@ -232,7 +233,6 @@ services:
volumes:
- agent-data:/home/agent/data
- project-repos:/home/agent/repos
- ./:/home/agent/disinto:ro
- ${HOME}/.claude:/home/agent/.claude
- ${HOME}/.claude.json:/home/agent/.claude.json:ro
- CLAUDE_BIN_PLACEHOLDER:/usr/local/bin/claude:ro
@ -261,9 +261,7 @@ services:
security_opt:
- apparmor=unconfined
volumes:
- ./vault:/home/agent/disinto/vault
- ./lib:/home/agent/disinto/lib:ro
- ./formulas:/home/agent/disinto/formulas:ro
- agent-data:/home/agent/data
environment:
FORGE_URL: http://forgejo:3000
DISINTO_CONTAINER: "1"
@ -2613,6 +2611,125 @@ EOF
echo " Formula: ${role}.toml"
}
# ── 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 <version>
# 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 <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
# 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/pending/${id}.toml"
# Create vault TOML with the specific version
cat > "$vault_toml" <<EOF
# vault/pending/${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
"
# Create branch
cd "$ops_root"
git checkout -B "$branch_name" 2>/dev/null || git checkout "$branch_name"
# Add and commit
git add -A
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
}
# 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/${PROJECT_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}/${PROJECT_REPO}/pulls/${pr_number}"
echo ""
echo "Release PR created: ${pr_url}"
echo ""
echo "Next steps:"
echo " 1. Review the PR"
echo " 2. Approve and merge (requires 2 reviewers for vault items)"
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"
}
# ── Main dispatch ────────────────────────────────────────────────────────────
case "${1:-}" in
@ -2624,6 +2741,7 @@ case "${1:-}" in
status) shift; disinto_status "$@" ;;
secrets) shift; disinto_secrets "$@" ;;
run) shift; disinto_run "$@" ;;
release) shift; disinto_release "$@" ;;
hire-an-agent) shift; disinto_hire_an_agent "$@" ;;
-h|--help) usage ;;
*) usage ;;

View file

@ -24,11 +24,14 @@ RUN curl -sL https://dl.gitea.com/tea/0.9.2/tea-0.9.2-linux-amd64 -o /usr/local/
# Non-root user
RUN useradd -m -u 1000 -s /bin/bash agent
# Copy disinto code into the image
COPY . /home/agent/disinto
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
# Entrypoint runs as root to start the cron daemon;
# cron jobs execute as the agent user (crontab -u agent).
WORKDIR /home/agent
WORKDIR /home/agent/disinto
ENTRYPOINT ["/entrypoint.sh"]

245
formulas/release.toml Normal file
View file

@ -0,0 +1,245 @@
# formulas/release.toml — Release formula
#
# Defines the release workflow: tag Forgejo main, push to mirrors, build
# and tag the agents Docker image, and restart agents.
#
# Triggered by vault PR approval (human creates vault PR, approves it, then
# runner executes via `disinto run <id>`).
#
# Example vault item:
# id = "release-v1.2.0"
# formula = "release"
# context = "Tag v1.2.0 — includes vault redesign, .profile system, architect agent"
# secrets = []
#
# Steps: preflight → tag-main → push-mirrors → build-image → tag-image → restart-agents → commit-result
name = "release"
description = "Tag Forgejo main, push to mirrors, build and tag agents image, restart agents"
version = 1
[context]
files = ["docker-compose.yml"]
# ─────────────────────────────────────────────────────────────────────────────────
# Step 1: preflight
# ─────────────────────────────────────────────────────────────────────────────────
[[steps]]
id = "preflight"
title = "Validate release prerequisites"
description = """
Validate release prerequisites before proceeding.
1. Check that RELEASE_VERSION is set:
- Must be in format: v1.2.3 (semver with 'v' prefix)
- Validate with regex: ^v[0-9]+\\.[0-9]+\\.[0-9]+$
- If not set, exit with error
2. Check that FORGE_TOKEN and FORGE_URL are set:
- Required for Forgejo API calls
3. Check that DOCKER_HOST is accessible:
- Test with: docker info
- Required for image build
4. Check current branch is main:
- git rev-parse --abbrev-ref HEAD
- Must be 'main' or 'master'
5. Pull latest code:
- git fetch origin "$PRIMARY_BRANCH"
- git reset --hard origin/"$PRIMARY_BRANCH"
- Ensure working directory is clean
6. Check if tag already exists locally:
- git tag -l "$RELEASE_VERSION"
- If exists, exit with error
7. Check if tag already exists on Forgejo:
- curl -sf -H "Authorization: token $FORGE_TOKEN" \
- "$FORGE_URL/api/v1/repos/johba/disinto/git/tags/$RELEASE_VERSION"
- If exists, exit with error
8. Export RELEASE_VERSION for subsequent steps:
- export RELEASE_VERSION (already set from vault action)
"""
# ─────────────────────────────────────────────────────────────────────────────────
# Step 2: tag-main
# ─────────────────────────────────────────────────────────────────────────────────
[[steps]]
id = "tag-main"
title = "Create tag on Forgejo main via API"
description = """
Create the release tag on Forgejo main via the Forgejo API.
1. Get current HEAD SHA of main:
- curl -sf -H "Authorization: token $FORGE_TOKEN" \
- "$FORGE_URL/api/v1/repos/johba/disinto/branches/$PRIMARY_BRANCH"
- Parse sha field from response
2. Create tag via Forgejo API:
- curl -sf -X POST \
- -H "Authorization: token $FORGE_TOKEN" \
- -H "Content-Type: application/json" \
- "$FORGE_URL/api/v1/repos/johba/disinto/tags" \
- -d "{\"tag\":\"$RELEASE_VERSION\",\"target\":\"$HEAD_SHA\",\"message\":\"Release $RELEASE_VERSION\"}"
- Parse response for success
3. Log the tag creation:
- echo "Created tag $RELEASE_VERSION on Forgejo (SHA: $HEAD_SHA)"
4. Store HEAD SHA for later verification:
- echo "$HEAD_SHA" > /tmp/release-head-sha
"""
# ─────────────────────────────────────────────────────────────────────────────────
# Step 3: push-mirrors
# ─────────────────────────────────────────────────────────────────────────────────
[[steps]]
id = "push-mirrors"
title = "Push tag to mirrors (Codeberg, GitHub)"
description = """
Push the newly created tag to all configured mirrors.
1. Add mirror remotes if not already present:
- Codeberg: git remote add codeberg git@codeberg.org:johba/disinto.git
- GitHub: git remote add github git@github.com:disinto/disinto.git
- Check with: git remote -v
2. Push tag to Codeberg:
- git push codeberg "$RELEASE_VERSION" --tags
- Or push all tags: git push codeberg --tags
3. Push tag to GitHub:
- git push github "$RELEASE_VERSION" --tags
- Or push all tags: git push github --tags
4. Verify tags exist on mirrors:
- curl -sf -H "Authorization: token $GITHUB_TOKEN" \
- "https://api.github.com/repos/disinto/disinto/tags/$RELEASE_VERSION"
- curl -sf -H "Authorization: token $FORGE_TOKEN" \
- "$FORGE_URL/api/v1/repos/johba/disinto/git/tags/$RELEASE_VERSION"
5. Log success:
- echo "Tag $RELEASE_VERSION pushed to mirrors"
"""
# ─────────────────────────────────────────────────────────────────────────────────
# Step 4: build-image
# ─────────────────────────────────────────────────────────────────────────────────
[[steps]]
id = "build-image"
title = "Build agents Docker image"
description = """
Build the new agents Docker image with the tagged code.
1. Build image without cache to ensure fresh build:
- docker compose build --no-cache agents
2. Verify image was created:
- docker images | grep disinto-agents
- Check image exists and has recent timestamp
3. Store image ID for later:
- docker images disinto-agents --format "{{.ID}}" > /tmp/release-image-id
4. Log build completion:
- echo "Built disinto-agents image"
"""
# ─────────────────────────────────────────────────────────────────────────────────
# Step 5: tag-image
# ─────────────────────────────────────────────────────────────────────────────────
[[steps]]
id = "tag-image"
title = "Tag Docker image with version"
description = """
Tag the newly built agents image with the release version.
1. Get the untagged image ID:
- docker images disinto-agents --format "{{.ID}}" --no-trunc | head -1
2. Tag the image:
- docker tag disinto-agents disinto-agents:$RELEASE_VERSION
3. Verify tag:
- docker images disinto-agents
4. Log tag:
- echo "Tagged disinto-agents:$RELEASE_VERSION"
"""
# ─────────────────────────────────────────────────────────────────────────────────
# Step 6: restart-agents
# ─────────────────────────────────────────────────────────────────────────────────
[[steps]]
id = "restart-agents"
title = "Restart agent containers with new image"
description = """
Restart agent containers to use the new image.
1. Pull the new image (in case it was pushed somewhere):
- docker compose pull agents
2. Stop and remove existing agent containers:
- docker compose down agents agents-llama 2>/dev/null || true
3. Start agents with new image:
- docker compose up -d agents agents-llama
4. Wait for containers to be healthy:
- for i in {1..30}; do
- if docker inspect --format='{{.State.Health.Status}}' agents | grep -q healthy; then
- echo "Agents container healthy"; break
- fi
- sleep 5
- done
5. Verify containers are running:
- docker compose ps agents agents-llama
6. Log restart:
- echo "Restarted agents containers"
"""
# ─────────────────────────────────────────────────────────────────────────────────
# Step 7: commit-result
# ─────────────────────────────────────────────────────────────────────────────────
[[steps]]
id = "commit-result"
title = "Write release result"
description = """
Write the release result to a file for tracking.
1. Get the image ID:
- IMAGE_ID=$(cat /tmp/release-image-id)
2. Create result file:
- cat > /tmp/release-result.json <<EOF
- {
- "version": "$RELEASE_VERSION",
- "image_id": "$IMAGE_ID",
- "forgejo_tag_url": "$FORGE_URL/johba/disinto/src/$RELEASE_VERSION",
- "timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
- "status": "success"
- }
- EOF
3. Copy result to data directory:
- mkdir -p "$PROJECT_REPO_ROOT/release"
- cp /tmp/release-result.json "$PROJECT_REPO_ROOT/release/$RELEASE_VERSION.json"
4. Log result:
- cat /tmp/release-result.json
5. Clean up temp files:
- rm -f /tmp/release-head-sha /tmp/release-image-id /tmp/release-result.json
"""

View file

@ -0,0 +1,35 @@
# vault/examples/release.toml
# Example: Release vault item schema
#
# This example demonstrates the release vault item schema for creating
# versioned releases with vault-gated approval.
#
# The release formula tags Forgejo main, pushes to mirrors, builds and
# tags the agents Docker image, and restarts agent containers.
#
# Example vault item (auto-generated by `disinto release v1.2.0`):
#
# id = "release-v120"
# formula = "release"
# context = "Release v1.2.0"
# secrets = []
#
# Steps executed by the release formula:
# 1. preflight - Validate prerequisites (version, FORGE_TOKEN, Docker)
# 2. tag-main - Create tag on Forgejo main via API
# 3. push-mirrors - Push tag to Codeberg and GitHub mirrors
# 4. build-image - Build agents Docker image with --no-cache
# 5. tag-image - Tag image with version (disinto-agents:v1.2.0)
# 6. restart-agents - Restart agent containers with new image
# 7. commit-result - Write release result to tracking file
id = "release-v120"
formula = "release"
context = "Release v1.2.0 — includes vault redesign, .profile system, architect agent"
secrets = []
# Optional: specify a larger model for complex release logic
# model = "sonnet"
# Optional: releases may take longer due to Docker builds
# timeout_minutes = 60