mirror of
https://github.com/alirezarezvani/ClaudeForge.git
synced 2026-07-03 02:13:15 -04:00
feat(ci): implement comprehensive CI/CD workflows and quality gates
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)
This commit is contained in:
@@ -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
|
||||||
@@ -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::"
|
||||||
@@ -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 }}"
|
||||||
@@ -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 }}"
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user