Skip to content

Implement fork-based workflow and release automation skill #369

@itdove

Description

@itdove

Overview

Implement fork-based workflow for release control and create a /release skill to automate the release process. This provides technical enforcement for preventing unauthorized releases without requiring GitHub Pro.

Reference Implementation: itdove/ai-guardian#16

Why This Matters

Current Risk

  • Anyone with Write access can create release tags
  • Could trigger unauthorized PyPI/package registry releases
  • No technical prevention on free GitHub accounts
  • Human error in manual release process

Solution Benefits

  • Technical enforcement - Only maintainers can push tags (fork-based workflow)
  • Automation - /release skill reduces human error
  • Monitoring - Tag monitor detects violations
  • Audit trail - All tag creation logged
  • Free - No GitHub Pro required

Architecture

┌─────────────────────────────────┐
│ Contributors (Fork-based)       │
│ - Must fork repository          │
│ - Cannot push tags              │
│ - Submit PRs from forks         │
└─────────────────────────────────┘
              │
              ▼
   ┌──────────────────┐
   │  Pull Request    │
   └──────────────────┘
              │
              ▼
┌─────────────────────────────────┐
│ Maintainers (Direct Access)     │
│ - Review/merge PRs              │
│ - Use /release skill            │
│ - Push tags (triggers publish)  │
└─────────────────────────────────┘
              │
              ▼
   ┌──────────────────┐
   │ Tag Monitor      │◄─── Detects unauthorized
   │ (GitHub Actions) │     Creates security issues
   └──────────────────┘
              │
              ▼
   ┌──────────────────┐
   │ Publish Workflow │
   │ (PyPI/Registry)  │
   └──────────────────┘

Implementation Tasks

Phase 1: Release Automation Skill

Create ~/.claude/skills/release/ with the following files:

File 1: SKILL.md (Skill Documentation)

Click to expand SKILL.md template
---
name: release
description: Automate DevAIFlow release workflow with version management, CHANGELOG updates, and git operations
user-invocable: true
---

# DevAIFlow Release Skill

Automates the release management workflow for DevAIFlow.

## Authorization Notice

**IMPORTANT**: This repository uses a fork-based workflow.

- **Maintainers** (@itdove, @other-maintainers): Can create and push releases
- **Contributors**: Should NOT create or push production tags
  - Fork the repository instead (see CONTRIBUTING.md)
  - Submit pull requests with changes
  - Update CHANGELOG.md in your PR
  - Maintainers will handle releases

## Usage

```bash
/release minor              # Create minor version release (1.1.0 -> 1.2.0)
/release patch              # Create patch version release (1.1.0 -> 1.1.1)
/release major              # Create major version release (1.0.0 -> 2.0.0)
/release hotfix v1.1.0      # Create hotfix from v1.1.0 tag
/release test               # Create TestPyPI test release (if applicable)

Workflow

When invoked, this skill:

  1. Safety Checks: Verify prerequisites before starting
  2. Version Management: Update version in all required files
  3. CHANGELOG Management: Update CHANGELOG.md with proper format
  4. Git Operations: Create branches, commits, and tags
  5. Post-Release Guidance: Provide checklist for manual steps

Version Management

CRITICAL: DevAIFlow stores version in multiple locations that MUST stay in sync.

TODO: Document your version file locations:

  • setup.py? (version = "X.Y.Z")
  • pyproject.toml? (version = "X.Y.Z")
  • src/package/__init__.py? (version = "X.Y.Z")
  • Other locations?

Version Format:

  • Production: "1.0.0" (semantic versioning)
  • Development: "1.1.0-dev" (on main branch)
  • Test: "1.2.0-test1" (for TestPyPI)

CHANGELOG.md Format

Location: /path/to/CHANGELOG.md

Format: Keep a Changelog format (https://keepachangelog.com/)

## [Unreleased]

### Added
- New features

### Changed
- Changes to existing functionality

### Fixed
- Bug fixes

## [1.2.0] - 2026-04-08

### Added
- Feature X

[Unreleased]: https://github.com/itdove/devaiflow/compare/v1.2.0...HEAD
[1.2.0]: https://github.com/itdove/devaiflow/releases/tag/v1.2.0

Safety Checks

Before starting any release:

  1. ✅ Verify git status is clean
  2. ✅ Verify on correct branch
  3. ✅ Verify tests pass
  4. ✅ Verify CHANGELOG.md has Unreleased section
  5. ✅ Verify versions match between files

Post-Release Checklist

⚠️ MAINTAINERS ONLY

Before pushing tag:

  • Confirm you are authorized
  • Review all changes on release branch
  • Verify tests pass: pytest or equivalent
  • Verify version is correct in all files

After creating release tag:

  1. Push tag: git push origin vX.Y.Z
  2. Monitor GitHub Actions
  3. Verify package publication
  4. Test installation
  5. Merge release branch back to main
  6. Bump version to next dev cycle

</details>

#### File 2: `release_helper.py` (Python Automation)

<details>
<summary>Click to expand release_helper.py</summary>

```python
#!/usr/bin/env python3
"""
Release Helper for DevAIFlow

Automates version updates, CHANGELOG management, and validation.
"""

