Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "python3 $CLAUDE_PROJECT_DIR/tools/hooks/ai/block-dangerous-commands.py"
}
]
}
]
}
}
13 changes: 13 additions & 0 deletions .config/pyproject_template/settings.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[project]
project_name = "bastproxy"
package_name = "bastproxy"
pypi_name = "bastproxy"
description = "A MUD proxy with plugin support for Python 3.12+"
author_name = "Bast"
author_email = "bast@bastproxy.com"
github_user = "endavis"
github_repo = "bastproxy-py3"

[template]
commit = "2c1171b97183a76fe8415d408432bf344081507c"
commit_date = "2026-01-23"
4 changes: 4 additions & 0 deletions .github/python-versions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"oldest": "3.12",
"newest": "3.13"
}
154 changes: 154 additions & 0 deletions .github/workflows/breaking-change-detection.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
name: Breaking Change Detection

on:
pull_request:
types: [opened, synchronize, edited]

permissions:
contents: read
issues: write
pull-requests: write

jobs:
detect-breaking-changes:
name: Detect API Breaking Changes
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0 # Need full history for comparison

- name: Fetch base branch
run: git fetch origin ${{ github.base_ref }}

- uses: actions/setup-python@v6
with:
python-version: "3.12"

- name: Install griffe
run: pip install griffe

- name: Check for breaking changes
id: griffe
run: |
set +e
OUTPUT=$(griffe check bastproxy \
--against "origin/${{ github.base_ref }}" \
--search src \
--verbose 2>&1)
EXIT_CODE=$?
set -e

echo "$OUTPUT"

if [ $EXIT_CODE -ne 0 ] && [ -n "$OUTPUT" ]; then
echo "has_breaking=true" >> "$GITHUB_OUTPUT"
else
echo "has_breaking=false" >> "$GITHUB_OUTPUT"
fi

# Save output for comment step (handle multiline)
{
echo "details<<EOF"
echo "$OUTPUT"
echo "EOF"
} >> "$GITHUB_OUTPUT"

- name: Report breaking changes
if: always()
uses: actions/github-script@v8
env:
HAS_BREAKING: ${{ steps.griffe.outputs.has_breaking }}
GRIFFE_OUTPUT: ${{ steps.griffe.outputs.details }}
with:
script: |
const pr = context.payload.pull_request;
const body = pr.body || '';
const hasBreaking = process.env.HAS_BREAKING === 'true';
const griffeOutput = process.env.GRIFFE_OUTPUT || '';

// Check if BREAKING CHANGE is documented
const hasBreakingChangeDoc = /BREAKING CHANGE:/i.test(body) ||
/breaking change/i.test(body);

// Find existing bot comment
const comments = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number
});

const marker = '<!-- breaking-change-detection -->';
const botComment = comments.data.find(c =>
c.body.includes(marker)
);

if (!hasBreaking) {
// No breaking changes - remove old comment if it exists
if (botComment) {
await github.rest.issues.deleteComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id
});
}
console.log('No breaking changes detected');
return;
}

// Build report
let report = `${marker}\n`;
report += '## API Breaking Changes Detected\n\n';
report += 'The following breaking changes were detected by [griffe](https://mkdocstrings.github.io/griffe/):\n\n';
report += '```\n';
report += griffeOutput.trim();
report += '\n```\n\n';

if (!hasBreakingChangeDoc) {
report += '### Required Actions\n\n';
report += 'Breaking changes detected but not documented!\n\n';
report += '**You must:**\n';
report += '1. Add `BREAKING CHANGE:` footer to your commit message\n';
report += '2. Document the breaking change in the PR description\n';
report += '3. Add migration guide to CHANGELOG.md\n';
report += '4. Update documentation\n\n';
report += '**Commit message format:**\n';
report += '```\n';
report += 'feat: description of change\n\n';
report += 'BREAKING CHANGE: describe what broke and how to migrate.\n';
report += '```\n';
} else {
report += '### Breaking Change Documented\n\n';
report += 'This PR includes breaking change documentation.\n\n';
report += '**Before merging, verify:**\n';
report += '- [ ] Migration guide in CHANGELOG.md\n';
report += '- [ ] Documentation updated\n';
report += '- [ ] Version will be bumped appropriately (major version)\n';
}

// Create or update comment
if (botComment) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
body: report
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
body: report
});
}

// Fail if not documented
if (!hasBreakingChangeDoc) {
core.setFailed(
'Breaking changes detected but not documented! ' +
'Add BREAKING CHANGE: footer to commit message and document in PR description.'
);
} else {
console.log('Breaking changes properly documented');
}
130 changes: 113 additions & 17 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,55 +2,151 @@ name: CI

on:
push:
branches: [main, develop]
branches: [main]
pull_request:
branches: [main, develop]
branches: [main]
types: [opened, synchronize, reopened, labeled]
workflow_dispatch:
workflow_call:

permissions:
contents: read

