name: Release
on:
# Trigger 1: Tag push (normal release flow)
push:
tags:
- 'v*'
# Trigger 2: Manual (for retroactive cleanup or special cases)
workflow_dispatch:
inputs:
version:
description: 'Version to release (e.g., 0.193.0)'
required: true
notes_only:
description: 'Only update release notes (skip artifact publishing)'
type: boolean
default: false
permissions:
contents: write
packages: write
id-token: write # Required for MCP Registry OIDC authentication
jobs:
# =============================================================================
# PREPARE: Extract version, build changelog, setup for parallel builds
# =============================================================================
prepare:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.config.outputs.version }}
notes_only: ${{ steps.config.outputs.notes_only }}
has_notes: ${{ steps.notes.outputs.has_notes }}
steps:
- name: Checkout code
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
submodules: true
- name: Determine version and mode
id: config
run: |
if [[ "${{ github.event_name }}" == "push" ]]; then
# Tag trigger: extract version from tag, full release
VERSION="${GITHUB_REF#refs/tags/v}"
NOTES_ONLY="false"
else
# Manual trigger: use inputs
VERSION="${{ inputs.version }}"
NOTES_ONLY="${{ inputs.notes_only }}"
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "notes_only=$NOTES_ONLY" >> $GITHUB_OUTPUT
echo "Release version: $VERSION"
echo "Notes only mode: $NOTES_ONLY"
- name: Setup git config
run: |
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
- name: Switch to latest main
run: |
git checkout main
git pull origin main
- name: Setup Python (for towncrier)
uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5
with:
python-version: '3.x'
- name: Install towncrier
run: pip install towncrier
- name: Build changelog with towncrier
id: towncrier
run: |
VERSION=${{ steps.config.outputs.version }}
# Check if there are any changelog fragments
FRAGMENT_COUNT=$(find changelog.d -name "*.md" ! -name ".gitkeep" 2>/dev/null | wc -l)
if [ "$FRAGMENT_COUNT" -gt 0 ]; then
echo "Found $FRAGMENT_COUNT changelog fragments"
towncrier build --version $VERSION --yes
echo "has_fragments=true" >> $GITHUB_OUTPUT
else
echo "No changelog fragments found, skipping towncrier build"
echo "has_fragments=false" >> $GITHUB_OUTPUT
fi
- name: Commit and push changelog changes
if: steps.towncrier.outputs.has_fragments == 'true'
run: |
VERSION=${{ steps.config.outputs.version }}
git add docs/CHANGELOG.md changelog.d/
git commit -m "docs: update changelog for v$VERSION [skip ci]"
git push origin HEAD:main
- name: Extract release notes from CHANGELOG
id: notes
run: |
VERSION=${{ steps.config.outputs.version }}
# Extract the section for this version from CHANGELOG.md
# Look for the version header and extract until the next version header or end
if [ -f "docs/CHANGELOG.md" ]; then
# Extract content between this version's header and the next header (or end)
NOTES=$(awk "/^## \[$VERSION\]/{flag=1; next} /^## \[/{flag=0} flag" docs/CHANGELOG.md)
if [ -n "$NOTES" ]; then
echo "Extracted release notes for version $VERSION"
# Save to file for later use
echo "$NOTES" > release_notes_content.md
echo "has_notes=true" >> $GITHUB_OUTPUT
else
echo "No release notes found for version $VERSION in CHANGELOG"
echo "has_notes=false" >> $GITHUB_OUTPUT
fi
else
echo "docs/CHANGELOG.md not found"
echo "has_notes=false" >> $GITHUB_OUTPUT
fi
- name: Upload release notes artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: release-notes
path: release_notes_content.md
if-no-files-found: ignore
# =============================================================================
# BUILD DOT-AI: npm publish + main Docker image (runs in parallel with plugin)
# =============================================================================
build-dot-ai:
needs: prepare
if: needs.prepare.outputs.notes_only != 'true'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
with:
fetch-depth: 0
submodules: true
- name: Setup Node.js
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
node-version: '22.x'
registry-url: 'https://registry.npmjs.org'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build project
run: npm run build
- name: Update package.json version
run: |
VERSION=${{ needs.prepare.outputs.version }}
CURRENT_VERSION=$(node -p "require('./package.json').version")
if [ "$CURRENT_VERSION" != "$VERSION" ]; then
echo "Updating package.json from $CURRENT_VERSION to $VERSION"
npm version $VERSION --no-git-tag-version
else
echo "Package.json already at correct version: $VERSION"
fi
- name: Update server.json version
run: |
VERSION=${{ needs.prepare.outputs.version }}
if [ ! -f "server.json" ]; then
echo "ERROR: server.json file not found!"
echo "The server.json file is required for MCP registry publication."
exit 1
fi
echo "Updating server.json to version $VERSION"
sed -i "s/\"version\": \"[^\"]*\"/\"version\": \"$VERSION\"/g" server.json
echo "Updated server.json:"
cat server.json
- name: Publish to npm
run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Create npm package tarball for Docker build
run: npm pack
- name: Set up QEMU
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GHCR_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25 # v5
with:
context: .
push: true
build-args: |
PACKAGE_VERSION=${{ needs.prepare.outputs.version }}
tags: |
ghcr.io/vfarcic/dot-ai:${{ needs.prepare.outputs.version }}
ghcr.io/vfarcic/dot-ai:latest
labels: |
org.opencontainers.image.source=https://github.com/vfarcic/dot-ai
org.opencontainers.image.description=AI-powered development productivity platform that enhances software development workflows through intelligent automation and AI-driven assistance
org.opencontainers.image.licenses=MIT
org.opencontainers.image.version=${{ needs.prepare.outputs.version }}
platforms: linux/amd64,linux/arm64
# =============================================================================
# BUILD AGENTIC-TOOLS: Plugin Docker image (runs in parallel with main)
# =============================================================================
build-agentic-tools:
needs: prepare
if: needs.prepare.outputs.notes_only != 'true'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
with:
submodules: true
- name: Set up QEMU
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GHCR_TOKEN }}
- name: Build and push agentic-tools plugin image
uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25 # v5
with:
context: packages/agentic-tools
file: packages/agentic-tools/Dockerfile
push: true
tags: |
ghcr.io/vfarcic/dot-ai-agentic-tools:${{ needs.prepare.outputs.version }}
ghcr.io/vfarcic/dot-ai-agentic-tools:latest
labels: |
org.opencontainers.image.source=https://github.com/vfarcic/dot-ai
org.opencontainers.image.description=dot-ai agentic-tools plugin providing kubectl operations
org.opencontainers.image.licenses=MIT
org.opencontainers.image.version=${{ needs.prepare.outputs.version }}
platforms: linux/amd64,linux/arm64
# =============================================================================
# GENERATE OPENAPI: Generate OpenAPI spec for CLI consumption
# =============================================================================
generate-openapi:
needs: prepare
if: needs.prepare.outputs.notes_only != 'true'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
with:
submodules: true
- name: Setup Node.js
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
node-version: '22.x'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build project
run: npm run build
- name: Generate OpenAPI spec
run: npm run generate:openapi
- name: Upload OpenAPI spec artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: openapi-spec
path: schema/openapi.json
# =============================================================================
# FINALIZE: Helm chart, commit, GitHub release (after both images built)
# =============================================================================
finalize:
needs: [prepare, build-dot-ai, build-agentic-tools, generate-openapi]
if: needs.prepare.outputs.notes_only != 'true'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
submodules: true
- name: Setup git config
run: |
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
- name: Switch to latest main
run: |
git checkout main
git pull origin main
- name: Download release notes artifact
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4
with:
name: release-notes
path: .
- name: Download OpenAPI spec artifact
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4
with:
name: openapi-spec
path: schema/
- name: Install Devbox
uses: jetify-com/devbox-install-action@a0d2d53632934ae004f878c840055956d9f741b0 # v0.14.0
with:
enable-cache: true
- name: Update package.json version
run: |
VERSION=${{ needs.prepare.outputs.version }}
npm version $VERSION --no-git-tag-version
- name: Update server.json version
run: |
VERSION=${{ needs.prepare.outputs.version }}
sed -i "s/\"version\": \"[^\"]*\"/\"version\": \"$VERSION\"/g" server.json
- name: Update Chart.yaml and values.yaml versions
run: |
VERSION=${{ needs.prepare.outputs.version }}
# Update Chart.yaml versions to match the coordinated release
sed -i "s/^version: .*/version: \"$VERSION\"/" charts/Chart.yaml
sed -i "s/^appVersion: .*/appVersion: \"$VERSION\"/" charts/Chart.yaml
# Update main image tag in values.yaml
sed -i "0,/tag: \"[^\"]*\"/s/tag: \"[^\"]*\"/tag: \"$VERSION\"/" charts/values.yaml
# Update agentic-tools plugin image tag in values.yaml
# This targets the tag under the agentic-tools section
sed -i "/agentic-tools:/,/^ [a-zA-Z]/{s/tag: \"[^\"]*\"/tag: \"$VERSION\"/}" charts/values.yaml
# Update dot.nu --dot-ai-tag default value
sed -i "s/--dot-ai-tag: string = \".*\"/--dot-ai-tag: string = \"$VERSION\"/" dot.nu
echo "Updated chart to reference Docker images:"
echo " - ghcr.io/vfarcic/dot-ai:$VERSION"
echo " - ghcr.io/vfarcic/dot-ai-agentic-tools:$VERSION"
- name: Log in to GitHub Container Registry for Helm
run: |
devbox run -- helm registry login ghcr.io -u ${{ github.actor }} --password-stdin <<< "${{ secrets.GHCR_TOKEN }}"
env:
HELM_EXPERIMENTAL_OCI: 1
- name: Package and push Helm chart
run: |
VERSION=${{ needs.prepare.outputs.version }}
devbox run -- helm package charts/ --version $VERSION --app-version $VERSION
devbox run -- helm push dot-ai-$VERSION.tgz oci://ghcr.io/vfarcic/dot-ai/charts
echo "Published Helm chart: oci://ghcr.io/vfarcic/dot-ai/charts/dot-ai:$VERSION"
- name: Commit version changes
run: |
VERSION=${{ needs.prepare.outputs.version }}
# Add all version-related changes and generated artifacts
git add package.json charts/Chart.yaml charts/values.yaml server.json dot.nu schema/openapi.json
git commit -m "chore: coordinated release v$VERSION
- npm package: v$VERSION
- docker image: ghcr.io/vfarcic/dot-ai:$VERSION
- docker image: ghcr.io/vfarcic/dot-ai-agentic-tools:$VERSION
- helm chart: oci://ghcr.io/vfarcic/dot-ai/charts/dot-ai:$VERSION
- openapi spec: schema/openapi.json
[skip ci]" || echo "No changes to commit"
git push origin HEAD:main
echo "Released coordinated version: $VERSION"
- name: Create GitHub Release
run: |
VERSION=${{ needs.prepare.outputs.version }}
# Build release notes content
cat > release_body.md << 'HEADER'
## Coordinated Release Artifacts
HEADER
cat >> release_body.md << ARTIFACTS
- **npm package**: \`@vfarcic/dot-ai@$VERSION\`
- **Docker image**: \`ghcr.io/vfarcic/dot-ai:$VERSION\` (also available as \`latest\`)
- **Docker image**: \`ghcr.io/vfarcic/dot-ai-agentic-tools:$VERSION\` (plugin)
- **Helm chart**: \`oci://ghcr.io/vfarcic/dot-ai/charts/dot-ai:$VERSION\`
ARTIFACTS
# Append towncrier-generated notes if available
if [ -f "release_notes_content.md" ]; then
echo "## What's Changed" >> release_body.md
echo "" >> release_body.md
cat release_notes_content.md >> release_body.md
fi
# Create or update GitHub release
if gh release view "v$VERSION" > /dev/null 2>&1; then
echo "Updating existing release v$VERSION"
gh release edit "v$VERSION" --notes-file release_body.md
else
echo "Creating new release v$VERSION"
gh release create "v$VERSION" \
--title "Release v$VERSION" \
--notes-file release_body.md
fi
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Trigger CLI Release
uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3.0.0
with:
token: ${{ secrets.CLI_DISPATCH_TOKEN }}
repository: vfarcic/dot-ai-cli
event-type: server-release
client-payload: '{"version": "${{ needs.prepare.outputs.version }}"}'
- name: Check if docs changed since last release
id: docs-check
run: |
PREV_TAG=$(git describe --tags --abbrev=0 HEAD~1 2>/dev/null || echo "")
if [ -n "$PREV_TAG" ]; then
DOCS_CHANGED=$(git diff --name-only $PREV_TAG HEAD -- README.md docs/ | wc -l)
echo "Checking for doc changes between $PREV_TAG and HEAD"
else
DOCS_CHANGED=1
echo "No previous tag found, will trigger website rebuild"
fi
if [ "$DOCS_CHANGED" -gt 0 ]; then
echo "docs-changed=true" >> $GITHUB_OUTPUT
else
echo "docs-changed=false" >> $GITHUB_OUTPUT
fi
- name: Trigger Website Rebuild
if: steps.docs-check.outputs.docs-changed == 'true'
uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3.0.0
with:
token: ${{ secrets.WEBSITE_DISPATCH_TOKEN }}
repository: vfarcic/dot-ai-website
event-type: upstream-release
client-payload: '{"source": "dot-ai", "version": "${{ needs.prepare.outputs.version }}"}'
# =============================================================================
# FINALIZE (NOTES-ONLY): Just commit changelog changes
# =============================================================================
finalize-notes-only:
needs: prepare
if: needs.prepare.outputs.notes_only == 'true'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
submodules: true
- name: Download release notes artifact
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4
with:
name: release-notes
path: .
- name: Create GitHub Release
run: |
VERSION=${{ needs.prepare.outputs.version }}
# Build release notes content
cat > release_body.md << 'HEADER'
## Coordinated Release Artifacts
HEADER
echo "*Release notes update only - artifacts were published previously*" >> release_body.md
echo "" >> release_body.md
# Append towncrier-generated notes if available
if [ -f "release_notes_content.md" ]; then
echo "## What's Changed" >> release_body.md
echo "" >> release_body.md
cat release_notes_content.md >> release_body.md
fi
# Create or update GitHub release
if gh release view "v$VERSION" > /dev/null 2>&1; then
echo "Updating existing release v$VERSION"
gh release edit "v$VERSION" --notes-file release_body.md
else
echo "Creating new release v$VERSION"
gh release create "v$VERSION" \
--title "Release v$VERSION" \
--notes-file release_body.md
fi
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# =============================================================================
# UPDATE UMBRELLA: Update dot-ai-stack umbrella chart
# =============================================================================
update-umbrella:
needs: [prepare, finalize]
runs-on: ubuntu-latest
if: needs.prepare.outputs.notes_only != 'true'
permissions:
contents: read
steps:
- name: Checkout umbrella chart
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
with:
repository: vfarcic/dot-ai-stack
token: ${{ secrets.WEBSITE_DISPATCH_TOKEN }}
- name: Install Devbox
uses: jetify-com/devbox-install-action@a0d2d53632934ae004f878c840055956d9f741b0 # v0.14.0
with:
enable-cache: true
- name: Update dependency version
run: |
devbox run -- yq -i '(.dependencies[] | select(.name == "dot-ai")).version = "${{ needs.prepare.outputs.version }}"' Chart.yaml
echo "Updated Chart.yaml:"
cat Chart.yaml
- name: Commit and push
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add Chart.yaml
git diff --staged --quiet || git commit -m "chore: bump dot-ai to ${{ needs.prepare.outputs.version }}"
git push