-
Notifications
You must be signed in to change notification settings - Fork 0
feat: implement self-healing Python deployment patterns and runtime modules #13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
4cac5d1
d0e4956
c28a5bb
7bf2edb
65926b2
7482e0d
94f4ae3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,78 @@ | ||||||||||||||||||||||||||||||||||||
| name: CI/CD Pipeline | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| on: | ||||||||||||||||||||||||||||||||||||
| push: | ||||||||||||||||||||||||||||||||||||
| branches: | ||||||||||||||||||||||||||||||||||||
| - main | ||||||||||||||||||||||||||||||||||||
| pull_request: | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| concurrency: | ||||||||||||||||||||||||||||||||||||
| group: ${{ github.workflow }}-${{ github.ref }} | ||||||||||||||||||||||||||||||||||||
| cancel-in-progress: true | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| jobs: | ||||||||||||||||||||||||||||||||||||
| test: | ||||||||||||||||||||||||||||||||||||
| runs-on: ubuntu-latest | ||||||||||||||||||||||||||||||||||||
| steps: | ||||||||||||||||||||||||||||||||||||
| - uses: actions/checkout@v4 | ||||||||||||||||||||||||||||||||||||
| env: | ||||||||||||||||||||||||||||||||||||
| FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| - name: Set up Python | ||||||||||||||||||||||||||||||||||||
| uses: actions/setup-python@v5 | ||||||||||||||||||||||||||||||||||||
| env: | ||||||||||||||||||||||||||||||||||||
| FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true | ||||||||||||||||||||||||||||||||||||
| with: | ||||||||||||||||||||||||||||||||||||
| python-version: "3.10" | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| - name: Install dependencies | ||||||||||||||||||||||||||||||||||||
| run: | | ||||||||||||||||||||||||||||||||||||
| python -m pip install --upgrade pip | ||||||||||||||||||||||||||||||||||||
| pip install ruff mypy pytest | ||||||||||||||||||||||||||||||||||||
| if [ -f pyproject.toml ]; then pip install -e .[dev]; fi | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| - name: Lint with ruff | ||||||||||||||||||||||||||||||||||||
| run: ruff check . | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| - name: Typecheck with mypy | ||||||||||||||||||||||||||||||||||||
| run: mypy src/selfheal/ | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| - name: Test with pytest | ||||||||||||||||||||||||||||||||||||
| run: pytest tests/ | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| deploy: | ||||||||||||||||||||||||||||||||||||
| needs: test | ||||||||||||||||||||||||||||||||||||
| if: github.ref == 'refs/heads/main' | ||||||||||||||||||||||||||||||||||||
| runs-on: ubuntu-latest | ||||||||||||||||||||||||||||||||||||
| steps: | ||||||||||||||||||||||||||||||||||||
| - uses: actions/checkout@v4 | ||||||||||||||||||||||||||||||||||||
| env: | ||||||||||||||||||||||||||||||||||||
| FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| - name: Deploy to PaaS | ||||||||||||||||||||||||||||||||||||
| run: | | ||||||||||||||||||||||||||||||||||||
| echo "Deploying to PaaS..." | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| - name: Health Check | ||||||||||||||||||||||||||||||||||||
| id: health_check | ||||||||||||||||||||||||||||||||||||
| uses: jtalk/url-health-check-action@v5 | ||||||||||||||||||||||||||||||||||||
| with: | ||||||||||||||||||||||||||||||||||||
| url: "https://your-production-url.com/health" | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
+56
to
+60
|
||||||||||||||||||||||||||||||||||||
| - name: Health Check | |
| id: health_check | |
| uses: jtalk/url-health-check-action@v5 | |
| with: | |
| url: "https://your-production-url.com/health" | |
| - name: Validate health check URL | |
| run: | | |
| if [ -z "${{ vars.PRODUCTION_HEALTH_URL }}" ]; then | |
| echo "::error::Repository or environment variable PRODUCTION_HEALTH_URL must be set to the deployed application's health endpoint." | |
| exit 1 | |
| fi | |
| - name: Health Check | |
| id: health_check | |
| uses: jtalk/url-health-check-action@v5 | |
| with: | |
| url: "${{ vars.PRODUCTION_HEALTH_URL }}" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Switch rollback API call to update the existing ref
The rollback step calls gh api with --method POST against /git/refs/heads/main, but this path is for updating an existing ref, which GitHub expects as a PATCH request; POST is for creating refs at /git/refs. In a failed health check scenario this request will error instead of resetting main to github.event.before, so the workflow reports rollback but leaves the bad deploy in place.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@copilot, make changes based on the above suggestion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@copilot, make changes based on the above suggestion.
Copilot
AI
Apr 23, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The rollback gh api call uses POST /git/refs/heads/main, which creates refs; updating main typically requires PATCH /git/refs/heads/main (or /git/refs/{ref}) with sha/force. As written this will likely fail and leave deployments un-rolled back.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| name: Self-Healing Workflow | ||
|
|
||
| on: | ||
| workflow_run: | ||
| workflows: ["CI/CD Pipeline"] | ||
| types: | ||
| - completed | ||
|
|
||
| jobs: | ||
| self_heal: | ||
| # Loop prevention: Only trigger if the failing branch isn't already a selfheal branch | ||
| if: > | ||
| github.event.workflow_run.conclusion == 'failure' && | ||
| !startsWith(github.event.workflow_run.head_branch, 'selfheal-') | ||
| runs-on: ubuntu-latest | ||
|
Comment on lines
+11
to
+15
|
||
| permissions: | ||
| contents: write | ||
| pull-requests: write | ||
|
|
||
| concurrency: | ||
| group: selfheal-${{ github.event.workflow_run.head_branch }} | ||
| cancel-in-progress: true | ||
|
|
||
| steps: | ||
| - name: Checkout failing branch | ||
| uses: actions/checkout@v4 | ||
| with: | ||
| ref: ${{ github.event.workflow_run.head_branch }} | ||
|
|
||
| - name: Create selfheal branch | ||
| id: branch | ||
| run: | | ||
| BRANCH_NAME="selfheal-${{ github.event.workflow_run.head_sha }}" | ||
| git checkout -b $BRANCH_NAME | ||
| echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT | ||
|
|
||
| - name: Extract Error Logs | ||
| uses: actions/github-script@v7 | ||
| id: logs | ||
| with: | ||
| script: | | ||
| // This is a placeholder for fetching failing job logs via API | ||
| // and saving them to error_logs.txt | ||
| const fs = require('fs'); | ||
| fs.writeFileSync('error_logs.txt', 'Mocked CI failure log extracted'); | ||
|
|
||
| - name: Apply LLM Fix (Mocked Copilot/AI Action) | ||
| id: llm_fix | ||
| uses: nick-fields/retry@v3 | ||
| with: | ||
| timeout_minutes: 10 | ||
| max_attempts: 3 | ||
| retry_wait_seconds: 30 | ||
| command: | | ||
| echo "Analyzing error_logs.txt with AI..." | ||
| echo "Applying patch..." | ||
| # Placeholder for actual LLM MCP/CLI integration | ||
| touch .ai_patch_applied | ||
|
|
||
| - name: Verify Fix with pytest | ||
| run: | | ||
| pip install pytest ruff mypy | ||
| if [ -f pyproject.toml ]; then pip install -e .[dev]; fi | ||
| pytest tests/ | ||
|
|
||
| - name: Open PR | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| run: | | ||
| git config user.name "github-actions[bot]" | ||
| git config user.email "github-actions[bot]@users.noreply.github.com" | ||
| git add . | ||
| git commit -m "Auto-remediation of CI failure" | ||
| # Simulated git push in workflow (replace with actual push in real repo context) | ||
| # git push -u origin ${{ steps.branch.outputs.branch_name }} | ||
| # gh pr create ... | ||
|
Comment on lines
+72
to
+76
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,4 +18,3 @@ target/ | |
| .DS_Store | ||
| .vscode/ | ||
| .idea/ | ||
| .port_sessions/ | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -91,3 +91,20 @@ Claw Code is built in the open alongside the broader UltraWorkers toolchain: | |||||
|
|
||||||
| - This repository does **not** claim ownership of the original Claude Code source material. | ||||||
| - This repository is **not affiliated with, endorsed by, or maintained by Anthropic**. | ||||||
|
|
||||||
| ## Self-Healing Features | ||||||
|
|
||||||
| This project includes robust self-healing patterns for both the deployment pipeline and the Python runtime. | ||||||
|
|
||||||
| ### CI/CD Self-Healing (.github/workflows) | ||||||
| - **Automatic rollback:** Health checks ping `/health` with exponential backoff post-deploy. If they fail after 10 attempts, the deploy is rolled back automatically. | ||||||
|
||||||
| - **Automatic rollback:** Health checks ping `/health` with exponential backoff post-deploy. If they fail after 10 attempts, the deploy is rolled back automatically. | |
| - **Automatic rollback:** Health checks ping `/health` at a fixed retry interval post-deploy. If they fail after 10 attempts, the deploy is rolled back automatically. |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,35 @@ | ||||||
| [build-system] | ||||||
| requires = ["hatchling"] | ||||||
| build-backend = "hatchling.build" | ||||||
|
|
||||||
| [project] | ||||||
| name = "selfheal-demo" | ||||||
| version = "0.1.0" | ||||||
| description = "Self-healing Python demo" | ||||||
| requires-python = ">=3.10" | ||||||
| dependencies = [ | ||||||
| "pydantic-settings>=2.0.0", | ||||||
| "tenacity>=8.2.0", | ||||||
| "structlog>=23.1.0" | ||||||
| ] | ||||||
|
|
||||||
| [project.optional-dependencies] | ||||||
| dev = [ | ||||||
| "pytest>=7.0", | ||||||
| "ruff>=0.1.0", | ||||||
| "mypy>=1.0.0", | ||||||
| "flask>=3.0.0", | ||||||
| "fastapi>=0.100.0", | ||||||
| "httpx>=0.24.0" | ||||||
| ] | ||||||
|
|
||||||
| [tool.ruff] | ||||||
| line-length = 100 | ||||||
| target-version = "py310" | ||||||
|
|
||||||
| [tool.mypy] | ||||||
| python_version = "3.10" | ||||||
| ignore_missing_imports = true | ||||||
|
|
||||||
| [tool.hatch.build.targets.wheel] | ||||||
| packages = ["src/selfheal"] | ||||||
|
badMade marked this conversation as resolved.
|
||||||
| packages = ["src/selfheal"] | |
| packages = ["src"] |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,38 @@ | ||||||||||||||||||||||||||||||||||||||||
| import os | ||||||||||||||||||||||||||||||||||||||||
| import sys | ||||||||||||||||||||||||||||||||||||||||
| import subprocess | ||||||||||||||||||||||||||||||||||||||||
| import structlog | ||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Importing Useful? React with 👍 / 👎.
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @copilot, make changes based on the above suggestion.
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @copilot, make changes based on the above suggestion. |
||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| logger = structlog.get_logger(__name__) | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| # Auto-install dependencies if in CI/CD self-healing mode | ||||||||||||||||||||||||||||||||||||||||
| if os.environ.get("SELFHEAL_AUTO_INSTALL") == "true": | ||||||||||||||||||||||||||||||||||||||||
| required_deps = ["pydantic-settings", "tenacity", "structlog"] | ||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||
| import importlib.util | ||||||||||||||||||||||||||||||||||||||||
| if not importlib.util.find_spec('pydantic_settings') or not importlib.util.find_spec('tenacity'): | ||||||||||||||||||||||||||||||||||||||||
| raise ImportError | ||||||||||||||||||||||||||||||||||||||||
| except ImportError: | ||||||||||||||||||||||||||||||||||||||||
| logger.info("Installing self-heal dependencies in CI environment") | ||||||||||||||||||||||||||||||||||||||||
| subprocess.check_call([sys.executable, "-m", "pip", "install"] + required_deps) | ||||||||||||||||||||||||||||||||||||||||
|
badMade marked this conversation as resolved.
|
||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+2
to
+18
|
||||||||||||||||||||||||||||||||||||||||
| import sys | |
| import subprocess | |
| import structlog | |
| logger = structlog.get_logger(__name__) | |
| # Auto-install dependencies if in CI/CD self-healing mode | |
| if os.environ.get("SELFHEAL_AUTO_INSTALL") == "true": | |
| required_deps = ["pydantic-settings", "tenacity", "structlog"] | |
| try: | |
| import importlib.util | |
| if not importlib.util.find_spec('pydantic_settings') or not importlib.util.find_spec('tenacity'): | |
| raise ImportError | |
| except ImportError: | |
| logger.info("Installing self-heal dependencies in CI environment") | |
| subprocess.check_call([sys.executable, "-m", "pip", "install"] + required_deps) | |
| import structlog | |
| logger = structlog.get_logger(__name__) |
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,103 @@ | ||||||||||
| import json | ||||||||||
| from typing import Optional | ||||||||||
| from pathlib import Path | ||||||||||
| from typing import Type, TypeVar | ||||||||||
| import structlog | ||||||||||
| from pydantic_settings import BaseSettings | ||||||||||
|
|
||||||||||
| logger = structlog.get_logger(__name__) | ||||||||||
|
|
||||||||||
| T = TypeVar("T", bound="SelfHealingConfig") | ||||||||||
|
|
||||||||||
| class SelfHealingConfig(BaseSettings): | ||||||||||
| """ | ||||||||||
| Pydantic BaseSettings subclass that self-heals its configuration file. | ||||||||||
| It will auto-regenerate missing files, backup+repair corrupt ones, | ||||||||||
| and raise explicitly for missing secrets. | ||||||||||
| """ | ||||||||||
|
|
||||||||||
| @classmethod | ||||||||||
| def load_or_heal( | ||||||||||
| cls: Type[T], | ||||||||||
| config_path: str, | ||||||||||
| sensitive_fields: Optional[list[str]] = None | ||||||||||
| ) -> T: | ||||||||||
|
Comment on lines
+20
to
+24
|
||||||||||
| sensitive_fields = sensitive_fields or [] | ||||||||||
| path = Path(config_path) | ||||||||||
|
|
||||||||||
| # 1. Regenerate missing config | ||||||||||
| if not path.exists(): | ||||||||||
| logger.warning("Config file missing, generating defaults", path=config_path) | ||||||||||
| return cls._generate_and_save(path, sensitive_fields) | ||||||||||
|
|
||||||||||
|
Comment on lines
+12
to
+32
|
||||||||||
| # 2. Try loading | ||||||||||
| try: | ||||||||||
| with open(path, 'r') as f: | ||||||||||
| data = json.load(f) | ||||||||||
| return cls(**data) | ||||||||||
| except json.JSONDecodeError as e: | ||||||||||
| logger.error("Config file is corrupt, backing up and regenerating", path=config_path, error=str(e)) | ||||||||||
| return cls._backup_and_regenerate(path, sensitive_fields) | ||||||||||
| except Exception as e: | ||||||||||
| # Catch pydantic validation errors | ||||||||||
| logger.error("Config validation failed, attempting field-level repair", path=config_path, error=str(e)) | ||||||||||
| return cls._repair_fields(path, sensitive_fields) | ||||||||||
|
badMade marked this conversation as resolved.
|
||||||||||
|
|
||||||||||
| @classmethod | ||||||||||
| def _generate_and_save(cls: Type[T], path: Path, sensitive_fields: list[str]) -> T: | ||||||||||
| # Pydantic will pull from defaults or environment variables | ||||||||||
| try: | ||||||||||
| instance = cls() | ||||||||||
| except ValueError as e: | ||||||||||
| logger.critical("Cannot generate default config. Missing sensitive/required fields?", error=str(e)) | ||||||||||
| raise | ||||||||||
|
Comment on lines
+49
to
+53
|
||||||||||
|
|
||||||||||
| cls._save(instance, path) | ||||||||||
| return instance | ||||||||||
|
|
||||||||||
| @classmethod | ||||||||||
| def _backup_and_regenerate(cls: Type[T], path: Path, sensitive_fields: list[str]) -> T: | ||||||||||
| backup_path = path.with_suffix('.bak') | ||||||||||
| if path.exists(): | ||||||||||
| path.rename(backup_path) | ||||||||||
| return cls._generate_and_save(path, sensitive_fields) | ||||||||||
|
badMade marked this conversation as resolved.
|
||||||||||
|
|
||||||||||
| @classmethod | ||||||||||
| def _repair_fields(cls: Type[T], path: Path, sensitive_fields: list[str]) -> T: | ||||||||||
| try: | ||||||||||
| with open(path, 'r') as f: | ||||||||||
| data = json.load(f) | ||||||||||
| except json.JSONDecodeError: | ||||||||||
| return cls._backup_and_regenerate(path, sensitive_fields) | ||||||||||
|
|
||||||||||
| # Field-level repair: Generate defaults, and override with valid keys from data | ||||||||||
| try: | ||||||||||
| defaults = cls().model_dump() | ||||||||||
| except ValueError as e: | ||||||||||
| logger.critical("Cannot repair config due to missing sensitive/required fields in env", error=str(e)) | ||||||||||
| raise | ||||||||||
|
Comment on lines
+77
to
+78
|
||||||||||
| logger.critical("Cannot repair config due to missing sensitive/required fields in env", error=str(e)) | |
| raise | |
| logger.critical("Cannot repair config due to missing sensitive/required fields in env", error=str(e)) | |
| raise |
Uh oh!
There was an error while loading. Please reload this page.