jobs:
test:
setup:
# Skip CI if triggered by a label event that isn't 'full-matrix'
if: github.event.action != 'labeled' || github.event.label.name == 'full-matrix'
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- uses: actions/checkout@v6
with:
sparse-checkout: .github/python-versions.json
sparse-checkout-cone-mode: false

- name: Set matrix based on config and label
id: set-matrix
run: |
# Read config
oldest=$(jq -r '.oldest' .github/python-versions.json)
newest=$(jq -r '.newest' .github/python-versions.json)

oldest_minor=${oldest#3.}
newest_minor=${newest#3.}

if [[ "${{ github.event.label.name }}" == "full-matrix" ]]; then
# Middle versions only (bookends already tested)
versions=""
for ((i=oldest_minor+1; i<newest_minor; i++)); do
if [[ -n "$versions" ]]; then
versions="$versions,"
fi
versions="$versions\"3.$i\""
done

if [[ -z "$versions" ]]; then
echo "No middle versions to test (oldest=$oldest, newest=$newest)"
echo 'matrix={"include":[]}' >> $GITHUB_OUTPUT
else
echo "Middle versions: $versions"
echo "matrix={\"os\":[\"ubuntu-latest\"],\"python-version\":[$versions]}" >> $GITHUB_OUTPUT
fi
else
# Bookend versions (oldest + newest)
echo "Bookend versions: $oldest, $newest"
echo "matrix={\"os\":[\"ubuntu-latest\"],\"python-version\":[\"$oldest\",\"$newest\"]}" >> $GITHUB_OUTPUT
fi

test:
needs: setup
if: ${{ needs.setup.outputs.matrix != '{"include":[]}' }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
python-version: ["3.12", "3.13"]
fail-fast: false
matrix: ${{ fromJson(needs.setup.outputs.matrix) }}
env:
UV_CACHE_DIR: /tmp/uv-cache
UV_CACHE_DIR: ${{ github.workspace }}/.uv-cache
BASTPROXY_HOME: ${{ github.workspace }}/tmp/bastproxy_home

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6

- name: Create BASTPROXY_HOME directory
run: mkdir -p $BASTPROXY_HOME

- uses: actions/setup-python@v5
- uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}

- uses: astral-sh/setup-uv@v1
- uses: astral-sh/setup-uv@v7
with:
version: "latest"

- name: Cache uv dependencies
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: /tmp/uv-cache
path: ${{ env.UV_CACHE_DIR }}
key: ${{ runner.os }}-uv-${{ hashFiles('uv.lock') }}
restore-keys: |
${{ runner.os }}-uv-

- name: Install dependencies
run: uv run doit dev
run: uv sync --all-extras --dev

- name: Run checks
run: uv run doit check
- name: Check code formatting
run: uv run ruff format --check src/ tests/

- name: Run coverage
- name: Run linting
run: uv run ruff check src/ tests/

- name: Run type checking
run: uv run mypy src/

- name: Run security scan
run: uv run bandit -c pyproject.toml -r src/

- name: Run spell check
run: uv run codespell src/ tests/ docs/ README.md

- name: Run tests with coverage
run: uv run pytest --cov=bastproxy --cov-report=xml:tmp/coverage.xml --cov-report=term -v

- name: Upload coverage to Codecov
if: matrix.python-version == '3.12'
uses: codecov/codecov-action@v4
if: matrix.python-version == '3.12' && matrix.os == 'ubuntu-latest'
uses: codecov/codecov-action@v5
with:
file: ./tmp/coverage.xml
flags: unittests
name: codecov-umbrella
fail_ci_if_error: false
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

ci-complete:
# Always run unless this is a non-full-matrix label event
if: always() && (github.event.action != 'labeled' || github.event.label.name == 'full-matrix')
needs: [setup, test]
runs-on: ubuntu-latest
steps:
- name: Check CI status
run: |
setup_result="${{ needs.setup.result }}"
test_result="${{ needs.test.result }}"

echo "Setup result: $setup_result"
echo "Test result: $test_result"

# Setup must succeed
if [[ "$setup_result" != "success" ]]; then
echo "Setup job failed"
exit 1
fi

# Test must succeed or be skipped (skipped = no middle versions)
if [[ "$test_result" != "success" && "$test_result" != "skipped" ]]; then
echo "Test job failed"
exit 1
fi

echo "CI completed successfully"
21 changes: 21 additions & 0 deletions .github/workflows/merge-gate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: Merge Gate

on:
pull_request:
branches: [main]
types: [opened, labeled, unlabeled, synchronize, reopened]

jobs:
require-label:
permissions:
contents: read
runs-on: ubuntu-latest
steps:
- name: Check for ready-to-merge label
run: |
if [[ "${{ contains(github.event.pull_request.labels.*.name, 'ready-to-merge') }}" != "true" ]]; then
echo "::error::PR requires 'ready-to-merge' label before merging"
echo "Add the label when the PR is reviewed and ready to merge."
exit 1
fi
echo "ready-to-merge label present"
Loading
Loading