Merge pull request 'fix: feat: versioned releases — vault-gated tag, image build, and deploy (#112)' (#114) from fix/issue-112 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
efe57a02c9
4 changed files with 406 additions and 5 deletions
126
bin/disinto
126
bin/disinto
|
|
@ -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 ;;
|
||||
|
|
|
|||
|
|
@ -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
245
formulas/release.toml
Normal 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
|
||||
"""
|
||||
35
vault/examples/release.toml
Normal file
35
vault/examples/release.toml
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue