From 16d2b1944016619aca33a613663be4998ce4b728 Mon Sep 17 00:00:00 2001 From: Agent Date: Wed, 1 Apr 2026 13:51:19 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20feat:=20versioned=20releases=20=E2=80=94?= =?UTF-8?q?=20vault-gated=20tag,=20image=20build,=20and=20deploy=20(#112)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bin/disinto | 143 ++++++++++++++++++++- docker/agents/Dockerfile | 5 +- formulas/release.toml | 245 ++++++++++++++++++++++++++++++++++++ vault/examples/release.toml | 35 ++++++ 4 files changed, 423 insertions(+), 5 deletions(-) create mode 100644 formulas/release.toml create mode 100644 vault/examples/release.toml diff --git a/bin/disinto b/bin/disinto index cc9a95d..9515340 100755 --- a/bin/disinto +++ b/bin/disinto @@ -40,6 +40,7 @@ Usage: disinto status Show factory status disinto secrets Manage encrypted secrets disinto run Run action in ephemeral runner container + disinto release Create vault PR for release (e.g., v1.2.0) disinto hire-an-agent [--formula ] 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,142 @@ 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 +# 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 + + # 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" + + # Read the release formula and inject version + local formula_content + formula_content=$(cat "$formula_path") + + # Create vault TOML with the specific version + cat > "$vault_toml" </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 + local auth_url + auth_url=$(printf '%s' "http://dev-bot:${FORGE_TOKEN}@$(echo "$FORGE_URL" | sed 's|https\{0,1}://||')" ) + local remote_url="${auth_url}/${PROJECT_REPO%/disinto}/.git" + + if ! git remote get-url origin >/dev/null 2>&1; then + # Get the actual remote URL from FORGE_URL + local forge_host + forge_host=$(echo "$FORGE_URL" | sed 's|https\{0,1}://||' | sed 's|/.*||') + local org_repo + org_repo=$(echo "$PROJECT_REPO" | sed 's|.*||') + remote_url="http://dev-bot:${FORGE_TOKEN}@${forge_host}/${PROJECT_REPO}.git" + fi + + 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 +2758,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 ;; diff --git a/docker/agents/Dockerfile b/docker/agents/Dockerfile index b1543fb..0b6fad5 100644 --- a/docker/agents/Dockerfile +++ b/docker/agents/Dockerfile @@ -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"] diff --git a/formulas/release.toml b/formulas/release.toml new file mode 100644 index 0000000..62add13 --- /dev/null +++ b/formulas/release.toml @@ -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 `). +# +# 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 <