From dd6a6c24d7e57bce7dd46363ed6add40c09e2cd8 Mon Sep 17 00:00:00 2001 From: Reza Rezvani Date: Wed, 12 Nov 2025 12:51:48 +0100 Subject: [PATCH] feat(ci): implement comprehensive CI/CD workflows and quality gates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 1: Core GitHub Workflows Implementation Composite Actions (4): - setup-python-deps: Cache Python dependencies for faster runs - fork-safety: Detect fork PRs and prevent malicious write operations - rate-limit-check: Circuit breaker pattern for GitHub API exhaustion - quality-gates: Python syntax, Markdown lint, Bash validation, secret scanning Workflows (5): - bootstrap.yml: One-time repository setup (labels, milestones, settings) - reusable-pr-checks.yml: DRY quality gate orchestrator - pr-into-dev.yml: Feature PR validation (branch names, conventional commits, linked issues) - dev-to-main.yml: Release gate validation (source branch, CHANGELOG, production readiness) - release.yml: Manual release creation with GitHub releases and auto-generated notes Branch Strategy: Standard (feature/* → dev → main) Quality Gates: Python, Markdown, Bash, Secrets Release Trigger: Manual via /release command or workflow_dispatch Implements comprehensive CI/CD system adapted from blueprint: - Fork safety and rate limiting for security - Conventional commits enforcement - Automated quality validation - Production release gates - GitHub release automation Next: Phase 2 (templates, CODEOWNERS, dependabot) --- .github/actions/fork-safety/action.yml | 93 ++++++ .github/actions/quality-gates/action.yml | 278 ++++++++++++++++ .github/actions/rate-limit-check/action.yml | 138 ++++++++ .github/actions/setup-python-deps/action.yml | 62 ++++ .github/workflows/bootstrap.yml | 190 +++++++++++ .github/workflows/dev-to-main.yml | 322 +++++++++++++++++++ .github/workflows/pr-into-dev.yml | 181 +++++++++++ .github/workflows/release.yml | 270 ++++++++++++++++ .github/workflows/reusable-pr-checks.yml | 127 ++++++++ 9 files changed, 1661 insertions(+) create mode 100644 .github/actions/fork-safety/action.yml create mode 100644 .github/actions/quality-gates/action.yml create mode 100644 .github/actions/rate-limit-check/action.yml create mode 100644 .github/actions/setup-python-deps/action.yml create mode 100644 .github/workflows/bootstrap.yml create mode 100644 .github/workflows/dev-to-main.yml create mode 100644 .github/workflows/pr-into-dev.yml create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/reusable-pr-checks.yml diff --git a/.github/actions/fork-safety/action.yml b/.github/actions/fork-safety/action.yml new file mode 100644 index 0000000..bbdc60a --- /dev/null +++ b/.github/actions/fork-safety/action.yml @@ -0,0 +1,93 @@ +name: 'Fork Safety Check' +description: 'Detect fork PRs to skip write operations and maintain security' +author: 'ClaudeForge' + +branding: + icon: 'shield' + color: 'blue' + +inputs: + github-token: + description: 'GitHub token for API access (usually secrets.GITHUB_TOKEN)' + required: false + default: ${{ github.token }} + +outputs: + is-fork: + description: 'Boolean indicating if the PR is from a fork (true/false)' + value: ${{ steps.check-fork.outputs.is-fork }} + should-skip-writes: + description: 'Boolean indicating if write operations should be skipped (true/false)' + value: ${{ steps.check-fork.outputs.should-skip-writes }} + source-repo: + description: 'Full name of the source repository (owner/repo)' + value: ${{ steps.check-fork.outputs.source-repo }} + base-repo: + description: 'Full name of the base repository (owner/repo)' + value: ${{ steps.check-fork.outputs.base-repo }} + +runs: + using: 'composite' + steps: + - name: Check if PR is from fork + id: check-fork + shell: bash + env: + GITHUB_TOKEN: ${{ inputs.github-token }} + run: | + echo "::group::Fork Safety Check" + + # Initialize outputs + IS_FORK="false" + SHOULD_SKIP_WRITES="false" + SOURCE_REPO="unknown" + BASE_REPO="unknown" + + # Check if this is a pull request event + if [[ "${{ github.event_name }}" == "pull_request"* ]]; then + echo "📋 Event: Pull Request detected" + + # Get fork status from event context + FORK_STATUS="${{ github.event.pull_request.head.repo.fork }}" + SOURCE_REPO="${{ github.event.pull_request.head.repo.full_name }}" + BASE_REPO="${{ github.event.pull_request.base.repo.full_name }}" + + echo "🔍 Source Repository: $SOURCE_REPO" + echo "đŸŽ¯ Base Repository: $BASE_REPO" + + if [[ "$FORK_STATUS" == "true" ]]; then + IS_FORK="true" + SHOULD_SKIP_WRITES="true" + echo "âš ī¸ Fork PR detected - Write operations should be skipped" + echo "🔒 Security: Preventing potential malicious actions from forked PR" + else + echo "✅ Same-repository PR - Write operations allowed" + fi + + else + echo "â„šī¸ Not a pull request event - treating as safe (non-fork)" + echo "📌 Event type: ${{ github.event_name }}" + fi + + # Set outputs + echo "is-fork=$IS_FORK" >> $GITHUB_OUTPUT + echo "should-skip-writes=$SHOULD_SKIP_WRITES" >> $GITHUB_OUTPUT + echo "source-repo=$SOURCE_REPO" >> $GITHUB_OUTPUT + echo "base-repo=$BASE_REPO" >> $GITHUB_OUTPUT + + # Summary + echo "" + echo "📊 Fork Safety Check Results:" + echo " - Is Fork: $IS_FORK" + echo " - Skip Writes: $SHOULD_SKIP_WRITES" + echo " - Source: $SOURCE_REPO" + echo " - Base: $BASE_REPO" + + echo "::endgroup::" + + - name: Log fork detection result + shell: bash + run: | + if [[ "${{ steps.check-fork.outputs.is-fork }}" == "true" ]]; then + echo "::warning::This PR is from a fork. Write operations will be skipped for security." + fi diff --git a/.github/actions/quality-gates/action.yml b/.github/actions/quality-gates/action.yml new file mode 100644 index 0000000..cf4576a --- /dev/null +++ b/.github/actions/quality-gates/action.yml @@ -0,0 +1,278 @@ +name: 'ClaudeForge Quality Gates' +description: 'Comprehensive quality validation for Python, Markdown, Bash scripts, and security' +author: 'ClaudeForge' + +branding: + icon: 'check-circle' + color: 'green' + +inputs: + python-version: + description: 'Python version to use for validation' + required: false + default: '3.11' + skip-python: + description: 'Skip Python validation' + required: false + default: 'false' + skip-markdown: + description: 'Skip Markdown validation' + required: false + default: 'false' + skip-bash: + description: 'Skip Bash script validation' + required: false + default: 'false' + skip-secrets: + description: 'Skip secret scanning' + required: false + default: 'false' + +outputs: + python-passed: + description: 'Whether Python validation passed' + value: ${{ steps.validate-python.outputs.passed }} + markdown-passed: + description: 'Whether Markdown validation passed' + value: ${{ steps.validate-markdown.outputs.passed }} + bash-passed: + description: 'Whether Bash validation passed' + value: ${{ steps.validate-bash.outputs.passed }} + secrets-passed: + description: 'Whether secret scanning passed' + value: ${{ steps.scan-secrets.outputs.passed }} + all-passed: + description: 'Whether all quality gates passed' + value: ${{ steps.summary.outputs.all-passed }} + +runs: + using: 'composite' + steps: + - name: Setup Python for validation + if: inputs.skip-python != 'true' + uses: actions/setup-python@v5 + with: + python-version: ${{ inputs.python-version }} + + - name: Install validation tools + if: inputs.skip-python != 'true' + shell: bash + run: | + pip install flake8 pylint mypy black --quiet + + - name: Validate Python syntax and style + id: validate-python + if: inputs.skip-python != 'true' + shell: bash + run: | + echo "::group::Python Validation" + PASSED="true" + + # Find all Python files + PYTHON_FILES=$(find skill -name "*.py" 2>/dev/null || echo "") + + if [ -z "$PYTHON_FILES" ]; then + echo "â„šī¸ No Python files found to validate" + echo "passed=true" >> $GITHUB_OUTPUT + echo "::endgroup::" + exit 0 + fi + + echo "📋 Found Python files:" + echo "$PYTHON_FILES" + echo "" + + # Run flake8 (syntax and style) + echo "🔍 Running flake8 (syntax and style)..." + if flake8 skill/ --count --select=E9,F63,F7,F82 --show-source --statistics; then + echo "✅ Flake8 syntax check passed" + else + echo "::error::Flake8 found syntax errors" + PASSED="false" + fi + + # Run flake8 for style (non-blocking) + echo "" + echo "🎨 Running flake8 (style warnings)..." + flake8 skill/ --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics || true + + echo "passed=$PASSED" >> $GITHUB_OUTPUT + echo "::endgroup::" + + if [ "$PASSED" = "false" ]; then + exit 1 + fi + + - name: Validate Markdown files + id: validate-markdown + if: inputs.skip-markdown != 'true' + shell: bash + run: | + echo "::group::Markdown Validation" + PASSED="true" + + # Find all Markdown files + MD_FILES=$(find . -name "*.md" -not -path "./node_modules/*" -not -path "./.git/*" 2>/dev/null || echo "") + + if [ -z "$MD_FILES" ]; then + echo "â„šī¸ No Markdown files found to validate" + echo "passed=true" >> $GITHUB_OUTPUT + echo "::endgroup::" + exit 0 + fi + + echo "📋 Found Markdown files: $(echo "$MD_FILES" | wc -l) files" + echo "" + + # Basic validation checks + echo "🔍 Checking Markdown files..." + + for file in $MD_FILES; do + # Check for empty files + if [ ! -s "$file" ]; then + echo "::warning file=$file::Empty Markdown file" + fi + + # Check for broken relative links (basic check) + BROKEN_LINKS=$(grep -o '\[.*\]([^h].*\.md)' "$file" | sed 's/.*(\(.*\))/\1/' || true) + for link in $BROKEN_LINKS; do + LINK_PATH=$(dirname "$file")/"$link" + if [ ! -f "$LINK_PATH" ]; then + echo "::warning file=$file::Potentially broken link: $link" + fi + done + done + + echo "✅ Markdown validation completed" + echo "passed=$PASSED" >> $GITHUB_OUTPUT + echo "::endgroup::" + + - name: Validate Bash scripts + id: validate-bash + if: inputs.skip-bash != 'true' + shell: bash + run: | + echo "::group::Bash Script Validation" + PASSED="true" + + # Find bash scripts + BASH_FILES=$(find . -name "*.sh" -not -path "./node_modules/*" -not -path "./.git/*" 2>/dev/null || echo "") + + if [ -z "$BASH_FILES" ]; then + echo "â„šī¸ No Bash scripts found to validate" + echo "passed=true" >> $GITHUB_OUTPUT + echo "::endgroup::" + exit 0 + fi + + echo "📋 Found Bash scripts:" + echo "$BASH_FILES" + echo "" + + # Validate syntax + echo "🔍 Checking Bash syntax..." + for script in $BASH_FILES; do + echo "Checking: $script" + if bash -n "$script" 2>&1 | grep -v "warning:"; then + echo " ✅ Syntax valid" + else + echo "::error file=$script::Bash syntax error detected" + PASSED="false" + fi + done + + echo "passed=$PASSED" >> $GITHUB_OUTPUT + echo "::endgroup::" + + if [ "$PASSED" = "false" ]; then + exit 1 + fi + + - name: Scan for secrets + id: scan-secrets + if: inputs.skip-secrets != 'true' + shell: bash + run: | + echo "::group::Secret Scanning" + PASSED="true" + + echo "🔍 Scanning for hardcoded secrets..." + + # Patterns to detect + PATTERNS=( + "api[_-]?key" + "api[_-]?secret" + "password\s*=" + "token\s*=" + "secret\s*=" + "AWS_ACCESS_KEY" + "AWS_SECRET_KEY" + "GITHUB_TOKEN" + "ANTHROPIC_API_KEY" + ) + + # Files to scan (exclude binary, vendor, etc.) + FILES=$(find . -type f \( -name "*.py" -o -name "*.sh" -o -name "*.md" -o -name "*.yml" -o -name "*.yaml" \) \ + -not -path "./node_modules/*" \ + -not -path "./.git/*" \ + -not -path "./venv/*" \ + 2>/dev/null || echo "") + + for pattern in "${PATTERNS[@]}"; do + MATCHES=$(echo "$FILES" | xargs grep -niE "$pattern" 2>/dev/null || true) + + if [ ! -z "$MATCHES" ]; then + # Filter out common false positives (examples, docs, variable declarations without values) + REAL_MATCHES=$(echo "$MATCHES" | grep -viE "(example|sample|placeholder|your_|xxx|<|template)" || true) + + if [ ! -z "$REAL_MATCHES" ]; then + echo "::warning::Potential secret found with pattern: $pattern" + echo "$REAL_MATCHES" + echo "" + fi + fi + done + + # Check for .env files committed + if find . -name ".env" -not -path "./.git/*" | grep -q ".env"; then + echo "::error::.env file found in repository. This should be in .gitignore!" + PASSED="false" + fi + + if [ "$PASSED" = "true" ]; then + echo "✅ No obvious secrets detected" + fi + + echo "passed=$PASSED" >> $GITHUB_OUTPUT + echo "::endgroup::" + + - name: Quality Gates Summary + id: summary + shell: bash + run: | + echo "::group::Quality Gates Summary" + + PYTHON_PASSED="${{ steps.validate-python.outputs.passed || 'true' }}" + MARKDOWN_PASSED="${{ steps.validate-markdown.outputs.passed || 'true' }}" + BASH_PASSED="${{ steps.validate-bash.outputs.passed || 'true' }}" + SECRETS_PASSED="${{ steps.scan-secrets.outputs.passed || 'true' }}" + + ALL_PASSED="true" + + echo "📊 Quality Gates Results:" + echo " - Python: $PYTHON_PASSED" + echo " - Markdown: $MARKDOWN_PASSED" + echo " - Bash: $BASH_PASSED" + echo " - Secrets: $SECRETS_PASSED" + echo "" + + if [[ "$PYTHON_PASSED" == "false" ]] || [[ "$MARKDOWN_PASSED" == "false" ]] || \ + [[ "$BASH_PASSED" == "false" ]] || [[ "$SECRETS_PASSED" == "false" ]]; then + ALL_PASSED="false" + echo "❌ Some quality gates failed" + else + echo "✅ All quality gates passed" + fi + + echo "all-passed=$ALL_PASSED" >> $GITHUB_OUTPUT + echo "::endgroup::" diff --git a/.github/actions/rate-limit-check/action.yml b/.github/actions/rate-limit-check/action.yml new file mode 100644 index 0000000..fc97845 --- /dev/null +++ b/.github/actions/rate-limit-check/action.yml @@ -0,0 +1,138 @@ +name: 'Rate Limit Check' +description: 'Circuit breaker to prevent GitHub API exhaustion' +author: 'ClaudeForge' + +branding: + icon: 'activity' + color: 'orange' + +inputs: + github-token: + description: 'GitHub token for API access (usually secrets.GITHUB_TOKEN)' + required: true + minimum-remaining: + description: 'Minimum API calls remaining to proceed (default: 50)' + required: false + default: '50' + fail-on-limit: + description: 'Whether to fail the workflow if below threshold (default: false, just warns)' + required: false + default: 'false' + +outputs: + can-proceed: + description: 'Boolean indicating if enough API calls remain (true/false)' + value: ${{ steps.check-limit.outputs.can-proceed }} + remaining: + description: 'Number of API calls remaining in current window' + value: ${{ steps.check-limit.outputs.remaining }} + limit: + description: 'Total API rate limit for the token' + value: ${{ steps.check-limit.outputs.limit }} + used: + description: 'Number of API calls used in current window' + value: ${{ steps.check-limit.outputs.used }} + reset-time: + description: 'Unix timestamp when rate limit resets' + value: ${{ steps.check-limit.outputs.reset-time }} + reset-time-human: + description: 'Human-readable time when rate limit resets' + value: ${{ steps.check-limit.outputs.reset-time-human }} + +runs: + using: 'composite' + steps: + - name: Check GitHub API Rate Limit + id: check-limit + shell: bash + env: + GITHUB_TOKEN: ${{ inputs.github-token }} + MIN_REMAINING: ${{ inputs.minimum-remaining }} + FAIL_ON_LIMIT: ${{ inputs.fail-on-limit }} + run: | + echo "::group::GitHub API Rate Limit Check" + + # Query rate limit status + echo "🔍 Querying GitHub API rate limit status..." + RATE_LIMIT_JSON=$(gh api rate_limit) + + # Extract core API rate limit info + REMAINING=$(echo "$RATE_LIMIT_JSON" | jq -r '.resources.core.remaining') + LIMIT=$(echo "$RATE_LIMIT_JSON" | jq -r '.resources.core.limit') + USED=$(echo "$RATE_LIMIT_JSON" | jq -r '.resources.core.used') + RESET_TIMESTAMP=$(echo "$RATE_LIMIT_JSON" | jq -r '.resources.core.reset') + + # Convert reset timestamp to human-readable format + if [[ "$OSTYPE" == "darwin"* ]]; then + # macOS date command + RESET_TIME_HUMAN=$(date -r "$RESET_TIMESTAMP" '+%Y-%m-%d %H:%M:%S %Z') + else + # Linux date command + RESET_TIME_HUMAN=$(date -d "@$RESET_TIMESTAMP" '+%Y-%m-%d %H:%M:%S %Z') + fi + + # Calculate percentage used + PERCENT_USED=$(echo "scale=1; ($USED * 100) / $LIMIT" | bc) + + # Determine if we can proceed + CAN_PROCEED="false" + if [[ $REMAINING -ge $MIN_REMAINING ]]; then + CAN_PROCEED="true" + fi + + # Set outputs + echo "can-proceed=$CAN_PROCEED" >> $GITHUB_OUTPUT + echo "remaining=$REMAINING" >> $GITHUB_OUTPUT + echo "limit=$LIMIT" >> $GITHUB_OUTPUT + echo "used=$USED" >> $GITHUB_OUTPUT + echo "reset-time=$RESET_TIMESTAMP" >> $GITHUB_OUTPUT + echo "reset-time-human=$RESET_TIME_HUMAN" >> $GITHUB_OUTPUT + + # Display results + echo "" + echo "📊 Rate Limit Status:" + echo " - Limit: $LIMIT" + echo " - Used: $USED ($PERCENT_USED%)" + echo " - Remaining: $REMAINING" + echo " - Minimum Required: $MIN_REMAINING" + echo " - Reset Time: $RESET_TIME_HUMAN" + echo "" + + # Status indicators + if [[ $CAN_PROCEED == "true" ]]; then + echo "✅ Sufficient API calls remaining ($REMAINING >= $MIN_REMAINING)" + else + echo "âš ī¸ Rate limit too low ($REMAINING < $MIN_REMAINING)" + echo "🕐 API limit will reset at: $RESET_TIME_HUMAN" + + # Calculate wait time + CURRENT_TIME=$(date +%s) + WAIT_SECONDS=$((RESET_TIMESTAMP - CURRENT_TIME)) + WAIT_MINUTES=$((WAIT_SECONDS / 60)) + + if [[ $WAIT_SECONDS -gt 0 ]]; then + echo "âąī¸ Wait time: ~$WAIT_MINUTES minutes" + else + echo "âąī¸ Rate limit should reset momentarily" + fi + fi + + echo "::endgroup::" + + # Handle warnings and failures + if [[ $CAN_PROCEED == "false" ]]; then + if [[ $FAIL_ON_LIMIT == "true" ]]; then + echo "::error::GitHub API rate limit too low ($REMAINING remaining, need $MIN_REMAINING). Failing workflow." + exit 1 + else + echo "::warning::GitHub API rate limit too low ($REMAINING remaining, need $MIN_REMAINING). Workflow may fail or be rate-limited." + fi + fi + + - name: Rate Limit Summary + shell: bash + if: steps.check-limit.outputs.can-proceed == 'false' + run: | + echo "::notice::âš ī¸ Low API Rate Limit Detected" + echo "::notice::Remaining: ${{ steps.check-limit.outputs.remaining }} / ${{ steps.check-limit.outputs.limit }}" + echo "::notice::Resets at: ${{ steps.check-limit.outputs.reset-time-human }}" diff --git a/.github/actions/setup-python-deps/action.yml b/.github/actions/setup-python-deps/action.yml new file mode 100644 index 0000000..85cfc6c --- /dev/null +++ b/.github/actions/setup-python-deps/action.yml @@ -0,0 +1,62 @@ +name: 'Setup Python Dependencies' +description: 'Sets up Python with caching for faster workflow runs' +author: 'ClaudeForge' + +inputs: + python-version: + description: 'Python version to use' + required: false + default: '3.11' + +outputs: + cache-hit: + description: 'Whether the cache was hit' + value: ${{ steps.cache-pip.outputs.cache-hit }} + python-version: + description: 'Python version that was installed' + value: ${{ steps.setup-python.outputs.python-version }} + +runs: + using: 'composite' + steps: + - name: Set up Python + id: setup-python + uses: actions/setup-python@v5 + with: + python-version: ${{ inputs.python-version }} + + - name: Get pip cache dir + id: pip-cache + shell: bash + run: echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT + + - name: Cache pip dependencies + id: cache-pip + uses: actions/cache@v4 + with: + path: ${{ steps.pip-cache.outputs.dir }} + key: ${{ runner.os }}-pip-${{ inputs.python-version }}-${{ hashFiles('skill/requirements.txt', '**/setup.py', '**/pyproject.toml') }} + restore-keys: | + ${{ runner.os }}-pip-${{ inputs.python-version }}- + ${{ runner.os }}-pip- + + - name: Install Python dependencies + shell: bash + run: | + python -m pip install --upgrade pip + # Install common validation tools + pip install flake8 pylint black mypy + # Install project dependencies if they exist + if [ -f "skill/requirements.txt" ]; then + pip install -r skill/requirements.txt + fi + if [ -f "requirements.txt" ]; then + pip install -r requirements.txt + fi + + - name: Display Python info + shell: bash + run: | + echo "Python version: $(python --version)" + echo "Pip version: $(pip --version)" + echo "Cache hit: ${{ steps.cache-pip.outputs.cache-hit }}" diff --git a/.github/workflows/bootstrap.yml b/.github/workflows/bootstrap.yml new file mode 100644 index 0000000..74672fc --- /dev/null +++ b/.github/workflows/bootstrap.yml @@ -0,0 +1,190 @@ +name: 'Bootstrap Repository' + +on: + workflow_dispatch: + inputs: + create-labels: + description: 'Create standard labels' + required: false + default: 'true' + type: boolean + create-milestones: + description: 'Create initial milestones' + required: false + default: 'true' + type: boolean + validate-settings: + description: 'Validate repository settings' + required: false + default: 'true' + type: boolean + +permissions: + contents: read + issues: write + pull-requests: write + +jobs: + bootstrap: + name: Setup Repository + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Rate limit check + uses: ./.github/actions/rate-limit-check + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + minimum-remaining: 100 + + - name: Create standard labels + if: inputs.create-labels == true + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + echo "::group::Creating Labels" + + # Type labels + gh label create "bug" --description "Something isn't working" --color "d73a4a" --force + gh label create "enhancement" --description "New feature or request" --color "a2eeef" --force + gh label create "documentation" --description "Improvements or additions to documentation" --color "0075ca" --force + gh label create "refactor" --description "Code refactoring" --color "fbca04" --force + gh label create "performance" --description "Performance improvements" --color "00ff00" --force + gh label create "security" --description "Security issues or improvements" --color "ee0701" --force + gh label create "test" --description "Testing related" --color "1d76db" --force + + # Priority labels + gh label create "priority: critical" --description "Critical priority" --color "b60205" --force + gh label create "priority: high" --description "High priority" --color "d93f0b" --force + gh label create "priority: medium" --description "Medium priority" --color "fbca04" --force + gh label create "priority: low" --description "Low priority" --color "0e8a16" --force + + # Status labels + gh label create "status: blocked" --description "Blocked by another issue" --color "d93f0b" --force + gh label create "status: in progress" --description "Work in progress" --color "0052cc" --force + gh label create "status: review needed" --description "Needs review" --color "fbca04" --force + gh label create "status: needs discussion" --description "Needs team discussion" --color "d876e3" --force + + # Component labels + gh label create "component: installer" --description "Installation scripts" --color "5319e7" --force + gh label create "component: skill" --description "Python skill modules" --color "5319e7" --force + gh label create "component: command" --description "Slash commands" --color "5319e7" --force + gh label create "component: agent" --description "Guardian agent" --color "5319e7" --force + gh label create "component: docs" --description "Documentation" --color "5319e7" --force + gh label create "component: ci/cd" --description "CI/CD workflows" --color "5319e7" --force + + # Additional labels + gh label create "good first issue" --description "Good for newcomers" --color "7057ff" --force + gh label create "help wanted" --description "Extra attention is needed" --color "008672" --force + gh label create "dependencies" --description "Dependency updates" --color "0366d6" --force + gh label create "breaking change" --description "Breaking change" --color "ee0701" --force + + echo "✅ Labels created successfully" + echo "::endgroup::" + + - name: Create milestones + if: inputs.create-milestones == true + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + echo "::group::Creating Milestones" + + # Get current date for due dates + CURRENT_DATE=$(date -u +%Y-%m-%d) + + # Calculate due dates (approximate) + V1_1_DUE=$(date -u -d "+1 month" +%Y-%m-%dT23:59:59Z 2>/dev/null || date -u -v+1m +%Y-%m-%dT23:59:59Z) + V1_2_DUE=$(date -u -d "+2 months" +%Y-%m-%dT23:59:59Z 2>/dev/null || date -u -v+2m +%Y-%m-%dT23:59:59Z) + V2_0_DUE=$(date -u -d "+4 months" +%Y-%m-%dT23:59:59Z 2>/dev/null || date -u -v+4m +%Y-%m-%dT23:59:59Z) + + # Create milestones (using gh api since gh doesn't have milestone create command) + gh api repos/${{ github.repository }}/milestones \ + --method POST \ + --field title="v1.1.0" \ + --field description="Additional templates, enhanced detection, granular quality scoring" \ + --field due_on="$V1_1_DUE" || echo "Milestone v1.1.0 may already exist" + + gh api repos/${{ github.repository }}/milestones \ + --method POST \ + --field title="v1.2.0" \ + --field description="VS Code extension, GitHub Actions enhancements, advanced quality hooks" \ + --field due_on="$V1_2_DUE" || echo "Milestone v1.2.0 may already exist" + + gh api repos/${{ github.repository }}/milestones \ + --method POST \ + --field title="v2.0.0" \ + --field description="AI-powered suggestions, multi-language support, web dashboard, plugin system" \ + --field due_on="$V2_0_DUE" || echo "Milestone v2.0.0 may already exist" + + echo "✅ Milestones created successfully" + echo "::endgroup::" + + - name: Validate repository settings + if: inputs.validate-settings == true + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + echo "::group::Validating Settings" + + # Get repository info + REPO_INFO=$(gh api repos/${{ github.repository }}) + + # Check important settings + HAS_ISSUES=$(echo "$REPO_INFO" | jq -r '.has_issues') + HAS_WIKI=$(echo "$REPO_INFO" | jq -r '.has_wiki') + HAS_DISCUSSIONS=$(echo "$REPO_INFO" | jq -r '.has_discussions') + + echo "📊 Repository Settings:" + echo " - Issues: $HAS_ISSUES" + echo " - Wiki: $HAS_WIKI" + echo " - Discussions: $HAS_DISCUSSIONS" + echo "" + + if [ "$HAS_ISSUES" != "true" ]; then + echo "::warning::Issues are not enabled. Consider enabling them in Settings > General > Features." + fi + + if [ "$HAS_DISCUSSIONS" != "true" ]; then + echo "::notice::Discussions are not enabled. Consider enabling them for community Q&A." + fi + + # Check if default branch is 'main' + DEFAULT_BRANCH=$(echo "$REPO_INFO" | jq -r '.default_branch') + echo " - Default Branch: $DEFAULT_BRANCH" + + if [ "$DEFAULT_BRANCH" != "main" ] && [ "$DEFAULT_BRANCH" != "dev" ]; then + echo "::warning::Default branch is '$DEFAULT_BRANCH'. Consider using 'main' or 'dev'." + fi + + echo "::endgroup::" + + - name: Bootstrap summary + run: | + echo "## 🎉 Repository Bootstrap Complete!" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Actions Performed" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [[ "${{ inputs.create-labels }}" == "true" ]]; then + echo "- ✅ Created 23 standard labels" >> $GITHUB_STEP_SUMMARY + fi + + if [[ "${{ inputs.create-milestones }}" == "true" ]]; then + echo "- ✅ Created 3 milestones (v1.1.0, v1.2.0, v2.0.0)" >> $GITHUB_STEP_SUMMARY + fi + + if [[ "${{ inputs.validate-settings }}" == "true" ]]; then + echo "- ✅ Validated repository settings" >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Next Steps" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "1. Create \`dev\` branch from \`main\`" >> $GITHUB_STEP_SUMMARY + echo "2. Configure branch protection rules" >> $GITHUB_STEP_SUMMARY + echo "3. Set \`dev\` as default branch for PRs" >> $GITHUB_STEP_SUMMARY + echo "4. Review and adjust labels/milestones as needed" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "🔗 See [GITHUB_WORKFLOWS.md](docs/GITHUB_WORKFLOWS.md) for complete setup guide" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/dev-to-main.yml b/.github/workflows/dev-to-main.yml new file mode 100644 index 0000000..6bc964c --- /dev/null +++ b/.github/workflows/dev-to-main.yml @@ -0,0 +1,322 @@ +name: 'PR Dev to Main (Release Gate)' + +on: + pull_request: + types: [opened, reopened, synchronize, ready_for_review] + branches: + - main + +permissions: + contents: read + pull-requests: write + +jobs: + validate-release-pr: + name: Validate Release PR + runs-on: ubuntu-latest + if: github.event.pull_request.draft == false + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Full history for changelog validation + + - name: Fork safety check + id: fork-check + uses: ./.github/actions/fork-safety + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Validate source branch + id: validate-branch + run: | + SOURCE_BRANCH="${{ github.head_ref }}" + echo "Source branch: $SOURCE_BRANCH" + + # Only allow specific branches to merge to main + ALLOWED_PATTERNS="^(dev|release/.*|dependabot/.*)$" + + if [[ "$SOURCE_BRANCH" =~ $ALLOWED_PATTERNS ]]; then + echo "✅ Source branch is allowed to merge to main" + echo "valid=true" >> $GITHUB_OUTPUT + else + echo "::error::Only 'dev', 'release/*', or 'dependabot/*' branches can merge to main" + echo "::error::Current branch: $SOURCE_BRANCH" + echo "::error::Please merge to 'dev' first, then create a PR from 'dev' to 'main'" + echo "valid=false" >> $GITHUB_OUTPUT + exit 1 + fi + + - name: Check CHANGELOG.md updated + id: check-changelog + run: | + echo "Checking if CHANGELOG.md was updated..." + + # Check if CHANGELOG.md exists + if [ ! -f "CHANGELOG.md" ]; then + echo "::warning::CHANGELOG.md not found" + echo "updated=false" >> $GITHUB_OUTPUT + exit 0 + fi + + # Check if CHANGELOG.md was modified in this PR + CHANGED_FILES=$(git diff --name-only origin/main...HEAD) + + if echo "$CHANGED_FILES" | grep -q "CHANGELOG.md"; then + echo "✅ CHANGELOG.md was updated" + echo "updated=true" >> $GITHUB_OUTPUT + else + echo "::warning::CHANGELOG.md was not updated in this PR" + echo "::warning::Consider adding release notes to CHANGELOG.md" + echo "updated=false" >> $GITHUB_OUTPUT + fi + + - name: Validate version consistency + id: check-version + run: | + echo "Checking version consistency across files..." + + # Extract version from CHANGELOG.md if it exists + if [ -f "CHANGELOG.md" ]; then + CHANGELOG_VERSION=$(grep -m 1 "^## \[" CHANGELOG.md | sed -n 's/.*\[\(.*\)\].*/\1/p' || echo "unknown") + echo "CHANGELOG.md version: $CHANGELOG_VERSION" + else + CHANGELOG_VERSION="unknown" + fi + + # Extract version from install scripts if they contain version info + if grep -q "v1\." install.sh 2>/dev/null; then + INSTALLER_VERSION=$(grep -o "v[0-9]\+\.[0-9]\+\.[0-9]\+" install.sh | head -1 || echo "unknown") + echo "Installer version: $INSTALLER_VERSION" + + if [ "$CHANGELOG_VERSION" != "unknown" ] && [ "$INSTALLER_VERSION" != "unknown" ]; then + if [ "v$CHANGELOG_VERSION" != "$INSTALLER_VERSION" ] && [ "$CHANGELOG_VERSION" != "$INSTALLER_VERSION" ]; then + echo "::warning::Version mismatch between CHANGELOG.md ($CHANGELOG_VERSION) and installer ($INSTALLER_VERSION)" + else + echo "✅ Version consistency validated" + fi + fi + fi + + echo "consistent=true" >> $GITHUB_OUTPUT + + - name: Production readiness checklist + run: | + echo "## 🚀 Production Readiness Checklist" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Check | Status |" >> $GITHUB_STEP_SUMMARY + echo "|-------|--------|" >> $GITHUB_STEP_SUMMARY + + # Source branch check + if [[ "${{ steps.validate-branch.outputs.valid }}" == "true" ]]; then + echo "| Source Branch | ✅ Valid |" >> $GITHUB_STEP_SUMMARY + else + echo "| Source Branch | ❌ Invalid |" >> $GITHUB_STEP_SUMMARY + fi + + # CHANGELOG check + if [[ "${{ steps.check-changelog.outputs.updated }}" == "true" ]]; then + echo "| CHANGELOG.md | ✅ Updated |" >> $GITHUB_STEP_SUMMARY + else + echo "| CHANGELOG.md | âš ī¸ Not Updated |" >> $GITHUB_STEP_SUMMARY + fi + + # Version check + if [[ "${{ steps.check-version.outputs.consistent }}" == "true" ]]; then + echo "| Version Consistency | ✅ Consistent |" >> $GITHUB_STEP_SUMMARY + else + echo "| Version Consistency | âš ī¸ Check Needed |" >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + + - name: Comment on branch validation failure + if: failure() && steps.validate-branch.outputs.valid != 'true' && steps.fork-check.outputs.should-skip-writes != 'true' + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const comment = `## ❌ Invalid Source Branch for Main + + Only the following branches can merge to \`main\`: + - \`dev\` (standard release flow) + - \`release/*\` (release branches) + - \`dependabot/*\` (dependency updates) + + **Current branch**: \`${{ github.head_ref }}\` + + ### How to Fix + + If this is a feature or fix branch: + 1. Close this PR + 2. Create a PR to \`dev\` instead + 3. After merging to \`dev\`, create a PR from \`dev\` to \`main\` + + If this is an emergency hotfix: + 1. Create a \`release/x.x.x-hotfix\` branch + 2. Make your changes there + 3. Create PR from release branch to \`main\` + + 📚 See [BRANCHING_STRATEGY.md](../blob/main/docs/BRANCHING_STRATEGY.md) for details.`; + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: comment + }); + + quality-checks: + name: Run Quality Checks + needs: validate-release-pr + uses: ./.github/workflows/reusable-pr-checks.yml + with: + python-version: '3.11' + skip-python: false + skip-markdown: false + skip-bash: false + skip-secrets: false + + production-build: + name: Validate Production Build + needs: [validate-release-pr, quality-checks] + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Validate installation scripts + run: | + echo "::group::Installation Script Validation" + + # Check install.sh + if [ -f "install.sh" ]; then + echo "✅ install.sh exists" + + # Check if executable + if [ -x "install.sh" ]; then + echo "✅ install.sh is executable" + else + echo "::warning::install.sh is not executable (chmod +x needed)" + fi + + # Validate syntax + if bash -n install.sh; then + echo "✅ install.sh syntax valid" + else + echo "::error::install.sh has syntax errors" + exit 1 + fi + else + echo "::error::install.sh not found" + exit 1 + fi + + # Check install.ps1 + if [ -f "install.ps1" ]; then + echo "✅ install.ps1 exists" + else + echo "::warning::install.ps1 not found" + fi + + echo "::endgroup::" + + - name: Validate skill modules structure + run: | + echo "::group::Skill Modules Validation" + + REQUIRED_FILES=( + "skill/analyzer.py" + "skill/validator.py" + "skill/generator.py" + "skill/template_selector.py" + "skill/workflow.py" + ) + + ALL_EXIST=true + for file in "${REQUIRED_FILES[@]}"; do + if [ -f "$file" ]; then + echo "✅ $file exists" + else + echo "::error::$file not found" + ALL_EXIST=false + fi + done + + if [ "$ALL_EXIST" = false ]; then + exit 1 + fi + + echo "::endgroup::" + + - name: Validate documentation + run: | + echo "::group::Documentation Validation" + + REQUIRED_DOCS=( + "README.md" + "CHANGELOG.md" + "LICENSE" + "docs/INSTALLATION.md" + "docs/QUICK_START.md" + ) + + ALL_EXIST=true + for doc in "${REQUIRED_DOCS[@]}"; do + if [ -f "$doc" ]; then + echo "✅ $doc exists" + else + echo "::warning::$doc not found" + fi + done + + echo "::endgroup::" + + release-summary: + name: Release Summary + needs: [validate-release-pr, quality-checks, production-build] + runs-on: ubuntu-latest + if: always() + + steps: + - name: Generate summary + run: | + echo "## 🚀 Release Gate Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Validation Results" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [[ "${{ needs.validate-release-pr.result }}" == "success" ]]; then + echo "- ✅ Release PR validated" >> $GITHUB_STEP_SUMMARY + else + echo "- ❌ Release PR validation failed" >> $GITHUB_STEP_SUMMARY + fi + + if [[ "${{ needs.quality-checks.result }}" == "success" ]]; then + echo "- ✅ Quality checks passed" >> $GITHUB_STEP_SUMMARY + else + echo "- ❌ Quality checks failed" >> $GITHUB_STEP_SUMMARY + fi + + if [[ "${{ needs.production-build.result }}" == "success" ]]; then + echo "- ✅ Production build validated" >> $GITHUB_STEP_SUMMARY + else + echo "- ❌ Production build validation failed" >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + + if [[ "${{ needs.validate-release-pr.result }}" == "success" ]] && \ + [[ "${{ needs.quality-checks.result }}" == "success" ]] && \ + [[ "${{ needs.production-build.result }}" == "success" ]]; then + echo "### ✅ All release gates passed! Ready for production." >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "After merging, consider:" >> $GITHUB_STEP_SUMMARY + echo "1. Creating a GitHub release with `/release` command" >> $GITHUB_STEP_SUMMARY + echo "2. Updating documentation as needed" >> $GITHUB_STEP_SUMMARY + echo "3. Announcing the release to users" >> $GITHUB_STEP_SUMMARY + else + echo "### ❌ Some release gates failed. Please review and fix before merging." >> $GITHUB_STEP_SUMMARY + fi diff --git a/.github/workflows/pr-into-dev.yml b/.github/workflows/pr-into-dev.yml new file mode 100644 index 0000000..e0a5765 --- /dev/null +++ b/.github/workflows/pr-into-dev.yml @@ -0,0 +1,181 @@ +name: 'PR into Dev' + +on: + pull_request: + types: [opened, reopened, synchronize, ready_for_review] + branches: + - dev + +permissions: + contents: read + pull-requests: write + issues: read + +jobs: + validate-pr: + name: Validate PR Structure + runs-on: ubuntu-latest + if: github.event.pull_request.draft == false + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Fork safety check + id: fork-check + uses: ./.github/actions/fork-safety + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Validate branch name + id: branch-name + run: | + BRANCH_NAME="${{ github.head_ref }}" + echo "Branch name: $BRANCH_NAME" + + # Valid prefixes for dev branch + VALID_PREFIXES="feature/ fix/ hotfix/ test/ refactor/ docs/" + + VALID=false + for prefix in $VALID_PREFIXES; do + if [[ "$BRANCH_NAME" == $prefix* ]]; then + VALID=true + echo "✅ Branch name is valid (starts with $prefix)" + break + fi + done + + if [ "$VALID" = false ]; then + echo "::error::Invalid branch name: $BRANCH_NAME" + echo "::error::Branch must start with one of: $VALID_PREFIXES" + echo "valid=false" >> $GITHUB_OUTPUT + exit 1 + else + echo "valid=true" >> $GITHUB_OUTPUT + fi + + - name: Validate PR title (Conventional Commits) + id: pr-title + run: | + PR_TITLE="${{ github.event.pull_request.title }}" + echo "PR Title: $PR_TITLE" + + # Conventional commit format: type(scope): subject + # Valid types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert + if [[ ! "$PR_TITLE" =~ ^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\(.+\))?:\ .+ ]]; then + echo "::error::PR title does not follow Conventional Commits format" + echo "::error::Format: type(scope): subject" + echo "::error::Example: feat(installer): add Windows PowerShell support" + echo "::error::Valid types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert" + echo "valid=false" >> $GITHUB_OUTPUT + exit 1 + else + echo "✅ PR title follows Conventional Commits format" + echo "valid=true" >> $GITHUB_OUTPUT + fi + + - name: Check for linked issues + id: linked-issues + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + PR_NUMBER="${{ github.event.pull_request.number }}" + PR_BODY="${{ github.event.pull_request.body }}" + + echo "Checking for linked issues in PR #$PR_NUMBER" + + # Check for keywords like "Closes", "Fixes", "Resolves", "Relates to" + ISSUE_KEYWORDS="Closes|Fixes|Resolves|Relates to|Ref|References" + + if echo "$PR_BODY" | grep -qiE "($ISSUE_KEYWORDS) #[0-9]+"; then + echo "✅ Found linked issue(s) in PR body" + echo "has-linked-issues=true" >> $GITHUB_OUTPUT + else + echo "::warning::No linked issues found in PR body" + echo "::warning::Please link at least one issue using keywords: Closes #123, Fixes #456, etc." + echo "has-linked-issues=false" >> $GITHUB_OUTPUT + # Not failing, just warning + fi + + - name: Comment on validation failure + if: failure() && steps.fork-check.outputs.should-skip-writes != 'true' + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const branchValid = '${{ steps.branch-name.outputs.valid }}'; + const titleValid = '${{ steps.pr-title.outputs.valid }}'; + + let comment = '## ❌ PR Validation Failed\n\n'; + + if (branchValid !== 'true') { + comment += '### Branch Name\n'; + comment += '- ❌ Branch name must start with: `feature/`, `fix/`, `hotfix/`, `test/`, `refactor/`, or `docs/`\n'; + comment += `- Current branch: \`${{ github.head_ref }}\`\n\n`; + } + + if (titleValid !== 'true') { + comment += '### PR Title\n'; + comment += '- ❌ PR title must follow Conventional Commits format\n'; + comment += '- Format: `type(scope): subject`\n'; + comment += '- Example: `feat(installer): add Windows PowerShell support`\n'; + comment += '- Valid types: `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `build`, `ci`, `chore`, `revert`\n\n'; + } + + comment += '### How to Fix\n\n'; + comment += '1. Rename your branch if needed: `git branch -m new-branch-name`\n'; + comment += '2. Update PR title to follow Conventional Commits format\n'; + comment += '3. Push changes and re-run checks\n\n'; + comment += '📚 See [CONTRIBUTING.md](../blob/main/docs/CONTRIBUTING.md) for more details.'; + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: comment + }); + + quality-checks: + name: Run Quality Checks + needs: validate-pr + uses: ./.github/workflows/reusable-pr-checks.yml + with: + python-version: '3.11' + skip-python: false + skip-markdown: false + skip-bash: false + skip-secrets: false + + pr-summary: + name: PR Summary + needs: [validate-pr, quality-checks] + runs-on: ubuntu-latest + if: always() + + steps: + - name: Generate summary + run: | + echo "## 📋 PR into Dev - Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Validation Results" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [[ "${{ needs.validate-pr.result }}" == "success" ]]; then + echo "- ✅ PR structure validated" >> $GITHUB_STEP_SUMMARY + else + echo "- ❌ PR structure validation failed" >> $GITHUB_STEP_SUMMARY + fi + + if [[ "${{ needs.quality-checks.result }}" == "success" ]]; then + echo "- ✅ Quality checks passed" >> $GITHUB_STEP_SUMMARY + else + echo "- ❌ Quality checks failed" >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + + if [[ "${{ needs.validate-pr.result }}" == "success" ]] && [[ "${{ needs.quality-checks.result }}" == "success" ]]; then + echo "### ✅ All checks passed! PR is ready for review." >> $GITHUB_STEP_SUMMARY + else + echo "### ❌ Some checks failed. Please review and fix before merging." >> $GITHUB_STEP_SUMMARY + fi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..ac13d5a --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,270 @@ +name: 'Create Release' + +on: + workflow_dispatch: + inputs: + version: + description: 'Release version (e.g., 1.1.0)' + required: true + type: string + prerelease: + description: 'Mark as pre-release' + required: false + default: false + type: boolean + draft: + description: 'Create as draft' + required: false + default: false + type: boolean + +permissions: + contents: write + pull-requests: read + issues: write + +jobs: + validate-release: + name: Validate Release + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Validate version format + id: validate-version + run: | + VERSION="${{ inputs.version }}" + echo "Validating version: $VERSION" + + # Check semantic versioning format + if [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]]; then + echo "✅ Version format is valid" + echo "valid=true" >> $GITHUB_OUTPUT + else + echo "::error::Invalid version format: $VERSION" + echo "::error::Must be semantic versioning (e.g., 1.1.0 or 1.1.0-beta.1)" + echo "valid=false" >> $GITHUB_OUTPUT + exit 1 + fi + + - name: Check if tag already exists + id: check-tag + run: | + VERSION="${{ inputs.version }}" + TAG="v$VERSION" + + if git rev-parse "$TAG" >/dev/null 2>&1; then + echo "::error::Tag $TAG already exists" + echo "::error::Please use a different version number" + echo "exists=true" >> $GITHUB_OUTPUT + exit 1 + else + echo "✅ Tag $TAG is available" + echo "exists=false" >> $GITHUB_OUTPUT + fi + + - name: Validate CHANGELOG.md + id: check-changelog + run: | + VERSION="${{ inputs.version }}" + + if [ ! -f "CHANGELOG.md" ]; then + echo "::warning::CHANGELOG.md not found" + echo "updated=false" >> $GITHUB_OUTPUT + exit 0 + fi + + # Check if version is mentioned in CHANGELOG + if grep -q "\[$VERSION\]" CHANGELOG.md || grep -q "## $VERSION" CHANGELOG.md; then + echo "✅ Version $VERSION found in CHANGELOG.md" + echo "updated=true" >> $GITHUB_OUTPUT + else + echo "::warning::Version $VERSION not found in CHANGELOG.md" + echo "::warning::Consider adding release notes before creating release" + echo "updated=false" >> $GITHUB_OUTPUT + fi + + create-release: + name: Create GitHub Release + needs: validate-release + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Rate limit check + uses: ./.github/actions/rate-limit-check + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + minimum-remaining: 100 + + - name: Extract release notes from CHANGELOG + id: extract-notes + run: | + VERSION="${{ inputs.version }}" + + if [ ! -f "CHANGELOG.md" ]; then + echo "CHANGELOG.md not found, using default release notes" + NOTES="Release v$VERSION + +See the full changelog at https://github.com/${{ github.repository }}/blob/main/CHANGELOG.md" + else + # Try to extract notes for this version + # Look for section between [VERSION] and next [VERSION] or end of file + NOTES=$(awk "/\[$VERSION\]|## $VERSION/,/^## \[|^## [0-9]/" CHANGELOG.md | tail -n +2 | head -n -1) + + if [ -z "$NOTES" ]; then + echo "No specific notes found for $VERSION in CHANGELOG.md" + NOTES="Release v$VERSION + +See the full changelog at https://github.com/${{ github.repository }}/blob/main/CHANGELOG.md" + else + echo "✅ Extracted release notes from CHANGELOG.md" + fi + fi + + # Save to file for GitHub release + echo "$NOTES" > /tmp/release-notes.md + echo "notes-file=/tmp/release-notes.md" >> $GITHUB_OUTPUT + + - name: Get commits since last release + id: get-commits + run: | + # Get the last release tag + LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") + + if [ -z "$LAST_TAG" ]; then + echo "No previous tags found, getting all commits" + COMMITS=$(git log --pretty=format:"- %s (%h)" --no-merges) + else + echo "Getting commits since $LAST_TAG" + COMMITS=$(git log ${LAST_TAG}..HEAD --pretty=format:"- %s (%h)" --no-merges) + fi + + echo "$COMMITS" > /tmp/commits.txt + echo "commits-file=/tmp/commits.txt" >> $GITHUB_OUTPUT + + - name: Create release notes + run: | + VERSION="${{ inputs.version }}" + TAG="v$VERSION" + + # Combine CHANGELOG notes with commit list + cat /tmp/release-notes.md > /tmp/final-notes.md + + echo "" >> /tmp/final-notes.md + echo "---" >> /tmp/final-notes.md + echo "" >> /tmp/final-notes.md + echo "## đŸ“Ļ Installation" >> /tmp/final-notes.md + echo "" >> /tmp/final-notes.md + echo "### One-Line Install (Recommended)" >> /tmp/final-notes.md + echo '```bash' >> /tmp/final-notes.md + echo "curl -fsSL https://raw.githubusercontent.com/${{ github.repository }}/main/install.sh | bash" >> /tmp/final-notes.md + echo '```' >> /tmp/final-notes.md + echo "" >> /tmp/final-notes.md + echo "### Manual Install" >> /tmp/final-notes.md + echo '```bash' >> /tmp/final-notes.md + echo "wget https://github.com/${{ github.repository }}/archive/refs/tags/$TAG.tar.gz" >> /tmp/final-notes.md + echo "tar -xzf $TAG.tar.gz" >> /tmp/final-notes.md + echo "cd ClaudeForge-${VERSION}" >> /tmp/final-notes.md + echo "./install.sh" >> /tmp/final-notes.md + echo '```' >> /tmp/final-notes.md + echo "" >> /tmp/final-notes.md + echo "📚 **Documentation**: https://github.com/${{ github.repository }}/blob/main/docs/INSTALLATION.md" >> /tmp/final-notes.md + echo "" >> /tmp/final-notes.md + + if [ -s /tmp/commits.txt ]; then + echo "## 📝 Commits" >> /tmp/final-notes.md + echo "" >> /tmp/final-notes.md + cat /tmp/commits.txt >> /tmp/final-notes.md + echo "" >> /tmp/final-notes.md + fi + + echo "---" >> /tmp/final-notes.md + echo "" >> /tmp/final-notes.md + echo "**Full Changelog**: https://github.com/${{ github.repository }}/blob/main/CHANGELOG.md" >> /tmp/final-notes.md + + - name: Create GitHub Release + id: create-release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + VERSION="${{ inputs.version }}" + TAG="v$VERSION" + DRAFT_FLAG="" + PRERELEASE_FLAG="" + + if [[ "${{ inputs.draft }}" == "true" ]]; then + DRAFT_FLAG="--draft" + fi + + if [[ "${{ inputs.prerelease }}" == "true" ]]; then + PRERELEASE_FLAG="--prerelease" + fi + + gh release create "$TAG" \ + --title "ClaudeForge $TAG" \ + --notes-file /tmp/final-notes.md \ + $DRAFT_FLAG \ + $PRERELEASE_FLAG + + echo "✅ Release $TAG created successfully" + echo "tag=$TAG" >> $GITHUB_OUTPUT + + - name: Update installation script references + if: inputs.prerelease == false && inputs.draft == false + run: | + VERSION="${{ inputs.version }}" + echo "::notice::Consider updating installer script to reference $VERSION" + echo "::notice::Files to update: install.sh, install.ps1 (if they contain version references)" + + release-summary: + name: Release Summary + needs: [validate-release, create-release] + runs-on: ubuntu-latest + if: always() + + steps: + - name: Generate summary + run: | + VERSION="${{ inputs.version }}" + TAG="v$VERSION" + + echo "## 🎉 Release Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [[ "${{ needs.validate-release.result }}" == "success" ]] && \ + [[ "${{ needs.create-release.result }}" == "success" ]]; then + echo "### ✅ Release $TAG Created Successfully!" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "🔗 **Release URL**: https://github.com/${{ github.repository }}/releases/tag/$TAG" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Next Steps" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "1. ✅ Release created on GitHub" >> $GITHUB_STEP_SUMMARY + + if [[ "${{ inputs.draft }}" == "true" ]]; then + echo "2. âš ī¸ Release is in **DRAFT** mode - publish when ready" >> $GITHUB_STEP_SUMMARY + fi + + if [[ "${{ inputs.prerelease }}" == "true" ]]; then + echo "3. âš ī¸ Marked as **PRE-RELEASE**" >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "4. Consider announcing the release:" >> $GITHUB_STEP_SUMMARY + echo " - Update README.md badges if needed" >> $GITHUB_STEP_SUMMARY + echo " - Post announcement in Discussions" >> $GITHUB_STEP_SUMMARY + echo " - Share on social media if applicable" >> $GITHUB_STEP_SUMMARY + else + echo "### ❌ Release Creation Failed" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Please check the workflow logs for details." >> $GITHUB_STEP_SUMMARY + fi diff --git a/.github/workflows/reusable-pr-checks.yml b/.github/workflows/reusable-pr-checks.yml new file mode 100644 index 0000000..b9f5511 --- /dev/null +++ b/.github/workflows/reusable-pr-checks.yml @@ -0,0 +1,127 @@ +name: 'Reusable PR Quality Checks' + +on: + workflow_call: + inputs: + python-version: + description: 'Python version to use' + required: false + default: '3.11' + type: string + skip-python: + description: 'Skip Python validation' + required: false + default: false + type: boolean + skip-markdown: + description: 'Skip Markdown validation' + required: false + default: false + type: boolean + skip-bash: + description: 'Skip Bash validation' + required: false + default: false + type: boolean + skip-secrets: + description: 'Skip secret scanning' + required: false + default: false + type: boolean + +permissions: + contents: read + pull-requests: write + +jobs: + quality-gates: + name: Quality Gates + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Fork safety check + id: fork-check + uses: ./.github/actions/fork-safety + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Rate limit check + uses: ./.github/actions/rate-limit-check + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + minimum-remaining: 50 + + - name: Run quality gates + id: quality + uses: ./.github/actions/quality-gates + with: + python-version: ${{ inputs.python-version }} + skip-python: ${{ inputs.skip-python }} + skip-markdown: ${{ inputs.skip-markdown }} + skip-bash: ${{ inputs.skip-bash }} + skip-secrets: ${{ inputs.skip-secrets }} + + - name: Quality check summary + run: | + echo "## 🔍 Quality Gates Results" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + PYTHON_PASSED="${{ steps.quality.outputs.python-passed }}" + MARKDOWN_PASSED="${{ steps.quality.outputs.markdown-passed }}" + BASH_PASSED="${{ steps.quality.outputs.bash-passed }}" + SECRETS_PASSED="${{ steps.quality.outputs.secrets-passed }}" + ALL_PASSED="${{ steps.quality.outputs.all-passed }}" + + echo "| Check | Status |" >> $GITHUB_STEP_SUMMARY + echo "|-------|--------|" >> $GITHUB_STEP_SUMMARY + + if [[ "${{ inputs.skip-python }}" != "true" ]]; then + if [[ "$PYTHON_PASSED" == "true" ]]; then + echo "| Python Syntax | ✅ Passed |" >> $GITHUB_STEP_SUMMARY + else + echo "| Python Syntax | ❌ Failed |" >> $GITHUB_STEP_SUMMARY + fi + fi + + if [[ "${{ inputs.skip-markdown }}" != "true" ]]; then + if [[ "$MARKDOWN_PASSED" == "true" ]]; then + echo "| Markdown Lint | ✅ Passed |" >> $GITHUB_STEP_SUMMARY + else + echo "| Markdown Lint | ❌ Failed |" >> $GITHUB_STEP_SUMMARY + fi + fi + + if [[ "${{ inputs.skip-bash }}" != "true" ]]; then + if [[ "$BASH_PASSED" == "true" ]]; then + echo "| Bash Scripts | ✅ Passed |" >> $GITHUB_STEP_SUMMARY + else + echo "| Bash Scripts | ❌ Failed |" >> $GITHUB_STEP_SUMMARY + fi + fi + + if [[ "${{ inputs.skip-secrets }}" != "true" ]]; then + if [[ "$SECRETS_PASSED" == "true" ]]; then + echo "| Secret Scan | ✅ Passed |" >> $GITHUB_STEP_SUMMARY + else + echo "| Secret Scan | âš ī¸ Warnings |" >> $GITHUB_STEP_SUMMARY + fi + fi + + echo "" >> $GITHUB_STEP_SUMMARY + + if [[ "$ALL_PASSED" == "true" ]]; then + echo "### ✅ All quality gates passed!" >> $GITHUB_STEP_SUMMARY + else + echo "### ❌ Some quality gates failed" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Please review the errors above and fix them before merging." >> $GITHUB_STEP_SUMMARY + fi + + - name: Fail if quality gates failed + if: steps.quality.outputs.all-passed != 'true' + run: | + echo "::error::Quality gates failed. Please review and fix the issues." + exit 1