docs: sync KB — tag-triggered-release-deploy.md,release-2026.3.20.md claude-code-config.md
All checks were successful
Reindex Knowledge Base / reindex (push) Successful in 3s

This commit is contained in:
Cal Corum 2026-03-20 14:00:43 -05:00
parent 730f100619
commit b1fed02219
3 changed files with 398 additions and 7 deletions

View File

@ -0,0 +1,254 @@
---
title: "Tag-Triggered Release and Deploy Guide"
description: "CalVer tag-triggered CI/CD workflow: push a git tag to build Docker images, then deploy with a script. Reference implementation from Major Domo Discord bot."
type: guide
domain: development
tags: [docker, gitea, deployment, ci, calver, scripts, bash]
---
# Tag-Triggered Release and Deploy Guide
Standard release workflow for Dockerized applications using CalVer git tags to trigger CI builds and a deploy script for production rollout. Decouples code merging from releasing — merges to `main` are free, releases are intentional.
## Overview
```
merge PR to main → code lands, nothing builds
.scripts/release.sh → creates CalVer tag, pushes to trigger CI
CI builds → Docker image tagged with version + "production"
.scripts/deploy.sh → pulls image on production host, restarts container
```
## CalVer Format
`YYYY.M.BUILD` — year, month (no leading zero), incrementing build number within that month.
Examples: `2026.3.10`, `2026.3.11`, `2026.4.1`
## Release Script
`.scripts/release.sh` — creates a git tag and pushes it to trigger CI.
```bash
# Auto-generate next version
.scripts/release.sh
# Explicit version
.scripts/release.sh 2026.3.11
# Skip confirmation
.scripts/release.sh -y
# Both
.scripts/release.sh 2026.3.11 -y
```
### What it does
1. Verifies you're on `main` and in sync with origin
2. Auto-generates next CalVer build number from existing tags (or uses the one you passed)
3. Validates version format and checks for duplicate tags
4. Shows commits since last tag for review
5. Confirms, then tags and pushes
### Reference implementation
```bash
#!/usr/bin/env bash
set -euo pipefail
SKIP_CONFIRM=false
VERSION=""
for arg in "$@"; do
case "$arg" in
-y) SKIP_CONFIRM=true ;;
*) VERSION="$arg" ;;
esac
done
# Ensure we're on main and up to date
BRANCH=$(git rev-parse --abbrev-ref HEAD)
if [[ "$BRANCH" != "main" ]]; then
echo "ERROR: Must be on main branch (currently on ${BRANCH})"
exit 1
fi
git fetch origin main --tags --quiet
LOCAL=$(git rev-parse HEAD)
REMOTE=$(git rev-parse origin/main)
if [[ "$LOCAL" != "$REMOTE" ]]; then
echo "ERROR: Local main is not up to date with origin. Run: git pull"
exit 1
fi
# Determine version
YEAR=$(date +%Y)
MONTH=$(date +%-m)
if [[ -z "$VERSION" ]]; then
LAST_BUILD=$(git tag --list "${YEAR}.${MONTH}.*" --sort=-v:refname | head -1 | awk -F. '{print $3}')
NEXT_BUILD=$(( ${LAST_BUILD:-0} + 1 ))
VERSION="${YEAR}.${MONTH}.${NEXT_BUILD}"
fi
# Validate
if [[ ! "$VERSION" =~ ^20[0-9]{2}\.[0-9]+\.[0-9]+$ ]]; then
echo "ERROR: Invalid version format '${VERSION}'. Expected YYYY.M.BUILD"
exit 1
fi
if git rev-parse "refs/tags/${VERSION}" &>/dev/null; then
echo "ERROR: Tag ${VERSION} already exists"
exit 1
fi
# Show what's being released
LAST_TAG=$(git tag --sort=-v:refname | head -1)
echo "Version: ${VERSION}"
echo "Previous: ${LAST_TAG:-none}"
echo "Commit: $(git log -1 --format='%h %s')"
if [[ -n "$LAST_TAG" ]]; then
echo "Changes since ${LAST_TAG}:"
git log "${LAST_TAG}..HEAD" --oneline --no-merges
fi
# Confirm
if [[ "$SKIP_CONFIRM" != true ]]; then
read -rp "Create tag ${VERSION} and trigger release? [y/N] " answer
[[ "$answer" =~ ^[Yy]$ ]] || { echo "Aborted."; exit 0; }
fi
# Tag and push
git tag "$VERSION"
git push origin tag "$VERSION"
echo "==> Tag ${VERSION} pushed. CI will build the image."
echo "Deploy with: .scripts/deploy.sh"
```
## CI Workflow (Gitea Actions)
`.gitea/workflows/docker-build.yml` — triggered by tag push, builds and pushes Docker image.
```yaml
name: Build Docker Image
on:
push:
tags:
- '20*' # matches CalVer tags like 2026.3.11
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Extract version from tag
id: version
run: |
VERSION=${GITHUB_REF#refs/tags/}
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
myorg/myapp:${{ steps.version.outputs.version }}
myorg/myapp:production
cache-from: type=registry,ref=myorg/myapp:buildcache
cache-to: type=registry,ref=myorg/myapp:buildcache,mode=max
```
### Docker image tags
| Tag | Type | Purpose |
|-----|------|---------|
| `2026.3.10` | Immutable | Pinpoints exact version, rollback target |
| `production` | Floating | Always the latest release, used in docker-compose |
## Deploy Script
`.scripts/deploy.sh` — pulls the latest `production` image and restarts the container.
```bash
# Interactive (confirms before deploying)
.scripts/deploy.sh
# Non-interactive
.scripts/deploy.sh -y
```
### What it does
1. Shows current branch, commit, and target
2. Saves previous image digest (for rollback)
3. Pulls latest image via `docker compose pull`
4. Restarts container via `docker compose up -d`
5. Waits 5 seconds, shows container status and logs
6. Prints rollback command if image changed
### Key details
- Uses SSH alias (`ssh akamai`) — never hardcode `ssh -i` paths
- Image tag is `production` (floating) — compose always pulls the latest release
- Rollback uses the saved digest, not a version tag, so it's exact
### Production docker-compose
```yaml
services:
myapp:
image: myorg/myapp:production
restart: unless-stopped
volumes:
- ./credentials.json:/app/credentials.json:ro # secrets read-only
- ./storage:/app/storage:rw # state files writable
- ./logs:/app/logs
```
## Hotfix Workflow
When production is broken and you need to fix fast:
1. Create a `hotfix/` branch from `main`
2. Fix, test, push, open PR
3. Merge PR to `main`
4. Delete the current tag and re-release on the new HEAD:
```bash
git tag -d YYYY.M.BUILD
git push origin :refs/tags/YYYY.M.BUILD
.scripts/release.sh YYYY.M.BUILD -y
# wait for CI...
.scripts/deploy.sh -y
```
Re-using the same version number is fine for hotfixes — it keeps the version meaningful ("this is what's deployed") rather than burning a new number for a 1-line fix.
## Why This Pattern
| Alternative | Downside |
|-------------|----------|
| Branch-push trigger | Every merge = build = deploy pressure. Can't batch changes. |
| `next-release` staging branch | Extra ceremony, merge conflicts, easy to forget to promote |
| `workflow_dispatch` (manual UI button) | Less scriptable, no git tag trail |
| GitHub Releases UI | Heavier than needed for Docker-only deploys |
Tag-triggered releases give you:
- Clear audit trail (`git tag` history)
- Easy rollbacks (`docker pull myorg/myapp:2026.3.9`)
- Scriptable (`release.sh` + `deploy.sh`)
- Decoupled merge and release cycles

