Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/release-labeler.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
labels:
- name: release:minor
color: "1d76db"
description: "Release should bump minor version"
- name: release:patch
color: "0e8a16"
description: "Release should bump patch version"
- name: release:none
color: "6e7781"
description: "Release should not cut a version"
133 changes: 21 additions & 112 deletions .github/workflows/release-on-main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ jobs:
if [ -z "$LAST_TAG" ]; then
LAST_TAG="v0.0.0"
fi
CURRENT_VERSION=$(node -p "require('./package.json').version")
echo "last_tag=$LAST_TAG" >> "$GITHUB_OUTPUT"
echo "current_version=$CURRENT_VERSION" >> "$GITHUB_OUTPUT"

- name: Gather merged PRs since last tag
id: prs
Expand Down Expand Up @@ -90,122 +92,29 @@ jobs:

echo "has_changes=true" >> "$GITHUB_OUTPUT"

- name: Decide release bump with Claude Haiku 4.5
- name: Decide release bump from PR labels
id: decide
if: steps.prs.outputs.has_changes == 'true'
env:
ANTHROPIC_API_KEY: ${{ secrets.CI_ANTHROPIC_KEY }}
LAST_TAG: ${{ steps.last_tag.outputs.last_tag }}
run: |
set -euo pipefail
if [ -z "${ANTHROPIC_API_KEY:-}" ]; then
echo "Missing CI_ANTHROPIC_KEY secret" >&2
exit 1
fi

read -r -d '' PROMPT <<'PROMPT_EOF' || true
You are a release manager. Analyze merged pull requests since the last release and decide semver bump.
Allowed outputs:
- none
- patch
- minor

Rules:
- Never return major. Major releases are manual-only.
- Use a judicious standard: user-facing features, capability expansion, or notable additive behavior => minor.
- Bug fixes, refactors, infra/internal changes, docs/tests only => patch or none.
- If no meaningful published change, choose none.

Return ONLY strict JSON:
{"decision":"none|patch|minor","reason":"short reason","highlights":["...","..."]}
PROMPT_EOF
PROMPT=$(echo "$PROMPT" | sed 's/^ //')

jq -n \
--arg model "claude-haiku-4-5" \
--arg system "You are precise and must output strict JSON only." \
--arg prompt "$PROMPT" \
--arg last_tag "$LAST_TAG" \
--slurpfile prs /tmp/pr_context_capped.json \
'{
model: $model,
max_tokens: 700,
temperature: 0,
system: $system,
messages: [
{role: "user", content: ($prompt + "\n\nLast release tag: " + $last_tag + "\n\nPRs:\n" + ($prs[0]|tojson))}
]
}' > /tmp/anthropic-payload.json

curl -sS https://api.anthropic.com/v1/messages \
-H "x-api-key: ${ANTHROPIC_API_KEY}" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
--data @/tmp/anthropic-payload.json > /tmp/anthropic-response.json

TEXT=$(jq -r '.content[0].text // empty' /tmp/anthropic-response.json)
if [ -z "$TEXT" ]; then
echo "Invalid Anthropic response" >&2
cat /tmp/anthropic-response.json >&2
exit 1
fi

echo "$TEXT" > /tmp/decision-raw.txt
python3 - <<'PY'
import json
import re
import sys

text = open('/tmp/decision-raw.txt', encoding='utf-8').read().strip()

def parse_json(candidate: str):
try:
return json.loads(candidate)
except Exception:
return None

decision = parse_json(text)
if decision is None:
fenced = re.search(r"```(?:json)?\s*([\s\S]*?)\s*```", text, re.IGNORECASE)
if fenced:
decision = parse_json(fenced.group(1).strip())

if decision is None:
decoder = json.JSONDecoder()
for index, ch in enumerate(text):
if ch != '{':
continue
try:
decision, _ = decoder.raw_decode(text[index:])
break
except Exception:
continue

if decision is None:
print("Failed to parse release decision JSON from model response", file=sys.stderr)
print(text, file=sys.stderr)
sys.exit(1)