import re
import sys
from datetime import datetime
from pathlib import Path
from typing import Tuple, Optional, List


class ReleaseHelper:
    """Helper class for managing DevAIFlow releases."""

    def __init__(self, repo_path: str = "."):
        """Initialize ReleaseHelper with repository path."""
        self.repo_path = Path(repo_path)
        
        # TODO: Update these paths for your project
        self.version_files = {
            'setup.py': self._update_setup_py,
            'pyproject.toml': self._update_pyproject_toml,
            'src/package/__init__.py': self._update_init_py,
        }
        
        self.changelog_path = self.repo_path / "CHANGELOG.md"

    def get_current_version(self) -> Tuple[Optional[str], bool]:
        """
        Get current version from all files.

        Returns:
            Tuple of (version, all_match)
        """
        versions = {}
        
        for file_path, _ in self.version_files.items():
            version = self._read_version_from_file(file_path)
            if version:
                versions[file_path] = version
        
        # Check if all versions match
        unique_versions = set(versions.values())
        all_match = len(unique_versions) == 1
        current_version = list(unique_versions)[0] if unique_versions else None
        
        return current_version, all_match

    def _read_version_from_file(self, file_path: str) -> Optional[str]:
        """Read version from a specific file."""
        try:
            full_path = self.repo_path / file_path
            if not full_path.exists():
                return None
                
            content = full_path.read_text()
            
            # Try different version patterns
            patterns = [
                r'version\s*=\s*["\']([^"\']+)["\']',
                r'__version__\s*=\s*["\']([^"\']+)["\']',
                r'"version":\s*"([^"]+)"',
            ]
            
            for pattern in patterns:
                match = re.search(pattern, content)
                if match:
                    return match.group(1)
                    
            return None
        except Exception as e:
            print(f"Error reading {file_path}: {e}", file=sys.stderr)
            return None

    def update_version(self, new_version: str) -> bool:
        """Update version in all files."""
        try:
            for file_path, update_func in self.version_files.items():
                full_path = self.repo_path / file_path
                if full_path.exists():
                    update_func(full_path, new_version)
            
            # Verify update
            version, all_match = self.get_current_version()
            return version == new_version and all_match
            
        except Exception as e:
            print(f"Error updating version: {e}", file=sys.stderr)
            return False

    def _update_setup_py(self, file_path: Path, new_version: str):
        """Update version in setup.py."""
        content = file_path.read_text()
        updated = re.sub(
            r'version\s*=\s*["\'][^"\']+["\']',
            f'version="{new_version}"',
            content
        )
        file_path.write_text(updated)

    def _update_pyproject_toml(self, file_path: Path, new_version: str):
        """Update version in pyproject.toml."""
        content = file_path.read_text()
        updated = re.sub(
            r'^version\s*=\s*"[^"]+"',
            f'version = "{new_version}"',
            content,
            flags=re.MULTILINE
        )
        file_path.write_text(updated)

    def _update_init_py(self, file_path: Path, new_version: str):
        """Update version in __init__.py."""
        content = file_path.read_text()
        updated = re.sub(
            r'^__version__\s*=\s*"[^"]+"',
            f'__version__ = "{new_version}"',
            content,
            flags=re.MULTILINE
        )
        file_path.write_text(updated)

    def calculate_next_version(self, current_version: str, release_type: str) -> Optional[str]:
        """Calculate next version based on release type."""
        # Remove -dev or -test* suffix
        base_version = re.sub(r'-dev|-test\d*', '', current_version)
        
        # Parse semantic version
        match = re.match(r'^(\d+)\.(\d+)\.(\d+)$', base_version)
        if not match:
            print(f"Error: Invalid version format: {base_version}", file=sys.stderr)
            return None
        
        major, minor, patch = map(int, match.groups())
        
        if release_type == "major":
            return f"{major + 1}.0.0"
        elif release_type == "minor":
            return f"{major}.{minor + 1}.0"
        elif release_type == "patch":
            return f"{major}.{minor}.{patch + 1}"
        elif release_type == "test":
            return f"{major}.{minor}.{patch}-test1"
        else:
            print(f"Error: Invalid release type: {release_type}", file=sys.stderr)
            return None

    def update_changelog(self, version: str, date: Optional[str] = None) -> bool:
        """Update CHANGELOG.md by moving Unreleased section to new version."""
        if date is None:
            date = datetime.now().strftime("%Y-%m-%d")
        
        try:
            content = self.changelog_path.read_text()
            
            # Check if Unreleased section exists
            if "## [Unreleased]" not in content:
                print("Warning: No [Unreleased] section found", file=sys.stderr)
                return False
            
            # Find Unreleased content
            unreleased_pattern = r'## \[Unreleased\]\s*(.*?)(?=## \[|$)'
            unreleased_match = re.search(unreleased_pattern, content, re.DOTALL)
            
            if not unreleased_match:
                return False
            
            unreleased_content = unreleased_match.group(1).strip()
            
            if not unreleased_content or unreleased_content.isspace():
                print("Warning: Unreleased section is empty", file=sys.stderr)
                return False
            
            # Create new version section
            version_section = f"## [{version}] - {date}\n\n{unreleased_content}\n\n"
            
            # Replace Unreleased with new empty + version
            new_content = re.sub(
                r'## \[Unreleased\]\s*.*?(?=## \[|$)',
                f"## [Unreleased]\n\n{version_section}",
                content,
                count=1,
                flags=re.DOTALL
            )
            
            # Update version links
            repo_url_match = re.search(
                r'\[Unreleased\]: (https://github\.com/[^/]+/[^/]+)/compare/v([^.]+\.[^.]+\.[^.]+)\.\.\.HEAD',
                new_content
            )
            
            if repo_url_match:
                repo_url = repo_url_match.group(1)
                new_content = re.sub(
                    r'\[Unreleased\]: .*?\n',
                    f'[Unreleased]: {repo_url}/compare/v{version}...HEAD\n',
                    new_content,
                    count=1
                )
                new_version_link = f'[{version}]: {repo_url}/releases/tag/v{version}\n'
                new_content = re.sub(
                    r'(\[Unreleased\]: .*?\n)',
                    f'\\1{new_version_link}',
                    new_content,
                    count=1
                )
            
            self.changelog_path.write_text(new_content)
            return True
            
        except Exception as e:
            print(f"Error updating CHANGELOG: {e}", file=sys.stderr)
            return False

    def validate_prerequisites(self, release_type: str = "regular") -> Tuple[bool, List[str]]:
        """Validate prerequisites for a release."""
        errors = []
        
        # Check version files exist
        for file_path in self.version_files.keys():
            full_path = self.repo_path / file_path
            if not full_path.exists():
                errors.append(f"{file_path} not found")
        
        if not self.changelog_path.exists():
            errors.append("CHANGELOG.md not found")
        
        if errors:
            return False, errors
        
        # Check versions match
        version, all_match = self.get_current_version()
        if not all_match:
            errors.append("Version mismatch between files")
        
        # For regular releases, check CHANGELOG
        if release_type == "regular":
            try:
                content = self.changelog_path.read_text()
                if "## [Unreleased]" not in content:
                    errors.append("CHANGELOG.md missing [Unreleased] section")
                else:
                    unreleased_pattern = r'## \[Unreleased\]\s*(.*?)(?=## \[|$)'
                    match = re.search(unreleased_pattern, content, re.DOTALL)
                    if match:
                        unreleased_content = match.group(1).strip()
                        if not unreleased_content or unreleased_content.isspace():
                            errors.append("CHANGELOG.md [Unreleased] section is empty")
            except Exception as e:
                errors.append(f"Error reading CHANGELOG.md: {e}")
        
        return len(errors) == 0, errors


