For AI Agents: This document serves as both DEVGUIDE.md and AGENTS.md (symlinked).
- Overview
- Architecture
- Core Workflow
- Caching Strategy
- Error Handling
- Release Process
- Deploying the Web Service
- Agent Guidelines
TagBot automatically creates Git tags and GitHub releases for Julia packages when they are registered in a Julia registry (typically General).
Flow: Package registered → TagBot queries registry for tree-sha1 → finds matching commit → creates tag + release.
TagBot has three components:
| Component | Location | Purpose |
|---|---|---|
| GitHub Action | tagbot/action/ |
Main functionality, runs in Docker |
| Web Service | tagbot/web/ |
Error reporting API on AWS Lambda (julia-tagbot.com) |
| Local CLI | tagbot/local/ |
Manual usage outside GitHub Actions |
tagbot/
├── __init__.py # LogFormatter, logger
├── action/
│ ├── __init__.py # TAGBOT_WEB, Abort, InvalidProject exceptions
│ ├── __main__.py # GitHub Action entrypoint
│ ├── changelog.py # Release notes generation (Jinja2)
│ ├── git.py # Git command wrapper
│ ├── gitlab.py # GitLab API wrapper (optional)
│ └── repo.py # Core logic: version discovery, release creation
├── local/
│ └── __main__.py # CLI entrypoint
└── web/
├── __init__.py # Flask app, /report endpoint
├── reports.py # Error report processing
└── templates/ # HTML templates
- Parse workflow inputs (token, registry, ssh, gpg, changelog config)
- Create
Repoinstance - Check if package is registered → exit if not
- Call
new_versions()to find versions needing tags - If
dispatch=true, create dispatch event and wait - Configure SSH/GPG keys if provided
- Determine which version gets "latest" badge
- For each version: optionally handle release branch, then
create_release() - Handle errors, create manual intervention issue if needed
_versions()parsesRegistry/Package/Versions.toml→{version: tree_sha}_filter_map_versions()for each version:- Skip if tag already exists (uses tags cache)
- Find commit SHA for tree-sha1:
- Primary:
git log --all --format="%H %T"cache lookup (O(1)) - Fallback: Search merged registry PRs for commit
- Primary:
- Returns
{tag_name: commit_sha}for versions needing tags
Repo (repo.py) - Core logic:
- Registry interaction:
_registry_path,_versions(),_registry_pr() - Commit resolution:
_commit_sha_of_tree(),_build_tree_to_commit_cache() - Release creation:
create_release(),configure_ssh(),configure_gpg() - Error handling:
handle_error(),create_issue_for_manual_tag()
Git (git.py) - Git operations:
- Clones repo to temp directory on first access
- Uses oauth2 token in clone URL
- Sanitizes output to hide tokens
- Methods:
command(),create_tag(),set_remote_url(),fetch_branch(), etc.
Changelog (changelog.py) - Release notes:
- Finds previous release by SemVer
- Collects issues/PRs closed in time range
- Extracts custom notes from registry PR (
<!-- BEGIN RELEASE NOTES -->) - Renders Jinja2 template
Subpackages: For monorepos with subdir input:
- Tag format:
SubPkgA-v1.0.0 - Tree SHA is for subdir, not root
Private registries: With registry_ssh input:
- Clones registry via SSH instead of API
_registry_pr()returns None
GitLab support: gitlab.py wraps python-gitlab to match PyGithub interface.
SSH keys: Used when GITHUB_TOKEN can't push (protected branches, workflow files).
GPG signing: Optional tag signing via configure_gpg().
Performance: 600+ versions in ~4 seconds via aggressive caching.
| Cache | Purpose | Built By |
|---|---|---|
__existing_tags_cache |
Skip existing tags | Single API call to get_git_matching_refs("tags/") |
__tree_to_commit_cache |
Tree SHA → commit | Single git log --all --format=%H %T |
__registry_prs_cache |
Fallback commit lookup | Fetch up to 300 merged PRs |
__commit_datetimes |
"Latest" determination | Lazily built |
Pattern for new caches:
def __init__(self, ...):
self.__cache: Optional[Dict[str, str]] = None
def _build_cache(self) -> Dict[str, str]:
if self.__cache is not None:
return self.__cache
# Build cache...
self.__cache = result
return result-
repo.handle_error()classifies exceptions:Abort: Expected, log onlyRequestException: Transient, allow retryGithubException: Check status (5xx/403 = transient, else report)
-
Reportable errors POST to
julia-tagbot.com/report -
Web service (
reports.handler()):- Deduplicates by Levenshtein distance on stacktrace
- Creates/updates issues in TagBotErrorReports
-
Manual intervention: When auto-tag fails (workflow file changes, etc.), creates issue with ready-to-run commands.
- Merge PRs to master
- Go to publish.yml workflow
- Run with desired version bump (major/minor/patch)
- Review and merge the created PR
- CI builds and pushes Docker image to
ghcr.io/juliaregistries/tagbot:{version}
The web service runs on AWS Lambda via Serverless Framework.
- Node.js and npm
- AWS credentials with deployment permissions
- Docker (for building Linux-compatible packages on macOS)
npm install
aws configure --profile tagbot # region: us-east-1# Production (with custom domain julia-tagbot.com)
GITHUB_TOKEN="ghp_..." npx serverless deploy --stage prod --aws-profile tagbot
# Dev (no custom domain)
npx serverless deploy --stage dev --aws-profile tagbot| File | Purpose |
|---|---|
serverless.yml |
Lambda functions, AWS config |
requirements.txt |
Python deps for Lambda (keep in sync with pyproject.toml) |
package.json |
Serverless plugins |
Environment variables (in serverless.yml):
GITHUB_TOKEN- Access to TagBotErrorReports repoTAGBOT_REPO- Main repo (default: JuliaRegistries/TagBot)TAGBOT_ISSUES_REPO- Error reports repo (default: JuliaRegistries/TagBotErrorReports)
Missing Python modules: Check requirements.txt, ensure serverless-python-requirements installed, try rm -rf .requirements .serverless
Broken symlinks: find . -maxdepth 1 -type l ! -name "AGENTS.md" -delete
# Recent API function logs (last 5 min)
aws logs filter-log-events --profile tagbot --region us-east-1 \
--log-group-name /aws/lambda/TagBotWeb-prod-api \
--start-time $(($(date +%s) * 1000 - 300000)) \
--query 'events[*].message' --output text
# Reports function logs
aws logs filter-log-events --profile tagbot --region us-east-1 \
--log-group-name /aws/lambda/TagBotWeb-prod-reports \
--start-time $(($(date +%s) * 1000 - 300000)) \
--query 'events[*].message' --output textOr view in AWS Console.
| Item | Value |
|---|---|
| Language | Python 3.12+ (Docker uses 3.14, Lambda uses 3.11) |
| Formatter | black |
| Linter | flake8 |
| Type Checker | mypy (stubs in stubs/) |
| Tests | pytest |
| Package Manager | pip (pyproject.toml) |
Style:
blackformatting, 88 char lines- Type hints on all functions
- Prefer
Optional[X]overX | None
Naming:
_methodfor private__cachefor cache attributesUPPER_SNAKEfor constants
Logging:
- Use
from .. import logger - Never log secrets (use
_sanitize())
- YAGNI - Don't add features "just in case"
- KISS - Simple over clever
- DRY - Use caching for repeated operations
- SRP - Each method does one thing
- Always use caches (
_build_tags_cache(),_build_tree_to_commit_cache()) - Batch operations (single API call > per-item calls)
- Git commands > GitHub API
- Lazy loading (build caches when first needed)
# Setup
python3 -m venv .venv && source .venv/bin/activate
pip install . && pip install pytest pytest-cov black flake8 mypy boto3
# Run all checks
make test
# Individual checks
make pytest black flake8 mypyTest locations: test/action/, test/web/, test/test_tagbot.py
- ❌ Print statements (use logger)
- ❌ Catch broad
Exceptionwithout re-raising - ❌ API calls in loops without caching
- ❌ Store secrets longer than necessary
- ❌ Modify
action.ymlwithout updatingpyproject.tomlversion - ❌ Add dependencies without updating
pyproject.toml
- Understand the data flow
- Check if caching exists for your use case
- Write tests
- Run
make test - Update DEVGUIDE.md if architecture changes