with one click
gh-prerelease
// Cut a GitHub prerelease off a specific commit — branch, bump version with `just update-version`, tag, push, and publish a prerelease
// Cut a GitHub prerelease off a specific commit — branch, bump version with `just update-version`, tag, push, and publish a prerelease
Use when building, editing, validating, or debugging generic Tracecat automations through Tracecat MCP, including workflow DSL/YAML authoring, table design, unique indexes, run-python Tracecat imports, agent presets, ai.agent or ai.preset_agent workflows, executions, validation, and workflow best practices.
Use when evaluating Tracecat MCP prompts, automation-authoring instructions, or agent behavior with the local Tracecat prompt eval harness, including static MCP prompt checks, live local workflow-authoring evals, and Codex vs Claude Code performance comparisons.
Use when building, editing, validating, or debugging Tracecat Slack bots and Slack-facing automations through Tracecat MCP, including Slack app mentions, interactive messages, event subscriptions, webhooks, thread replies, Slack tools, ai.agent or ai.preset_agent bots, Slack tone, and Slack smoke tests.
Use when adding or updating documentation pages in an existing docs site. Covers matching nearby docs tone and structure, planning navigation and page content, running product services and docs previews, capturing supporting UI screenshots with Chrome DevTools, suppressing Next.js floating dev indicators before screenshots, taking full-height Mintlify docs screenshots for PR descriptions, keeping PR-only artifacts out of committed files, and creating or updating GitHub PRs with verified documentation labels and screenshot links.
| name | gh-prerelease |
| description | Cut a GitHub prerelease off a specific commit — branch, bump version with `just update-version`, tag, push, and publish a prerelease |
| disable-model-invocation | true |
| argument-hint | [<tag>] [<commit>] |
Cut a GitHub prerelease for Tracecat off a specific commit. Mirrors .github/workflows/create-release.yml + publish-release.yml, but tags an arbitrary commit (not just main's HEAD) and publishes a GitHub prerelease directly instead of going through the draft-release flow.
The long-lived release/<tag> branch is left in place so further hotfix commits can be cherry-picked onto it later.
Full argument string: $ARGUMENTS.
Split $ARGUMENTS on whitespace into tokens:
<tag> (optional). Semver-style version with a prerelease suffix, e.g. 0.20.0-rc.1, 1.0.0-beta.48-rc.5. Do not include a leading v — tags in this repo are bare versions.<commit> (optional, default HEAD). Anything git rev-parse accepts: a SHA, branch, HEAD, HEAD~3, origin/main, etc.Do not rely on $1, $2 — only $ARGUMENTS is reliably substituted in skill markdown.
Keep <tag> in Tracecat's public release/image tag convention. just update-version <tag> writes that value to __version__, and writes a separate PEP 440-compatible value to __pep440_version__ for Hatchling package metadata. This lets Git branches, GitHub releases, and image tags stay as 1.0.0-beta.48-rc.5 while Python builds use 1.0.0b48+rc.5.
Examples:
Public <tag> | Python package version |
|---|---|
1.0.0-alpha.1 | 1.0.0a1 |
1.0.0-beta.48 | 1.0.0b48 |
1.0.0-rc.5 | 1.0.0rc5 |
1.0.0-beta.48-rc.5 | 1.0.0b48+rc.5 |
1.0.0-dev.3 | 1.0.0.dev3 |
1.0.0-post.1 | 1.0.0.post1 |
After running just update-version <tag>, verify both fields:
PUBLIC_VERSION=$(grep -oP '__version__ = "\K[^"]+' tracecat/__init__.py)
PYTHON_VERSION=$(grep -oP '__pep440_version__ = "\K[^"]+' tracecat/__init__.py)
uv run python - "$PYTHON_VERSION" <<'PY'
import sys
from packaging.version import Version
Version(sys.argv[1])
PY
If <tag> is missing, suggest the next logical RC tag by inspecting recent tags:
git fetch --tags --prune
LATEST_RC=$(git tag --sort=-v:refname | rg -m1 -- '-rc\.[0-9]+$' || true)
LATEST_RC matches <base>-rc.<N>, the suggested tag is <base>-rc.<N+1>. For example, 1.0.0-beta.48-rc.4 → 1.0.0-beta.48-rc.5.
BASE="${LATEST_RC%-rc.*}"
N="${LATEST_RC##*-rc.}"
SUGGESTED="${BASE}-rc.$((N + 1))"
-rc.<N> tag exists, stop and ask — there is no unambiguous "next" without a precedent.Present the suggestion in one line and wait for explicit confirmation (y to accept, or have the user supply an alternative). Do not proceed silently. If the user accepts, use the suggestion as <tag> for the rest of the workflow.
Validate <tag> against ^[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z]+\.[0-9]+){1,2}$. If it is a stable release (e.g. plain 0.20.0), refuse and point the user at .github/workflows/create-release.yml — this skill is for prereleases only.
Stop and report on any failure.
git rev-parse --is-inside-work-tree
git fetch --all --tags --prune
Resolve the commit:
COMMIT_SHA=$(git rev-parse --verify "<commit>^{commit}")
git log -1 --format='%h %s' "$COMMIT_SHA"
Verify the tag does not already exist anywhere:
git rev-parse --verify "refs/tags/<tag>" 2>/dev/null # must fail
git ls-remote --tags origin "refs/tags/<tag>" # must print nothing
gh release view "<tag>" --json tagName 2>/dev/null # must fail
If any of these succeed, stop. Ask the user whether to pick a different tag or remove the existing one first.
Verify the release branch does not already exist:
git rev-parse --verify "refs/heads/release/<tag>" 2>/dev/null # must fail
git ls-remote --heads origin "release/<tag>" # must print nothing
Working-tree state. Run git status --porcelain.
just update-version will touch (tracecat/__init__.py, packages/tracecat-registry/tracecat_registry/__init__.py, CONTRIBUTING.md, .github/ISSUE_TEMPLATE/bug_report.md, plus matches under docker-compose*.yml, docs/**, deployments/**).In one message, present:
<short-sha> + subject.<tag>.release/<tag>.<tag> and <commit> substituted).build-push-images.yml (matches tags: '*.*.*' in its push trigger).Stop and wait for y.
git switch --create "release/<tag>" "$COMMIT_SHA"
Match .github/workflows/create-release.yml:
yes | just update-version <tag>
update-version.sh prompts before overwriting files; yes | answers y.
Important: pass the public release tag, e.g. 1.0.0-beta.48-rc.5. update-version.sh keeps that value in __version__ and writes the PEP 440 equivalent, e.g. 1.0.0b48+rc.5, to __pep440_version__ for Python package builds.
Sanity-check the result:
git diff --quiet && { echo "update-version produced no changes" >&2; exit 1; }
NEW_VERSION=$(grep -oP '__version__ = "\K[^"]+' tracecat/__init__.py)
[ "$NEW_VERSION" = "<tag>" ] || { echo "version mismatch: got $NEW_VERSION, expected <tag>" >&2; exit 1; }
PYTHON_VERSION=$(grep -oP '__pep440_version__ = "\K[^"]+' tracecat/__init__.py)
REGISTRY_PYTHON_VERSION=$(grep -oP '__pep440_version__ = "\K[^"]+' packages/tracecat-registry/tracecat_registry/__init__.py)
[ "$PYTHON_VERSION" = "$REGISTRY_PYTHON_VERSION" ] || { echo "Python version mismatch: tracecat=$PYTHON_VERSION registry=$REGISTRY_PYTHON_VERSION" >&2; exit 1; }
uv run python - "$PYTHON_VERSION" <<'PY'
import sys
from packaging.version import Version
Version(sys.argv[1])
PY
If it fails: git restore . (after confirming with the user) and abort.
Stage only the files update-version modified, individually. Do not use git add -A or git add ..
git status --porcelain
# Then for each modified path shown:
git add -- <path>
Then:
git commit -m "release: <tag>"
(Matches the release: ${VERSION} message used by create-release.yml.)
git push -u origin "release/<tag>"
The annotated tag points to the version-bump commit (the new HEAD), mirroring publish-release.yml tagging the merge commit.
git tag -a "<tag>" -m "Release <tag>"
git push origin "refs/tags/<tag>"
This push event for a *.*.* tag triggers .github/workflows/build-push-images.yml, which builds and publishes ghcr.io/tracecathq/tracecat:<tag> and ghcr.io/tracecathq/tracecat-ui:<tag>. (Because the prerelease tag has a -suffix, neither image will be retagged :latest — that branch in the workflow is guarded on !startsWith(..., 'nightly-') and non-prerelease semver via the matrix tags.)
Build release notes that (a) only cover changes since the previous published release or prerelease and (b) are grouped into the same categories the release-drafter GitHub Action uses on main. Then publish.
PREV_TAG=$(gh release list --exclude-drafts --limit 1 --json tagName --jq '.[0].tagName')
gh release list orders by created-at desc and includes prereleases, so this picks the most recent published release of any kind. If empty, fall back to the most recent reachable tag:
PREV_TAG=${PREV_TAG:-$(git describe --tags --abbrev=0 "${COMMIT_SHA}^")}
Stop and ask if neither resolves.
REPO=$(gh repo view --json nameWithOwner --jq .nameWithOwner)
RAW_NOTES=$(gh api "repos/$REPO/releases/generate-notes" \
--method POST \
-f tag_name="<tag>" \
-f previous_tag_name="$PREV_TAG" \
-f target_commitish="$COMMIT_SHA" \
--jq .body)
This is the same auto-generated body gh release create --generate-notes would produce, but with previous_tag_name pinned so the diff window is exactly PREV_TAG..<tag> (prereleases included as PREV) instead of "latest stable release".
PR_NUMS=$(printf '%s\n' "$RAW_NOTES" | grep -oE 'pull/[0-9]+' | grep -oE '[0-9]+' | sort -un)
For each PR number, fetch metadata once:
gh pr view "$N" --json number,title,labels,author,url
.github/release-drafter.ymlDrop any PR carrying an exclude-labels value (skip changelog, release).
For the rest, bucket each PR into the first matching category in this order. The list mirrors .github/release-drafter.yml exactly — if that file changes, update this list:
| # | Category title | Labels that match |
|---|---|---|
| 1 | Breaking changes | breaking, breaking ui, breaking frontend, breaking engine, breaking app, breaking infra |
| 2 | Deprecations | deprecation |
| 3 | Security | security |
| 4 | Playbooks | playbook |
| 5 | Integrations | integrations |
| 6 | Agents | agents |
| 7 | Performance improvements | performance |
| 8 | Enhancements | enhancement |
| 9 | Bug fixes | fix |
| 10 | Infrastructure | infra |
| 11 | Documentation | documentation |
| 12 | Dependencies | dependencies |
| 13 | Build system | build |
| 14 | Other improvements | internal |
Anything left with no matching label goes under a trailing Other section. Do not silently drop PRs.
Format each entry as - <cleaned-title> (#<number>) (matches release-drafter's change-template). Strip conventional-commit prefixes from the title using the same replacer regex the config uses:
^(build|chore|ci|depr|deps|docs|feat|fix|helm|infra|perf|refactor|release|revert|security|style|test)(\(.*\))?(\!)?:\s
## <Category title>
- <title> (#<number>)
- ...
Only emit a category header if it has at least one entry. End the body with:
**Full changelog**: https://github.com/<owner>/<repo>/compare/<PREV_TAG>...<tag>
Write the body to a temp file and create the release with --notes-file (not --generate-notes):
BODY_FILE=$(mktemp)
# write categorized markdown to "$BODY_FILE"
gh release create "<tag>" \
--target "release/<tag>" \
--prerelease \
--title "Tracecat <tag>" \
--notes-file "$BODY_FILE"
rm -f "$BODY_FILE"
If there are zero PRs between PREV_TAG and <tag> (rare — usually means you tagged the same commit), publish with a single-line body: No changes since \<PREV_TAG>`.`
Print:
release/<tag> (pushed, long-lived — do not delete).<tag> → <short-sha> of the version-bump commit.gh release view <tag> --json url --jq .url.gh run list --workflow build-push-images.yml --branch <tag> --limit 1 (the workflow run shows up under the tag ref).<tag> is a stable release (no prerelease suffix). Stable releases go through create-release.yml → PR → publish-release.yml.y at step 2. Never push branches or tags before confirmation.--force.git add -A or git add .. Stage paths individually.--no-gpg-sign) or hooks (--no-verify). If signing fails, stop and ask the user to fix it.release/<previous-tag> branch as the <commit> argument so the version-bump history stays linear.