View File

@ -0,0 +1,101 @@
---
title: Major Domo v2 Release — 2026.3.20
description: "Performance release: parallelized API calls, caching improvements, CI overhaul to tag-triggered releases, and async hotfix."
type: reference
domain: major-domo
tags: [discord, major-domo, deployment, release-notes, docker, ci]
---
# Major Domo v2 Release — 2026.3.20
**Date:** 2026-03-20
**Tag:** `2026.3.10`
**Image:** `manticorum67/major-domo-discordapp:production`
**Server:** akamai (`/root/container-data/major-domo`)
**Deploy method:** `.scripts/release.sh` → CI → `.scripts/deploy.sh`
## Release Summary
Performance-focused release with 12 merged PRs covering parallelized API calls, caching improvements, CI workflow overhaul, and a production hotfix. Also retired the `next-release` staging branch in favor of direct-to-main merges with tag-triggered releases.
## Hotfix During Release
**PR #117** — ScorecardTracker async mismatch. PR #106 added `await` to all `scorecard_tracker` method calls across `scorebug.py`, `live_scorebug_tracker.py`, and `cleanup_service.py`, but the tracker methods themselves were still synchronous. This caused `TypeError: object NoneType can't be used in 'await' expression` on `/scorebug` and `TypeError: object list can't be used in 'await' expression` in the background scorebug update loop. Fixed by making all 6 public `ScorecardTracker` methods async and adding 5 missing `await`s in `cleanup_service.py`.
**Root cause:** PR #106 was created by an issue-worker agent that modified callers without modifying the tracker class. The async tracker conversion existed only in uncommitted working tree changes that were never included in any PR.
**Lesson:** Issue-worker agent PRs that add `await` to calls must verify the called methods are actually async — not just that the callers compile.
## Infrastructure Changes
### CI: Tag-triggered releases (PRs #110, #113)
Replaced branch-push CI with tag-push CI. Merging to `main` no longer triggers a Docker build.
- **Before:** Push to `main` or `next-release` → auto-build → auto-tag CalVer
- **After:** Push CalVer tag (`git tag 2026.3.11 && git push --tags`) → build → Docker image tagged `:version` + `:production`
Also removed the `pull_request` trigger that was building Docker images on every PR branch push.
### Release and deploy scripts (PRs #114, #115)
- `.scripts/release.sh` — auto-generates next CalVer tag, shows changelog, confirms, pushes tag
- `.scripts/deploy.sh` — updated to use SSH alias (`ssh akamai`) and `:production` image tag
### Docker volume split (PR #86)
Split the single `./storage:/app/data` volume into:
- `./storage/major-domo-service-creds.json:/app/data/major-domo-service-creds.json:ro` (credentials)
- `./storage:/app/storage:rw` (state files)
Production compose on akamai was updated manually before deploy. All 5 tracker default paths changed from `data/` to `storage/`.
### Retired `next-release` branch
All references to `next-release` removed from CLAUDE.md and CI workflow. New workflow: branch from `main` → PR to `main` → tag to release.
## Performance Changes
### Parallelized API calls
| PR | What | Impact |
|----|------|--------|
| #88 | `schedule_service`: `get_team_schedule`, `get_recent_games`, `get_upcoming_games` use `asyncio.gather()` | Up to 18 sequential HTTP requests → concurrent |
| #90 | Team lookups in `/publish-scorecard`, `/scorebug`, `/injury`, trade validation | 2 sequential calls → concurrent per location |
| #102 | `asyncio.gather()` across multiple command files | Broad latency reduction |
### Caching
| PR | What | Impact |
|----|------|--------|
| #99 | Cache user team lookup in `player_autocomplete` with 60s TTL, reduce Discord limit to 25 | Faster autocomplete on repeat use |
| #98 | Replace Redis `KEYS` with `SCAN` for cache invalidation | Non-blocking invalidation |
### Micro-optimizations
| PR | What | Impact |
|----|------|--------|
| #93 | Use `channel.purge()` instead of per-message `message.delete()` loops | 1 API call vs up to 100 per channel clear |
| #96 | Replace `json.dumps(value)` probe with `isinstance()` in JSON logger | Eliminates full serialization on every log call |
| #97 | Cache `inspect.signature()` at decoration time in all 3 decorators | Introspection cost paid once, not per-call |
## Cleanup
| PR | What |
|----|------|
| #104 | Remove dead `@self.tree.interaction_check` decorator block and duplicate `self.maintenance_mode` assignment in `bot.py` |
| #103 | Remove unused `weeks_ahead` parameter from `get_upcoming_games` |
## Test Coverage
- 16 new tests for `schedule_service` (`test_services_schedule.py` — first coverage for this service)
- Tests use existing `GameFactory`/`TeamFactory` from `tests/factories.py`
- 2 existing scorebug tests updated for async tracker methods
- Full suite: 967+ tests passing
## Deployment Notes
- Production compose updated on akamai before deploy (volume split)
- Image tag changed from `:latest` to `:production`
- Deployed twice: once before hotfix (broken `/scorebug`), once after (#117)
- Final deploy confirmed healthy — all background tasks started, gateway connected

View File

@ -90,12 +90,9 @@ All defined in `~/.claude.json` under `mcpServers`:
Hooks are configured in `~/.claude/settings.json` under the `hooks` key. They run shell commands or HTTP requests in response to events.
### Current Hooks
Current hooks are defined in `~/.claude/settings.json` — check the `hooks` key for the live list.
| Event | Action |
|-------|--------|
| `PostToolUse` (Edit/Write/MultiEdit) | Auto-format code via `format-code.sh` |
| `SubagentStop` | Notify via `notify-subagent-done.sh` |
Additionally, the `format-on-save@agent-toolkit` plugin registers its own `PostToolUse` hook for auto-formatting files on Edit/Write (runs ruff for Python, prettier for JS/TS, shfmt for shell, etc). See the plugin source at `~/.claude/plugins/cache/agent-toolkit/format-on-save/` for the full formatter list.
## Permissions
@ -103,5 +100,44 @@ Permission rules live in `~/.claude/settings.json` under `permissions.allow` and
Common patterns:
- `"mcp__gitea-mcp__*"` — allow all gitea MCP tools
- `"WebFetch(domain:docs.example.com)"` — allow fetching from specific domain
- `"Bash(ssh:*)"` — allow SSH commands
- `"WebFetch(domain:*)"` — allow fetching from any domain
- `"Bash"` — allow all Bash commands (subject to cmd-gate hook below)
## permission-manager Plugin (cmd-gate)
The `permission-manager@agent-toolkit` plugin adds a `PreToolUse` hook on all `Bash` tool calls. It parses commands into an AST via `shfmt --tojson`, classifies each segment, and returns allow/ask/deny.
### How it works
1. Compound commands (`&&`, `||`, pipes) are split into segments
2. Each segment is checked against **custom allow patterns** first (bypasses classifiers)
3. Then dispatched to language/tool-specific classifiers (git, docker, npm, etc.)
4. Most restrictive result wins across all segments
### Custom allow patterns
Loaded from two files (both checked, merged):
- **Global:** `~/.claude/command-permissions.json`
- **Project-level:** `.claude/command-permissions.json` (relative to cwd)
Format:
```json
{
"allow": [
"git pull*",
"git push origin main"
]
}
```
Patterns are **bash glob matched** per-segment against the command string. Project-level patterns are resolved from `$PWD` at hook time (i.e., the project Claude Code is working in, not the target of a `cd` in the command).
### Protected branches
The git classifier denies `git push` to `main` or `master` by default. To allow pushes to main from a specific project, add `"git push origin main"` to that project's `.claude/command-permissions.json`.
### Bypass
- Scheduled tasks use `--permission-mode bypassPermissions` which skips cmd-gate entirely
- For classifier development details, see `development/permission-manager-classifier-development.md`
- Plugin source: `~/.claude/plugins/cache/agent-toolkit/permission-manager/`