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
All checks were successful
Reindex Knowledge Base / reindex (push) Successful in 3s
This commit is contained in:
parent
730f100619
commit
b1fed02219
254
development/tag-triggered-release-deploy.md
Normal file
254
development/tag-triggered-release-deploy.md
Normal 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
|
||||
101
major-domo/release-2026.3.20.md
Normal file
101
major-domo/release-2026.3.20.md
Normal 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
|
||||
@ -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/`
|
||||
|
||||
Loading…
Reference in New Issue
Block a user