with open('/tmp/decision.json', 'w', encoding='utf-8') as fh:
json.dump(decision, fh)
fh.write('\n')
PY

DECISION=$(jq -r '.decision' /tmp/decision.json)
REASON=$(jq -r '.reason' /tmp/decision.json)

if [ "$DECISION" = "major" ]; then
echo "Major bump proposed but blocked by policy" >&2
exit 1
fi

case "$DECISION" in
none|patch|minor) ;;
*)
echo "Unexpected decision: $DECISION" >&2
exit 1
;;
esac
DECISION=$(jq -r '
if any(.[]; (.labels // []) | index("release:minor")) then "minor"
elif any(.[]; (.labels // []) | index("release:patch")) then "patch"
elif all(.[]; ((.labels // []) | index("release:none"))) then "none"
else "patch"
end
' /tmp/pr_context.json)

REASON=$(jq -r --arg decision "$DECISION" '
if $decision == "minor" then
"At least one merged PR requested a minor release via label."
elif $decision == "patch" then
"No minor label found; defaulting to patch for shipped changes."
else
"All merged PRs were explicitly marked release:none."
end
' /tmp/pr_context.json)

echo "decision=$DECISION" >> "$GITHUB_OUTPUT"
echo "reason=$REASON" >> "$GITHUB_OUTPUT"
Expand All @@ -228,7 +137,7 @@ jobs:
BUMP: ${{ steps.decide.outputs.decision }}
run: |
set -euo pipefail
CURRENT=$(node -p "require('./package.json').version")
CURRENT="${{ steps.last_tag.outputs.current_version }}"
NEXT=$(node -e "const v=process.argv[1].split('.').map(Number); if(process.argv[2]==='minor'){v[1]++; v[2]=0;} else {v[2]++;} process.stdout.write(v.join('.'))" -- "${CURRENT}" "${BUMP}")
node -e "const fs=require('fs'); const p='package.json'; const j=JSON.parse(fs.readFileSync(p,'utf8')); j.version=process.argv[1]; fs.writeFileSync(p, JSON.stringify(j,null,2)+'\\n');" -- "${NEXT}"
if [ -f package-lock.json ]; then
Expand Down
2 changes: 2 additions & 0 deletions CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,8 @@ Broker mode also emits best-effort context usage telemetry in inbox pull `meta`

### Release Updater / Rollback (CLI env overrides)

Baudbot release versioning is driven by the root `package.json.version`. Runtime and release metadata record both semver and git SHA, while on-disk release snapshots remain SHA-addressed.

These are **command-time overrides** for `baudbot update` / `baudbot rollback` (or the underlying scripts). They are not required in `~/.config/.env`.

| Variable | Description | Default |
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ See [SECURITY.md](SECURITY.md) for full threat model, trust boundaries, and know
- [docs/linux-runtime.md](docs/linux-runtime.md) — Linux execution model, tools, and constraints
- [docs/operations.md](docs/operations.md) — day-2 operations (start/stop/update/rollback/audit)
- [docs/architecture.md](docs/architecture.md) — source/runtime/release architecture
- [docs/releases.md](docs/releases.md) — semver policy and release automation
- [CONFIGURATION.md](CONFIGURATION.md) — full env var reference
- [SECURITY.md](SECURITY.md) — deep security model and vulnerability reporting
- [CONTRIBUTING.md](CONTRIBUTING.md) — contribution workflow
Expand Down
17 changes: 17 additions & 0 deletions bin/baudbot
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,18 @@ if [ -f "$RUNTIME_NODE_HELPER" ]; then
source "$RUNTIME_NODE_HELPER"
fi

JSON_COMMON_HELPER="$BAUDBOT_ROOT/bin/lib/json-common.sh"
if [ -f "$JSON_COMMON_HELPER" ]; then
# shellcheck source=bin/lib/json-common.sh
source "$JSON_COMMON_HELPER"
fi

VERSION_COMMON_HELPER="$BAUDBOT_ROOT/bin/lib/version-common.sh"
if [ -f "$VERSION_COMMON_HELPER" ]; then
# shellcheck source=bin/lib/version-common.sh
source "$VERSION_COMMON_HELPER"
fi

json_get_string_or_empty() {
local file="$1"
local key="$2"
Expand Down Expand Up @@ -63,6 +75,11 @@ else
fi

version() {
if [ -n "${VERSION_COMMON_HELPER:-}" ] && [ -f "$VERSION_COMMON_HELPER" ]; then
bb_package_version_or_unknown "$BAUDBOT_ROOT"
return 0
fi

local package_json="$BAUDBOT_ROOT/package.json"
local pkg_version=""

Expand Down
14 changes: 13 additions & 1 deletion bin/deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ source "$SCRIPT_DIR/lib/json-common.sh"
source "$SCRIPT_DIR/lib/deploy-common.sh"
# shellcheck source=bin/lib/runtime-node.sh
source "$SCRIPT_DIR/lib/runtime-node.sh"
# shellcheck source=bin/lib/version-common.sh
source "$SCRIPT_DIR/lib/version-common.sh"
bb_enable_strict_mode
bb_init_paths

Expand Down Expand Up @@ -422,25 +424,35 @@ if [ "$DRY_RUN" -eq 0 ]; then
GIT_SHA=""
GIT_SHA_SHORT=""
GIT_BRANCH=""
RELEASE_VERSION=""
RELEASE_TAG=""

if (cd "$BAUDBOT_SRC" && git rev-parse HEAD >/dev/null 2>&1); then
GIT_SHA=$(cd "$BAUDBOT_SRC" && git rev-parse HEAD 2>/dev/null || echo "unknown")
GIT_SHA_SHORT=$(cd "$BAUDBOT_SRC" && git rev-parse --short HEAD 2>/dev/null || echo "unknown")
GIT_BRANCH=$(cd "$BAUDBOT_SRC" && git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
RELEASE_VERSION="$(bb_package_version_or_unknown "$BAUDBOT_SRC")"
RELEASE_TAG="$(bb_release_tag_for_version "$RELEASE_VERSION")"
elif [ -f "$RELEASE_META_FILE" ]; then
GIT_SHA="$(json_get_string_or_empty "$RELEASE_META_FILE" "sha")"
GIT_SHA_SHORT="$(json_get_string_or_empty "$RELEASE_META_FILE" "short")"
GIT_BRANCH="$(json_get_string_or_empty "$RELEASE_META_FILE" "branch")"
RELEASE_VERSION="$(json_get_string_or_empty "$RELEASE_META_FILE" "version")"
RELEASE_TAG="$(json_get_string_or_empty "$RELEASE_META_FILE" "tag")"
fi

[ -n "$GIT_SHA" ] || GIT_SHA="unknown"
[ -n "$GIT_SHA_SHORT" ] || GIT_SHA_SHORT="unknown"
[ -n "$GIT_BRANCH" ] || GIT_BRANCH="unknown"
[ -n "$RELEASE_VERSION" ] || RELEASE_VERSION="unknown"
[ -n "$RELEASE_TAG" ] || RELEASE_TAG="$(bb_release_tag_for_version "$RELEASE_VERSION")"
DEPLOY_TS=$(date -u +"%Y-%m-%dT%H:%M:%SZ")

# Write version file via agent
as_agent bash -c "cat > '$VERSION_FILE'" <<VEOF
{
"version": "$RELEASE_VERSION",
"tag": "$RELEASE_TAG",
"sha": "$GIT_SHA",
"short": "$GIT_SHA_SHORT",
"branch": "$GIT_BRANCH",
Expand All @@ -449,7 +461,7 @@ if [ "$DRY_RUN" -eq 0 ]; then
}
VEOF
Comment on lines 449 to 462
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 "vunknown" written into metadata JSON when version is unavailable

When neither the git repo nor an existing $RELEASE_META_FILE provides a version (e.g., a freshly checked-out archive with no package.json), RELEASE_VERSION is set to "unknown". The subsequent bb_release_tag_for_version "unknown" call then writes "tag": "vunknown" into baudbot-version.json. Tools or operators parsing the tag field for semver validity would receive an unexpected value. Guarding the bb_release_tag_for_version call to only run when RELEASE_VERSION is not "unknown" would be more consistent.

Prompt To Fix With AI
This is a comment left during a code review.
Path: bin/deploy.sh
Line: 449-462

Comment:
**`"vunknown"` written into metadata JSON when version is unavailable**

When neither the git repo nor an existing `$RELEASE_META_FILE` provides a version (e.g., a freshly checked-out archive with no `package.json`), `RELEASE_VERSION` is set to `"unknown"`. The subsequent `bb_release_tag_for_version "unknown"` call then writes `"tag": "vunknown"` into `baudbot-version.json`. Tools or operators parsing the tag field for semver validity would receive an unexpected value. Guarding the `bb_release_tag_for_version` call to only run when `RELEASE_VERSION` is not `"unknown"` would be more consistent.

How can I resolve this? If you propose a fix, please make it concise.

as_agent chmod 644 "$VERSION_FILE"
log "✓ baudbot-version.json ($GIT_SHA_SHORT @ $GIT_BRANCH)"
log "✓ baudbot-version.json ($RELEASE_VERSION, $GIT_SHA_SHORT @ $GIT_BRANCH)"

# Generate sha256 manifest of all deployed files (excluding node_modules)
# Agent reads its own files to compute hashes
Expand Down
16 changes: 12 additions & 4 deletions bin/lib/baudbot-runtime.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,17 @@ has_systemd() {
print_deployed_version() {
local agent_user="${BAUDBOT_AGENT_USER:-baudbot_agent}"
local version_file="/home/$agent_user/.pi/agent/baudbot-version.json"
local version=""
local tag=""
local short=""
local sha=""
local branch=""
local deployed_at=""
local line=""

if [ -r "$version_file" ]; then
version="$(json_get_string_or_empty "$version_file" "version")"
tag="$(json_get_string_or_empty "$version_file" "tag")"
short="$(json_get_string_or_empty "$version_file" "short")"
sha="$(json_get_string_or_empty "$version_file" "sha")"
branch="$(json_get_string_or_empty "$version_file" "branch")"
Expand All @@ -28,21 +32,23 @@ print_deployed_version() {
local version_json=""
version_json="$(sudo -u "$agent_user" sh -c "cat '$version_file' 2>/dev/null" || true)"
if [ -n "$version_json" ]; then
version="$(printf '%s' "$version_json" | json_get_string_stdin_or_empty "version" 2>/dev/null || true)"
tag="$(printf '%s' "$version_json" | json_get_string_stdin_or_empty "tag" 2>/dev/null || true)"
short="$(printf '%s' "$version_json" | json_get_string_stdin_or_empty "short" 2>/dev/null || true)"
sha="$(printf '%s' "$version_json" | json_get_string_stdin_or_empty "sha" 2>/dev/null || true)"
branch="$(printf '%s' "$version_json" | json_get_string_stdin_or_empty "branch" 2>/dev/null || true)"
deployed_at="$(printf '%s' "$version_json" | json_get_string_stdin_or_empty "deployed_at" 2>/dev/null || true)"
fi
fi

if [ -z "$short" ] && [ -z "$sha" ] && [ -z "$branch" ] && [ -z "$deployed_at" ]; then
if [ -z "$version" ] && [ -z "$short" ] && [ -z "$sha" ] && [ -z "$branch" ] && [ -z "$deployed_at" ]; then
local release_target=""
local release_sha=""

release_target="$(readlink -f /opt/baudbot/current 2>/dev/null || true)"
if printf '%s\n' "$release_target" | grep -Eq '/releases/[0-9a-f]{7,40}$'; then
release_sha="${release_target##*/}"
echo -e "${BOLD}deployed version:${RESET} ${release_sha:0:7} sha: $release_sha (from /opt/baudbot/current)"
echo -e "${BOLD}deployed version:${RESET} unknown (${release_sha:0:7}) sha: $release_sha (from /opt/baudbot/current)"
else
echo -e "${BOLD}deployed version:${RESET} unavailable"
fi
Expand All @@ -53,8 +59,10 @@ print_deployed_version() {
short="${sha:0:7}"
fi

line="${short:-unknown}"
[ -n "$branch" ] && line="$line (branch: $branch)"
line="${version:-unknown}"
[ -n "$short" ] && line="$line ($short)"
[ -n "$tag" ] && line="$line tag: $tag"
[ -n "$branch" ] && line="$line branch: $branch"
[ -n "$deployed_at" ] && line="$line deployed: $deployed_at"
[ -n "$sha" ] && line="$line sha: $sha"

Expand Down
8 changes: 7 additions & 1 deletion bin/lib/release-runtime-common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,10 @@ bb_verify_deployed_release_sha() {

local version_file="$BAUDBOT_AGENT_HOME/.pi/agent/baudbot-version.json"
local deployed_sha
local deployed_version

deployed_sha="$(sudo -u "$BAUDBOT_AGENT_USER" sh -c "cat '$version_file' 2>/dev/null" | json_get_string_stdin "sha" 2>/dev/null || true)"
deployed_version="$(sudo -u "$BAUDBOT_AGENT_USER" sh -c "cat '$version_file' 2>/dev/null" | json_get_string_stdin "version" 2>/dev/null || true)"

if [ -z "$deployed_sha" ]; then
die "deployed version file missing or unreadable: $version_file"
Expand All @@ -71,6 +73,10 @@ bb_verify_deployed_release_sha() {
fi

if [ -n "$verified_label" ]; then
log "deployed version verified: $verified_label"
if [ -n "$deployed_version" ]; then
log "deployed version verified: $deployed_version ($verified_label)"
else
log "deployed version verified: $verified_label"
fi
fi
}
32 changes: 32 additions & 0 deletions bin/lib/version-common.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#!/bin/bash
# Shared version helpers for Baudbot shell scripts.

bb_package_json_path() {
local root="${1:?repo root required}"
echo "$root/package.json"
}

bb_package_lock_json_path() {
local root="${1:?repo root required}"
echo "$root/package-lock.json"
}

bb_package_version() {
local root="${1:?repo root required}"
local package_json=""

package_json="$(bb_package_json_path "$root")"
[ -r "$package_json" ] || return 1

json_get_string "$package_json" "version"
}
Comment on lines +14 to +22
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Undocumented dependency on json_get_string from json-common.sh

bb_package_version calls json_get_string, which is defined in bin/lib/json-common.sh and not in this file. All current callers (deploy.sh, update-release.sh, baudbot, the test file) correctly source json-common.sh first, but there is no declaration of this requirement in version-common.sh itself. A sourcing guard or a comment at the top noting the prerequisite would prevent subtle breakage if this helper is ever sourced in a new context without the dependency.

Prompt To Fix With AI
This is a comment left during a code review.
Path: bin/lib/version-common.sh
Line: 14-22

Comment:
**Undocumented dependency on `json_get_string` from `json-common.sh`**

`bb_package_version` calls `json_get_string`, which is defined in `bin/lib/json-common.sh` and not in this file. All current callers (`deploy.sh`, `update-release.sh`, `baudbot`, the test file) correctly source `json-common.sh` first, but there is no declaration of this requirement in `version-common.sh` itself. A sourcing guard or a comment at the top noting the prerequisite would prevent subtle breakage if this helper is ever sourced in a new context without the dependency.

How can I resolve this? If you propose a fix, please make it concise.


bb_package_version_or_unknown() {
local root="${1:?repo root required}"
bb_package_version "$root" 2>/dev/null || echo "unknown"
}

bb_release_tag_for_version() {
local version="${1:?version required}"
echo "v$version"
}
Loading
Loading