def main():
    """CLI interface for release helper."""
    import argparse
    
    parser = argparse.ArgumentParser(description="DevAIFlow Release Helper")
    parser.add_argument("--repo", default=".", help="Repository path")
    
    subparsers = parser.add_subparsers(dest="command", help="Command to run")
    
    # Get version
    subparsers.add_parser("get-version", help="Get current version")
    
    # Update version
    update_parser = subparsers.add_parser("update-version", help="Update version")
    update_parser.add_argument("version", help="New version (e.g., 1.2.0)")
    
    # Calculate next version
    calc_parser = subparsers.add_parser("calc-version", help="Calculate next version")
    calc_parser.add_argument("current", help="Current version")
    calc_parser.add_argument("type", choices=["major", "minor", "patch", "test"])
    
    # Update changelog
    changelog_parser = subparsers.add_parser("update-changelog", help="Update CHANGELOG.md")
    changelog_parser.add_argument("version", help="Version to release")
    changelog_parser.add_argument("--date", help="Release date (YYYY-MM-DD)")
    
    # Validate
    validate_parser = subparsers.add_parser("validate", help="Validate prerequisites")
    validate_parser.add_argument("--type", default="regular", choices=["regular", "hotfix", "test"])
    
    args = parser.parse_args()
    
    helper = ReleaseHelper(args.repo)
    
    if args.command == "get-version":
        version, all_match = helper.get_current_version()
        print(f"Current version: {version}")
        print(f"All files match: {all_match}")
        sys.exit(0 if all_match else 1)
    
    elif args.command == "update-version":
        success = helper.update_version(args.version)
        sys.exit(0 if success else 1)
    
    elif args.command == "calc-version":
        next_version = helper.calculate_next_version(args.current, args.type)
        if next_version:
            print(next_version)
            sys.exit(0)
        else:
            sys.exit(1)
    
    elif args.command == "update-changelog":
        success = helper.update_changelog(args.version, args.date)
        sys.exit(0 if success else 1)
    
    elif args.command == "validate":
        valid, errors = helper.validate_prerequisites(args.type)
        if valid:
            print("✓ All prerequisites validated")
            sys.exit(0)
        else:
            print("✗ Validation failed:", file=sys.stderr)
            for error in errors:
                print(f"  - {error}", file=sys.stderr)
            sys.exit(1)
    
    else:
        parser.print_help()
        sys.exit(1)


