Skip to content

fix: add missing __future__ imports for Python 3.9 compat (bulk_init,… #54

fix: add missing __future__ imports for Python 3.9 compat (bulk_init,…

fix: add missing __future__ imports for Python 3.9 compat (bulk_init,… #54

Workflow file for this run

name: pyqual CI with GitHub Integration
on:
push:
branches: [main, master]
pull_request:
branches: [main, master]
issues:
types: [opened, edited, labeled]
workflow_dispatch:
inputs:
issue_number:
description: 'Issue number to process (optional)'
required: false
type: string
jobs:
quality-loop:
runs-on: ubuntu-latest
permissions:
contents: write
issues: write
pull-requests: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Install tools
run: |
pip install -e .
npm install -g @anthropic-ai/claude-code || true
# Install GitHub CLI
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg
sudo chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null
sudo apt update && sudo apt install gh -y
- name: Fetch GitHub issues as tasks
id: fetch-tasks
run: |
echo "::group::Fetching GitHub issues"
python3 -m pyqual.github_tasks --fetch-issues --label "pyqual-fix" --output .pyqual/github_tasks.json || true
echo "::endgroup::"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Process triggering issue
if: github.event_name == 'issues' || (github.event_name == 'workflow_dispatch' && github.event.inputs.issue_number != '')
id: process-issue
run: |
echo "::group::Processing triggering issue"
python3 << 'EOF'
import json
import os
from pathlib import Path
# Get issue number from event or workflow_dispatch input
issue_number = os.environ.get("ISSUE_NUMBER") or os.environ.get("INPUT_ISSUE_NUMBER")
issue_title = os.environ.get("ISSUE_TITLE", "Unknown")
issue_body = os.environ.get("ISSUE_BODY", "")
if not issue_number:
print("No issue number provided, skipping issue processing")
sys.exit(0)
print(f"Processing issue #{issue_number}: {issue_title[:60]}...")
# Read existing TODO.md if present (preserves prefact-generated content)
todo_path = Path("TODO.md")
existing_content = ""
if todo_path.exists():
existing_content = todo_path.read_text()
print("Preserved existing TODO.md content from prefact")
# Create GitHub issue section
github_section = f"# GitHub Issue #{issue_number}\n\n"
github_section += f"- [ ] #{issue_number}: {issue_title}\n\n"
github_section += "### Issue Body\n"
todo_body = issue_body[:500] + "..." if len(issue_body) > 500 else issue_body
github_section += todo_body + "\n\n---\n*Auto-generated from GitHub issue event*\n"
# Combine: GitHub issue first, then existing prefact content
if existing_content:
todo_content = github_section + "\n" + existing_content
else:
todo_content = github_section
todo_path.write_text(todo_content)
print(f"Created TODO.md with issue #{issue_number} + {len(existing_content)} bytes existing content")
# Save issue metadata for later
meta_path = Path(".pyqual/triggering_issue.json")
meta_path.parent.mkdir(parents=True, exist_ok=True)
with open(meta_path, "w") as f:
json.dump({
"number": int(issue_number),
"title": issue_title,
"body": issue_body[:1000],
"action": os.environ.get("ISSUE_ACTION", "opened")
}, f, indent=2)
print(f"Saved issue metadata to {meta_path}")
EOF
echo "::endgroup::"
env:
ISSUE_NUMBER: ${{ github.event.issue.number }}
ISSUE_TITLE: ${{ github.event.issue.title }}
ISSUE_BODY: ${{ github.event.issue.body }}
ISSUE_ACTION: ${{ github.event.action }}
- name: Process issue with Claude Code
if: github.event_name == 'issues' || (github.event_name == 'workflow_dispatch' && github.event.inputs.issue_number != '')
continue-on-error: true
id: process-claude
run: |
echo "::group::Processing issue with Claude Code"
export PATH="$HOME/.local/bin:$PATH"
# Get issue details
ISSUE_NUM="${{ github.event.issue.number || github.event.inputs.issue_number }}"
ISSUE_TITLE="${{ github.event.issue.title }}"
ISSUE_BODY="${{ github.event.issue.body }}"
echo "Processing issue #$ISSUE_NUM: $ISSUE_TITLE"
if [[ -z "${ANTHROPIC_API_KEY:-}" ]]; then
echo "ANTHROPIC_API_KEY not set; skipping Claude Code processing"
echo "changes_made=false" >> "$GITHUB_OUTPUT"
else
# Create prompt for Claude Code
PROMPT=$(cat << 'PROMPT_EOF'
You are given a GitHub issue stored in `.pyqual/triggering_issue.json`.
Read that file before making any changes so you use the exact issue title, body, and number.
Instructions:
1. Analyze the issue requirements carefully.
2. Make the necessary code/documentation changes.
3. Ensure all quality gates still pass after changes.
4. Do not commit - just apply changes.
Apply changes directly to the repository files.
PROMPT_EOF
)
# Run Claude Code with the prompt
if timeout 600 claude -p "$PROMPT" \
--model sonnet \
--allowedTools "Edit,Read,Write,Bash(git diff),Bash(python),Bash(pytest -x)" \
--output-format text; then
echo "βœ… Claude Code successfully processed the issue"
echo "changes_made=true" >> $GITHUB_OUTPUT
else
rc=$?
echo "⚠️ Claude Code exited with code $rc"
echo "changes_made=false" >> $GITHUB_OUTPUT
fi
fi
# Check if changes were made
if [ -n "$(git status --porcelain)" ]; then
echo "Changes detected:"
git status --short
else
echo "No changes made"
fi
echo "::endgroup::"
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Run quality gate loop
id: pyqual-run
continue-on-error: true
run: |
echo "::group::Running pyqual"
pyqual run --config pyqual.yaml 2>&1 | tee .pyqual/pyqual_output.log
exit_code=${PIPESTATUS[0]}
echo "exit_code=$exit_code" >> $GITHUB_OUTPUT
exit $exit_code
env:
LLM_MODEL: ${{ secrets.LLM_MODEL || 'openrouter/qwen/qwen3-coder-next' }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PYQUAL_MAX_TODOS: "3"
- name: Analyze failure and post comment
if: steps.pyqual-run.outputs.exit_code != '0' && github.event_name == 'pull_request'
run: |
echo "::group::Analyzing failure"
python3 << 'EOF'
import os
import sys
from pathlib import Path
from datetime import datetime
sys.path.insert(0, '.')
from pyqual.github_actions import GitHubActionsReporter
from pyqual.pipeline import PipelineError
reporter = GitHubActionsReporter()
if not reporter.is_running_in_github_actions():
print("Not in GitHub Actions, skipping comment")
sys.exit(0)
# Read pyqual output
log_file = Path(".pyqual/pyqual_output.log")
logs = log_file.read_text() if log_file.exists() else "No logs available"
# Determine failure stage from logs
stage = "unknown"
error_msg = "Pipeline failed"
suggestions = []
if "claude_fix" in logs and ("timeout" in logs.lower() or "rate limit" in logs.lower()):
stage = "claude_fix"
error_msg = "Claude Code fix stage timed out or hit rate limit"
suggestions = [
"Try running with fewer TODO items: `PYQUAL_MAX_TODOS=3 pyqual run`",
"Check ANTHROPIC_API_KEY is valid and has quota",
"Consider using llx fallback instead of Claude Code"
]
elif "test" in logs and "failed" in logs.lower():
stage = "test"
error_msg = "Tests are failing"
suggestions = [
"Run tests locally: `pytest -xvs` to see detailed output",
"Check if test failures are related to your changes",
"Update tests if the behavior change is intentional"
]
elif "prefact" in logs:
stage = "prefact"
error_msg = "Prefact analysis found issues"
suggestions = [
"Review prefact output in .pyqual/prefact.json",
"Fix code quality issues manually or use `pyqual run` to auto-fix",
"Some issues may be false positives - review carefully"
]
report = reporter.generate_failure_report(
stage_name=stage,
error=error_msg,
logs=logs,
suggestions=suggestions
)
success = reporter.post_pr_comment(report)
print(f"Posted comment: {success}")
# Create or find GitHub issue for tracking
issue_title = f"[CI Fail] Stage '{stage}' failed in PR #{pr_number}"
issue_body = "## CI Failure Report\n\n"
issue_body += f"**Stage:** `{stage}`\n"
issue_body += f"**Error:** {error_msg}\n"
issue_body += f"**PR:** #{pr_number}\n"
issue_body += f"**Timestamp:** {timestamp}\n\n"
issue_body += "### Logs\n<details>\n<summary>Click to expand</summary>\n\n```\n"
issue_body += logs[:2000]
issue_body += "\n```\n</details>\n\n"
issue_body += "### Next Steps\n"
issue_body += "- [ ] Review the failure logs above\n"
issue_body += "- [ ] Fix the underlying issue\n"
issue_body += "- [ ] Re-run pyqual CI\n\n"
issue_body += "---\n_Auto-generated by pyqual CI_"
issue_num = reporter.ensure_issue_exists(
title=issue_title,
body=issue_body,
labels=["ci-failure", "pyqual-fix"]
)
if issue_num:
print(f"GitHub issue for tracking: #{issue_num}")
# Add ticket to TODO.md
todo_path = Path("TODO.md")
pr_number = os.environ.get("PR_NUMBER", "unknown")
timestamp = datetime.now().strftime("%Y-%m-%d")
todo_entry = f"\n- [ ] [CI Fail] Stage '{stage}' failed in PR #{pr_number} - {error_msg} ({timestamp})\n"
if todo_path.exists():
content = todo_path.read_text()
if "## CI Failures" not in content:
content += "\n\n## CI Failures\n\n"
content += todo_entry
else:
content = f"# TODO\n\n## CI Failures\n{todo_entry}"
todo_path.write_text(content)
print(f"Added ticket to TODO.md: {todo_entry.strip()}")
EOF
echo "::endgroup::"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
- name: Update GitHub issue on failure
if: steps.pyqual-run.outputs.exit_code != '0' && github.event_name == 'issues'
run: |
python3 << 'EOF'
import os
import sys
import json
from pathlib import Path
sys.path.insert(0, '.')
from pyqual.github_actions import GitHubActionsReporter
reporter = GitHubActionsReporter()
issue_number = os.environ.get("GITHUB_ISSUE_NUMBER")
if not issue_number:
print("No issue number found")
sys.exit(0)
log_file = Path(".pyqual/pyqual_output.log")
logs = log_file.read_text() if log_file.exists() else "No logs available"
body = "## ❌ Pyqual failed to process this issue\n\n"
body += "The automated fix attempt failed. Please check the issue details and try again.\n\n"
body += "### Error Output\n<details>\n<summary>Logs</summary>\n\n```\n"
body += logs[:2000]
body += "\n```\n</details>\n\n"
body += "### Next Steps\n"
body += "1. Review the issue description for clarity\n"
body += "2. Ensure the issue has the `pyqual-fix` label\n"
body += "3. Run pyqual locally: `pyqual run --max 5`\n"
body += "4. Check pipeline logs in GitHub Actions\n\n"
body += "---\n"
sha = os.environ.get('GITHUB_SHA', 'unknown')[:8]
body += f"_Failed at: {sha}_"
reporter.post_issue_comment(body, int(issue_number))
# Add to TODO.md for tracking
todo_path = Path("TODO.md")
if todo_path.exists():
content = todo_path.read_text()
if "[FAILED]" not in content:
content += f"\n- [ ] [FAILED] Issue #{issue_number} - pyqual failed to fix\n"
todo_path.write_text(content)
EOF
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_ISSUE_NUMBER: ${{ github.event.issue.number }}
- name: Update GitHub issue on success
if: (steps.pyqual-run.outputs.exit_code == '0' && github.event_name == 'issues') || (steps.pyqual-run.outputs.exit_code == '0' && github.event_name == 'workflow_dispatch' && github.event.inputs.issue_number != '')
run: |
python3 << 'EOF'
import os
import sys
from pathlib import Path
sys.path.insert(0, '.')
from pyqual.github_actions import GitHubActionsReporter
from pyqual.gates import GateSet
from pyqual.config import PyqualConfig
reporter = GitHubActionsReporter()
# Get issue number from issues event or workflow_dispatch input
issue_number = os.environ.get("GITHUB_ISSUE_NUMBER") or os.environ.get("INPUT_ISSUE_NUMBER")
print(f"DEBUG: GITHUB_ISSUE_NUMBER = {os.environ.get('GITHUB_ISSUE_NUMBER')}")
print(f"DEBUG: INPUT_ISSUE_NUMBER = {os.environ.get('INPUT_ISSUE_NUMBER')}")
print(f"DEBUG: Final issue_number = {issue_number}")
print(f"DEBUG: Reporter token available = {bool(reporter.token)}")
print(f"DEBUG: Reporter repo = {reporter.repo}")
if not issue_number:
print("No issue number found - skipping comment and close")
sys.exit(0)
print(f"Processing issue #{issue_number}")
# Calculate completion percentage using pyqual gates
try:
config = PyqualConfig.load()
gate_set = GateSet(config.gates)
completion_pct = gate_set.completion_percentage(Path("."))
print(f"πŸ“Š Ticket completion: {completion_pct:.1f}%")
except Exception as e:
print(f"Could not calculate completion percentage: {e}")
completion_pct = None
# Parse pyqual output for a simple success signal.
log_file = Path(".pyqual/pyqual_output.log")
pyqual_output = log_file.read_text() if log_file.exists() else ""
# Check if all gates passed.
all_gates_passed = "all_gates_passed: true" in pyqual_output or "result: all_gates_passed" in pyqual_output
# The workflow only reaches this step when `pyqual run` exited 0.
# Use the successful pipeline result as the close signal.
can_close = all_gates_passed
print("Quality Gates Check:")
print(f" all_gates_passed: {all_gates_passed}")
print(f" can_close_issue: {can_close}")
# Check if changes were made.
import subprocess
result = subprocess.run(["git", "status", "--porcelain"], capture_output=True, text=True)
changes = result.stdout.strip()
if can_close:
sha = os.environ.get('GITHUB_SHA', 'unknown')[:8]
pct_str = f"{completion_pct:.1f}%" if completion_pct else "N/A"
if changes:
body = "## βœ… Task completed successfully\n\n"
body += f"**Completion: {pct_str}**\n\n"
body += "All quality gates passed and changes have been made.\n\n"
body += "Changes have been made and pushed to the repository.\n\n"
body += "### Files Modified\n<details>\n<summary>Click to see changes</summary>\n\n```\n"
body += changes[:2000]
body += f"\n```\n</details>\n\n"
body += "---\n"
body += "_Closing this issue as all requirements are met βœ“_"
else:
body = "## βœ… Task completed successfully\n\n"
body += f"**Completion: {pct_str}**\n\n"
body += "The pipeline ran successfully and all quality gates passed.\n\n"
body += "---\n"
body += "_Closing this issue as requirements are already met βœ“_"
# Post success comment.
reporter.post_issue_comment(body, int(issue_number))
# Close the issue.
if reporter.token and reporter.repo:
import subprocess
close_cmd = [
"gh", "issue", "close", str(issue_number),
"--repo", reporter.repo,
"--comment", "Auto-closed: All quality gates passed (coverage β‰₯55%, CC ≀15, VALLM β‰₯90%)"
]
result = subprocess.run(
close_cmd,
capture_output=True, text=True,
env={**os.environ, "GH_TOKEN": reporter.token}
)
if result.returncode == 0:
print(f"βœ“ Closed issue #{issue_number}")
else:
print(f"βœ— Failed to close issue: {result.stderr[:200]}")
else:
# Don't close - post status update with completion percentage.
sha = os.environ.get('GITHUB_SHA', 'unknown')[:8]
pct_str = f"{completion_pct:.1f}%" if completion_pct else "N/A"
body = f"## πŸ“Š Quality Gates Status - {pct_str} Complete\n\n"
body += "Pipeline completed but not all requirements met:\n\n"
body += f"- **all_gates_passed:** {'βœ“' if all_gates_passed else 'βœ—'}\n"
body += "- **pipeline:** completed, but manual review required\n\n"
body += "Issue remains open - manual review required.\n\n"
body += f"---\n_Processed at: {sha}_"
reporter.post_issue_comment(body, int(issue_number))
print(f"βœ— Issue #{issue_number} remains open - quality gates not fully met")
# Update TODO.md to mark as done
todo_path = Path("TODO.md")
if todo_path.exists():
content = todo_path.read_text()
if f"#{issue_number}:" in content:
content = content.replace(f"- [ ] #{issue_number}:", f"- [x] #{issue_number}:")
todo_path.write_text(content)
print(f"Marked issue #{issue_number} as completed in TODO.md")
EOF
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_ISSUE_NUMBER: ${{ github.event.issue.number }}
INPUT_ISSUE_NUMBER: ${{ github.event.inputs.issue_number }}
- name: Push changes if successful
if: steps.pyqual-run.outputs.exit_code == '0'
run: |
if [ -n "$(git status --porcelain)" ]; then
git config user.email "pyqual-bot@semcod.github.io"
git config user.name "Pyqual Bot"
git add -A
git commit -m "fix: pyqual auto-fix [ci skip]" || true
git push origin HEAD || true
fi
- name: Close related GitHub issues on push/PR success
if: steps.pyqual-run.outputs.exit_code == '0' && (github.event_name == 'push' || github.event_name == 'pull_request')
run: |
echo "::group::Closing related GitHub issues"
python3 << 'EOF'
import os
import sys
import re
import subprocess
from pathlib import Path
sys.path.insert(0, '.')
from pyqual.github_actions import GitHubActionsReporter
reporter = GitHubActionsReporter()
if not reporter.token or not reporter.repo:
print("GITHUB_TOKEN or GITHUB_REPOSITORY not set - skipping issue closure")
sys.exit(0)
# Parse pyqual output for metrics
log_file = Path(".pyqual/pyqual_output.log")
pyqual_output = log_file.read_text() if log_file.exists() else ""
# Check if all gates passed
all_gates_passed = "all_gates_passed: true" in pyqual_output or "result: all_gates_passed" in pyqual_output
if not all_gates_passed:
print("Quality gates did not pass - not auto-closing issues")
sys.exit(0)
# Get changed files to find related issues
try:
result = subprocess.run(
["git", "log", "-1", "--pretty=format:%s%n%b"],
capture_output=True, text=True, timeout=10
)
commit_msg = result.stdout
except Exception:
commit_msg = ""
# Look for issue references in commit message (e.g., "Fixes #123", "Closes #456")
issue_refs = re.findall(r'(?:fixes?|closes?|resolves?)\s+#(\d+)', commit_msg, re.IGNORECASE)
# Also check TODO.md for marked-as-done items
todo_path = Path("TODO.md")
if todo_path.exists():
content = todo_path.read_text()
# Find checked items with issue references: "- [x] #123: ..."
done_issues = re.findall(r'- \[x\] #(\d+):', content)
issue_refs.extend(done_issues)
# Deduplicate
issue_refs = list(set(issue_refs))
if not issue_refs:
print("No related GitHub issues found to close")
sys.exit(0)
print(f"Found {len(issue_refs)} issue(s) to potentially close: {issue_refs}")
closed_count = 0
for issue_num in issue_refs:
print(f" Processing issue #{issue_num}...")
# Check if issue is still open
check_result = subprocess.run(
["gh", "issue", "view", issue_num, "--repo", reporter.repo, "--json", "state"],
capture_output=True, text=True, timeout=30,
env={**os.environ, "GH_TOKEN": reporter.token}
)
if check_result.returncode != 0:
print(f" Issue #{issue_num} not found or error checking status")
continue
try:
import json
issue_data = json.loads(check_result.stdout)
if issue_data.get("state") != "open":
print(f" Issue #{issue_num} already closed")
continue
except Exception:
pass
# Post success comment
body = "## βœ… Automatically closed via pyqual CI\n\n"
body += "All quality gates passed on the associated changes.\n\n"
body += f"- **Commit:** {os.environ.get('GITHUB_SHA', 'unknown')[:8]}\n"
body += "---\n_Auto-closed by pyqual push/PR workflow_"
reporter.post_issue_comment(body, int(issue_num))
# Close the issue
close_result = subprocess.run(
["gh", "issue", "close", issue_num, "--repo", reporter.repo,
"--comment", "Auto-closed: All quality gates passed (push/PR workflow)"],
capture_output=True, text=True, timeout=30,
env={**os.environ, "GH_TOKEN": reporter.token}
)
if close_result.returncode == 0:
print(f" βœ… Closed issue #{issue_num}")
closed_count += 1
else:
print(f" ⚠️ Failed to close issue #{issue_num}: {close_result.stderr[:100]}")
print(f"\nClosed {closed_count} issue(s)")
EOF
echo "::endgroup::"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: pyqual-metrics
path: .pyqual/
if-no-files-found: ignore
- name: Fail job if pyqual failed
if: steps.pyqual-run.outputs.exit_code != '0'
run: |
echo "::error::Pyqual pipeline failed - see logs and PR comments for details"
exit 1