fix(ccp): make agent functional + fix Gitea release timestamp bug

Three related fixes uncovered during a marcelle CCP registration test:

1. ccp-agent image was missing bash + curl + jq + python3, so every
   spawn('bash', ...) in upgrade.routes.ts and backup.routes.ts failed
   silently with ENOENT. CCP kept reading stale status.json files from
   disk, masking that no agent had successfully checked for updates in
   weeks. apk-add the missing tools.

2. ccp-agent's /app/instance mount was :ro, blocking the agent from
   writing data/upgrade/status.json (and result/progress/backups).
   Agent already has docker.sock — removing :ro is not a security
   escalation. Patched both docker-compose.yml and docker-compose.prod.yml.

3. Gitea 1.23.x only initializes Release.CreatedUnix inside its
   createTag() helper, which is skipped if the tag already exists on
   origin. The old DEV_WORKFLOW pattern (push tag, then run
   build-release.sh --upload) was triggering this — releases got
   created_unix=0 and lost /releases/latest sort order to v2.9.14.
   build-release.sh now removes the remote tag first and POSTs with
   target_commitish so Gitea creates the tag and release atomically.

After these fixes, CCP's "Check for Updates" path returns truthful
data end-to-end (verified on marcelle: v2.9.15 -> v2.10.1, 1 behind).

Bunker Admin
This commit is contained in:
bunker-admin 2026-05-20 11:59:35 -06:00
parent a82e95946b
commit a531f9b9ce
4 changed files with 24 additions and 6 deletions

View File

@ -8,7 +8,10 @@ COPY src/ ./src/
RUN npx tsc RUN npx tsc
FROM node:20-alpine FROM node:20-alpine
RUN apk add --no-cache docker-cli docker-cli-compose git rsync # bash + curl + jq + python3 are required by the changemaker scripts the agent
# shells out to (upgrade-check.sh, upgrade.sh, backup.sh). Without them, every
# /upgrade/* and /backup/* call returns "command not found" failures.
RUN apk add --no-cache docker-cli docker-cli-compose git rsync bash curl jq python3
WORKDIR /app WORKDIR /app
COPY package*.json ./ COPY package*.json ./
RUN npm ci --production RUN npm ci --production

View File

@ -1427,9 +1427,10 @@ services:
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
- ccp-agent-data:/var/lib/ccp-agent - ccp-agent-data:/var/lib/ccp-agent
- ccp-agent-certs:/etc/ccp-agent - ccp-agent-certs:/etc/ccp-agent
# Mount the instance directory so the agent can read compose files and run # Mount the instance directory so the agent can read compose files and
# `docker compose -p <project>` commands against the real project on disk. # write status.json + backups (writable; agent already has docker.sock,
- .:/app/instance:ro # so file write access is not an additional security escalation).
- .:/app/instance
environment: environment:
- AGENT_PORT=7443 - AGENT_PORT=7443
- AGENT_DATA_DIR=/var/lib/ccp-agent - AGENT_DATA_DIR=/var/lib/ccp-agent

View File

@ -1450,7 +1450,10 @@ services:
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
- ccp-agent-data:/var/lib/ccp-agent - ccp-agent-data:/var/lib/ccp-agent
- ccp-agent-certs:/etc/ccp-agent - ccp-agent-certs:/etc/ccp-agent
- .:/app/instance:ro # Writable: agent must write data/upgrade/{status,progress,result}.json
# and data/backups/*.tar.gz. Agent already has docker.sock — file write
# access is not an additional security escalation.
- .:/app/instance
environment: environment:
- AGENT_PORT=7443 - AGENT_PORT=7443
- AGENT_DATA_DIR=/var/lib/ccp-agent - AGENT_DATA_DIR=/var/lib/ccp-agent

View File

@ -295,12 +295,23 @@ if [[ "$UPLOAD" == "true" ]]; then
fi fi
fi fi
# Gitea 1.23.x only initializes Release.CreatedUnix inside its createTag()
# path. If the git tag already exists on origin when we POST /releases,
# createTag() is skipped and CreatedUnix stays 0, which makes /releases/latest
# silently return an older release. Remove the remote tag first so Gitea
# creates it via target_commitish below. The tag is preserved locally and
# gets recreated at the same SHA — no history is lost.
if git ls-remote --exit-code origin "refs/tags/${TAG}" >/dev/null 2>&1; then
warn "Removing remote tag ${TAG} so Gitea can recreate it (CreatedUnix init)"
git push origin ":refs/tags/${TAG}" >/dev/null 2>&1 || true
fi
info "Creating Gitea release ${TAG}..." info "Creating Gitea release ${TAG}..."
RELEASE_RESPONSE=$(curl -sf -X POST \ RELEASE_RESPONSE=$(curl -sf -X POST \
"${GITEA_HOST}/api/v1/repos/admin/changemaker.lite/releases" \ "${GITEA_HOST}/api/v1/repos/admin/changemaker.lite/releases" \
-H "Authorization: token ${GITEA_TOKEN}" \ -H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d "{\"tag_name\":\"${TAG}\",\"name\":\"Changemaker Lite ${TAG}\",\"body\":\"Release ${TAG} (${COMMIT_SHA})\"}" \ -d "{\"tag_name\":\"${TAG}\",\"target_commitish\":\"${COMMIT_SHA}\",\"name\":\"Changemaker Lite ${TAG}\",\"body\":\"Release ${TAG} (${COMMIT_SHA})\"}" \
2>/dev/null || true) 2>/dev/null || true)
RELEASE_ID=$(echo "$RELEASE_RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true) RELEASE_ID=$(echo "$RELEASE_RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)