if __name__ == "__main__":
    main()

TODO for release_helper.py:

  1. Update self.version_files dict with your actual version file locations
  2. Add/modify update functions for your specific file formats
  3. Test with your repository structure

Phase 2: Tag Monitoring Workflow

File: .github/workflows/tag-monitor.yml

Click to expand tag-monitor.yml
name: Tag Creation Monitor

on:
  push:
    tags:
      - 'v*'      # Production tags (v1.0.0)
      - 'v*-test*' # Test tags (v1.0.0-test1)

jobs:
  monitor-and-notify:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      issues: write

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Get tag information
        id: tag-info
        run: |
          TAG_NAME="${GITHUB_REF#refs/tags/}"
          TAG_CREATOR="${GITHUB_ACTOR}"
          TAG_COMMIT=$(git rev-list -n 1 "${TAG_NAME}")
          COMMIT_AUTHOR=$(git log -1 --pretty=%an "${TAG_COMMIT}")

          echo "tag_name=${TAG_NAME}" >> $GITHUB_OUTPUT
          echo "tag_creator=${TAG_CREATOR}" >> $GITHUB_OUTPUT
          echo "tag_commit=${TAG_COMMIT}" >> $GITHUB_OUTPUT
          echo "commit_author=${COMMIT_AUTHOR}" >> $GITHUB_OUTPUT

      - name: Check authorization
        id: auth-check
        run: |
          TAG_NAME="${{ steps.tag-info.outputs.tag_name }}"
          TAG_CREATOR="${{ steps.tag-info.outputs.tag_creator }}"

          # TODO: Update with your authorized maintainers
          AUTHORIZED_USERS=("itdove")

          # Check if production release
          if [[ "${TAG_NAME}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
            IS_PRODUCTION="true"
            TAG_TYPE="Production Release"
          elif [[ "${TAG_NAME}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+-test ]]; then
            IS_PRODUCTION="false"
            TAG_TYPE="Test Release"
          else
            IS_PRODUCTION="false"
            TAG_TYPE="Unknown"
          fi

          # Check authorization
          IS_AUTHORIZED="false"
          if [ "${IS_PRODUCTION}" = "true" ]; then
            for user in "${AUTHORIZED_USERS[@]}"; do
              if [ "${TAG_CREATOR}" = "${user}" ]; then
                IS_AUTHORIZED="true"
                break
              fi
            done
          else
            IS_AUTHORIZED="true"
          fi

          echo "is_production=${IS_PRODUCTION}" >> $GITHUB_OUTPUT
          echo "tag_type=${TAG_TYPE}" >> $GITHUB_OUTPUT
          echo "is_authorized=${IS_AUTHORIZED}" >> $GITHUB_OUTPUT

          echo "Tag Type: ${TAG_TYPE}"
          echo "Created by: ${TAG_CREATOR}"
          echo "Authorized: ${IS_AUTHORIZED}"

      - name: Log tag creation (authorized)
        if: steps.auth-check.outputs.is_authorized == 'true'
        run: |
          echo "::notice title=Authorized Tag Created::Tag '${{ steps.tag-info.outputs.tag_name }}' created by @${{ steps.tag-info.outputs.tag_creator }}"
          echo ""
          echo "========================================================================"
          echo "✓ AUTHORIZED TAG CREATION"
          echo "========================================================================"
          echo "Tag:              ${{ steps.tag-info.outputs.tag_name }}"
          echo "Type:             ${{ steps.auth-check.outputs.tag_type }}"
          echo "Created by:       @${{ steps.tag-info.outputs.tag_creator }}"
          echo "Commit:           ${{ steps.tag-info.outputs.tag_commit }}"
          echo "Authorization:    ✓ Authorized"
          echo "========================================================================"

      - name: Log tag creation (unauthorized)
        if: steps.auth-check.outputs.is_authorized == 'false'
        run: |
          echo "::error title=Unauthorized Tag::Production tag '${{ steps.tag-info.outputs.tag_name }}' created by unauthorized user @${{ steps.tag-info.outputs.tag_creator }}"
          echo ""
          echo "========================================================================"
          echo "✗ UNAUTHORIZED TAG CREATION"
          echo "========================================================================"
          echo "Tag:              ${{ steps.tag-info.outputs.tag_name }}"
          echo "Type:             ${{ steps.auth-check.outputs.tag_type }}"
          echo "Created by:       @${{ steps.tag-info.outputs.tag_creator }}"
          echo "Authorization:    ✗ NOT AUTHORIZED"
          echo ""
          echo "ACTION REQUIRED:"
          echo "1. Delete tag: git push origin :refs/tags/${{ steps.tag-info.outputs.tag_name }}"
          echo "2. Report to repository owner"
          echo "========================================================================"
          exit 1

      - name: Create security issue (unauthorized)
        if: steps.auth-check.outputs.is_authorized == 'false'
        uses: actions/github-script@v7
        with:
          script: |
            const tagName = '${{ steps.tag-info.outputs.tag_name }}';
            const tagCreator = '${{ steps.tag-info.outputs.tag_creator }}';
            const commitSha = '${{ steps.tag-info.outputs.tag_commit }}';

            await github.rest.issues.create({
              owner: context.repo.owner,
              repo: context.repo.repo,
              title: `🚨 Unauthorized Release Tag: ${tagName}`,
              labels: ['security', 'unauthorized-release', 'priority-high'],
              body: `## Unauthorized Release Tag Detected

            **Tag:** \`${tagName}\`
            **Created by:** @${tagCreator}
            **Commit:** ${commitSha}
            **Time:** ${new Date().toISOString()}

            ## Required Actions

            - [ ] Verify if intentional
            - [ ] Delete tag: \`git push origin :refs/tags/${tagName}\`
            - [ ] Review RELEASING.md policy with @${tagCreator}
            - [ ] Recreate by authorized user if needed

            See [RELEASING.md](RELEASING.md) for authorization policy.

            *Auto-generated by tag-monitor workflow*`
            });

TODO for tag-monitor.yml:

  1. Update AUTHORIZED_USERS array with your maintainers
  2. Adjust tag patterns if needed (v* vs other formats)
  3. Customize security issue template

Phase 3: CODEOWNERS File

File: .github/CODEOWNERS

Click to expand CODEOWNERS
# CODEOWNERS - Define code ownership and required reviewers
# See: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners

# Default owner for everything
* @itdove

# Critical workflow files - require owner approval
/.github/workflows/publish.yml @itdove
/.github/workflows/tag-monitor.yml @itdove

# Release documentation - require owner approval
/RELEASING.md @itdove
/CONTRIBUTING.md @itdove

# Security and configuration
/.github/CODEOWNERS @itdove
/.gitignore @itdove

# Version files - require owner approval
/setup.py @itdove
/pyproject.toml @itdove
/src/package/__init__.py @itdove

# TODO: Add any other critical files specific to devaiflow

TODO for CODEOWNERS:

  1. Update version file paths to match your project
  2. Add additional maintainers if needed
  3. Add any project-specific critical files

Phase 4: Fork-Based Workflow Documentation

File: CONTRIBUTING.md (New)

Click to expand CONTRIBUTING.md structure
# Contributing to DevAIFlow

## Fork-Based Workflow

All external contributions must come from forks.

### Quick Start

```bash
# 1. Fork the repository
gh repo fork itdove/devaiflow --clone

# 2. Create feature branch
cd devaiflow
git checkout -b feature-name

# 3. Make changes
git add .
git commit -m "feat: description"

# 4. Push to your fork
git push origin feature-name

# 5. Create pull request
gh pr create --web

Detailed Setup

  1. Fork the Repository

    • Via UI: Click "Fork" button
    • Via CLI: gh repo fork itdove/devaiflow --clone
  2. Configure Remotes

    git remote add upstream https://github.com/itdove/devaiflow.git
    git remote -v
  3. Keep Fork Synced

    git checkout main
    git fetch upstream
    git merge upstream/main
    git push origin main

Branch Naming

  • feature/description - New features
  • fix/description - Bug fixes
  • docs/description - Documentation
  • refactor/description - Code refactoring
  • test/description - Tests

Commit Messages

<type>: <subject>

<body>

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

Types: feat, fix, docs, refactor, test, chore

Pull Request Process

  1. Update CHANGELOG.md under [Unreleased]
  2. Add tests for new features/fixes
  3. Ensure all tests pass
  4. Request review

Release Process

Contributors CANNOT create releases.

  • ✅ Submit PRs with features/fixes
  • ✅ Update CHANGELOG.md in PR
  • ❌ DO NOT create version tags
  • ❌ DO NOT modify version numbers

Maintainers handle all releases.

Testing

# TODO: Add your project's test commands
pytest
# or
python -m unittest
# or
npm test

Code Style

TODO: Add your project's code style guidelines

Getting Help

  • 📖 Read documentation
  • 🐛 Open an issue
  • 💬 Ask in your PR

</details>

#### File: `RELEASING.md` (Update or Create)

<details>
<summary>Click to expand RELEASING.md structure</summary>

```markdown
# Release Management Process

## Release Authorization Policy

**IMPORTANT**: Only repository maintainers can create release tags.

### Repository Access Model

**Fork-Based Workflow:**

- **Maintainers** (@itdove): Write/Admin access
  - Can push tags
  - Can create releases
  - Handle PyPI publications

- **Contributors**: Must use forks
  - Cannot push tags
  - All changes via PRs
  - See [CONTRIBUTING.md](CONTRIBUTING.md)

### Authorized Release Managers

Currently authorized:
- @itdove (Repository Owner)

### For Contributors

**DO NOT create release tags.**

Instead:
1. Fork repository
2. Submit PRs with changes
3. Update CHANGELOG.md
4. Maintainers handle releases

### For Maintainers

Use the `/release` skill:

```bash
/release minor   # 1.1.0 -> 1.2.0
/release patch   # 1.1.0 -> 1.1.1
/release major   # 1.0.0 -> 2.0.0

Version Numbering

Semantic Versioning (MAJOR.MINOR.PATCH):

  • MAJOR: Breaking changes
  • MINOR: New features (backward compatible)
  • PATCH: Bug fixes (backward compatible)
  • Development: X.Y.Z-dev (on main)

Release Workflow

Prerequisites

  • All tests pass
  • CHANGELOG.md updated
  • Main branch up-to-date

Steps

  1. Use /release <type> skill
  2. Review changes on release branch
  3. Create tag: git tag -a vX.Y.Z -m "Release X.Y.Z"
  4. Push tag: git push origin vX.Y.Z
  5. Monitor GitHub Actions
  6. Verify publication
  7. Merge back to main

Hotfix Workflow

  1. Use /release hotfix vX.Y.Z
  2. Fix bug on hotfix branch
  3. Update version and CHANGELOG
  4. Create tag
  5. Push tag
  6. Merge to release branch
  7. Cherry-pick to main

Tag Monitoring

The tag-monitor.yml workflow:

  • Logs all tag creation
  • Verifies authorization
  • Creates security issues for violations
  • Provides audit trail

Protected Files (CODEOWNERS)

Changes require maintainer approval:

  • .github/workflows/*.yml
  • RELEASING.md
  • Version files

</details>

#### Update: `README.md`

Add Contributing section:

```markdown
## Contributing

We use a **fork-based workflow**.

### Quick Start

```bash
# 1. Fork repository
gh repo fork itdove/devaiflow --clone

# 2. Create feature branch
git checkout -b feature-name

# 3. Make changes and commit
git commit -am "feat: description"

# 4. Push to fork
git push origin feature-name

# 5. Create PR
gh pr create --web

Important

  • ✅ All contributions via forks
  • ✅ Update CHANGELOG.md
  • ✅ Add tests
  • ❌ Do NOT create release tags

See CONTRIBUTING.md for details.


### Phase 5: Testing

Create tests for the release helper:

<details>
<summary>Click to expand test structure</summary>

```python
#!/usr/bin/env python3
"""Tests for release helper module."""

import sys
import tempfile
from pathlib import Path

sys.path.insert(0, str(Path.home() / ".claude" / "skills" / "release"))
from release_helper import ReleaseHelper


def test_get_current_version():
    """Test getting current version."""
    # Create test repo structure
    # Add test assertions
    pass


def test_version_mismatch_detection():
    """Test detecting version mismatches."""
    pass


def test_update_version():
    """Test updating version in all files."""
    pass


def test_calculate_next_version_minor():
    """Test calculating next minor version."""
    helper = ReleaseHelper()
    assert helper.calculate_next_version("1.1.0-dev", "minor") == "1.2.0"


def test_calculate_next_version_major():
    """Test calculating next major version."""
    helper = ReleaseHelper()
    assert helper.calculate_next_version("1.1.0-dev", "major") == "2.0.0"


def test_update_changelog():
    """Test CHANGELOG.md updates."""
    pass


def test_validate_prerequisites():
    """Test prerequisites validation."""
    pass


if __name__ == "__main__":
    # Run tests
    pass

Implementation Checklist

Preparation

  • Identify all version file locations in devaiflow
  • Check current CHANGELOG.md format
  • Identify authorized maintainers
  • Review current release process
  • Check if PyPI/package registry is used

Release Skill

  • Create ~/.claude/skills/release/ directory
  • Create SKILL.md with project-specific details
  • Create release_helper.py and update version file paths
  • Make release_helper.py executable: chmod +x release_helper.py
  • Create tests in tests/test_release_helper.py
  • Test helper script with actual repo
  • Create README.md and EXAMPLE_USAGE.md for skill

Tag Monitoring

  • Create .github/workflows/tag-monitor.yml
  • Update AUTHORIZED_USERS array
  • Test workflow with test tag
  • Verify security issue creation works

CODEOWNERS

  • Create .github/CODEOWNERS
  • List all critical files
  • Test that PR approval is required

Documentation

  • Create CONTRIBUTING.md
  • Create or update RELEASING.md
  • Update README.md with Contributing section
  • Update any team/project docs

Verification

  • Run all tests: pytest or equivalent
  • Test fork workflow with test account
  • Test /release skill with test release
  • Verify tag monitor catches unauthorized tags
  • Verify CODEOWNERS blocks workflow changes

Deployment

  • Review with team
  • Notify existing collaborators
  • Remove Write access from non-maintainers
  • Monitor first few releases
  • Document any issues/improvements

Questions to Answer First

Before starting implementation, answer these:

  1. Version Files: Where is version stored?

    • setup.py?
    • pyproject.toml?
    • init.py?
    • package.json?
    • Other: ___________
  2. Package Registry: Where do you publish?

    • PyPI
    • npm
    • Other: ___________
    • Not applicable
  3. CHANGELOG Format: What format?

    • Keep a Changelog
    • Custom format
    • None (need to create)
  4. Current Maintainers: Who should be authorized?

  5. Test Strategy: How to test?

    • pytest
    • unittest
    • Other: ___________
  6. Existing Workflows: Any release workflows?

    • Yes (need to review)
    • No (need to create)

Reference Files from ai-guardian

All implementation files from ai-guardian#16 are available for reference:

Skill files:

  • ~/.claude/skills/release/SKILL.md
  • ~/.claude/skills/release/release_helper.py
  • ~/.claude/skills/release/README.md
  • ~/.claude/skills/release/EXAMPLE_USAGE.md

Repository files:

  • .github/workflows/tag-monitor.yml
  • .github/CODEOWNERS
  • CONTRIBUTING.md
  • RELEASING.md (updated)
  • tests/test_release_helper.py

Success Metrics

After implementation:

  • ✅ Only maintainers can push production tags
  • /release skill reduces manual errors
  • ✅ All tag creation is logged and audited
  • ✅ Unauthorized attempts trigger security alerts
  • ✅ Clear contributor guidelines
  • ✅ Consistent version management
  • ✅ Automated CHANGELOG updates

Timeline Estimate

  • Preparation & Planning: 2 hours
  • Release Skill Development: 4-6 hours
  • Workflows & CODEOWNERS: 2 hours
  • Documentation: 3-4 hours
  • Testing & Verification: 2-3 hours
  • Total: ~13-17 hours

Support

Questions or issues during implementation?

  1. Reference ai-guardian#16 for examples
  2. Check file templates in this issue
  3. Open discussion in this issue
  4. Tag @itdove for help

Labels

enhancement, tooling, developer-experience, release-management, security, automation

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions