Skip to content
Draft
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
3 changes: 3 additions & 0 deletions .github/self-heal-schedule.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
schedule: "0 0 * * 1"
rationale: "Initial fallback schedule due to low commit volume. Computed by self-healing automation."
LAST_UPDATED: "2025-05-31T00:00:00Z"
79 changes: 79 additions & 0 deletions .github/workflows/compute-schedule.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
name: Compute Self-Heal Schedule

on:
schedule:
- cron: "0 0 1 * *" # Runs monthly
workflow_dispatch:

concurrency:
group: compute-schedule-${{ github.ref }}
cancel-in-progress: true

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

jobs:
compute-schedule:
runs-on: ubuntu-latest
if: !startsWith(github.ref_name, 'selfheal-')
timeout-minutes: 5
steps:
- name: Checkout Code
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20

- name: Install Dependencies
run: npm ci

- name: Configure Git Committer
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"

- name: Run Compute Schedule
run: |
chmod +x scripts/compute_schedule.mjs
node scripts/compute_schedule.mjs || true
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Check for Duplicate PRs
id: duplicate_check
run: |
RECENT_PR=$(gh pr list --label self-heal-schedule --state open --json createdAt -q '.[0].createdAt' || echo "null")
if [ ! -z "$RECENT_PR" ] && [ "$RECENT_PR" != "null" ]; then
echo "duplicate=true" >> $GITHUB_OUTPUT
echo "Found open self-heal-schedule PR, skipping new PR creation."
else
echo "duplicate=false" >> $GITHUB_OUTPUT
fi
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Create PR
if: steps.duplicate_check.outputs.duplicate == 'false'
run: |
if [ -n "$(git status --porcelain)" ]; then
BRANCH_NAME="selfheal-schedule-$(date +%s)"
git checkout -b $BRANCH_NAME

git add .github/self-heal-schedule.yml .github/workflows/self-heal.yml
git commit -m "Update self-heal schedule"
git push origin $BRANCH_NAME

gh pr create \
--title "[Self-Heal Schedule] Update cadence" \
--body "Automated update to self-healing CI schedule based on telemetry." \
--label "automation,self-heal-schedule" \
--base main
fi
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
132 changes: 132 additions & 0 deletions .github/workflows/self-heal.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
name: Self-Heal Auto-Repair

on:
schedule:
- cron: "0 0 * * 1" # AUTO-UPDATED
workflow_run:
workflows: ["ci"]
types:
- completed
workflow_dispatch:

concurrency:
group: selfheal-${{ github.ref }}
cancel-in-progress: true

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

jobs:
repair:
runs-on: ubuntu-latest
if: >-
!startsWith(github.ref_name, 'selfheal-') &&
(
github.event_name == 'schedule' ||
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'failure')
)
timeout-minutes: 15
steps:
- name: Checkout Code
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20

- name: Configure Git Committer
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"

- name: Cleanup Stale PRs
run: |
STALE_DATE=$(date -d "7 days ago" --iso-8601=seconds)
for pr in $(gh pr list --label self-heal --search "created:<$STALE_DATE" --json number -q '.[].number'); do
gh pr close "$pr" --comment "Auto-closing stale self-heal PR" || true
done
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Run Self-Heal Repair
id: repair
continue-on-error: true
run: |
chmod +x scripts/self_heal.mjs scripts/healthcheck.mjs
node scripts/self_heal.mjs
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Upload Logs
uses: actions/upload-artifact@v4
if: always()
with:
name: self-heal-logs
path: '*.log'

- name: Check for Duplicate PRs
id: duplicate_check
run: |
RECENT_PR=$(gh pr list --label self-heal --state open --json createdAt -q '.[0].createdAt' || echo "null")
if [ ! -z "$RECENT_PR" ] && [ "$RECENT_PR" != "null" ]; then
echo "duplicate=true" >> $GITHUB_OUTPUT
echo "Found open self-heal PR, skipping new PR creation."
else
echo "duplicate=false" >> $GITHUB_OUTPUT
fi
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Create PR
if: steps.duplicate_check.outputs.duplicate == 'false' && steps.repair.outcome == 'success'
run: |
if [ -n "$(git status --porcelain)" ]; then
BRANCH_NAME="selfheal-repair-$(date +%s)"
git checkout -b $BRANCH_NAME

# Entropy check: if we see suspicious patterns, we can abort, but we rely on git add paths as primary safeguard.
if git diff | grep -iE '(api_key|token|password|secret)[^a-zA-Z0-9]*='; then
echo "Potential secrets detected in diff, aborting PR creation."
# Return true so workflow doesnt completely fail, but no PR is made
exit 0
fi

# Add specified files only to prevent pushing forbidden changes
for path in src/ tests/ package.json package-lock.json eslint.config.mjs; do
git add "$path" 2>/dev/null || true
done

git commit -m "Auto-repair dependencies and code formatting"
git push origin $BRANCH_NAME

TITLE="[Self-Heal Manual] Repair"
if [ "${{ github.event_name }}" == "schedule" ]; then
TITLE="[Self-Heal Scheduled] Drift fixes"
elif [ "${{ github.event_name }}" == "workflow_run" ]; then
TITLE="[Self-Heal Reactive] CI fix"
fi

PR_BODY="Automated self-healing PR to fix code drift and CI failures. Review logs in actions.\n\n"
PR_BODY+="Triggers explained:\n"
PR_BODY+="1. Scheduled: Proactive drift detection\n"
PR_BODY+="2. CI Failure: Reactive repair trigger\n"
PR_BODY+="3. Manual: workflow_dispatch\n\n"
PR_BODY+="Checklist:\n"
PR_BODY+="- [ ] No unintended source logic changes\n"
PR_BODY+="- [ ] All health checks pass\n"
PR_BODY+="- [ ] Review workflow logs\n"

gh pr create \
--title "$TITLE" \
--body "$PR_BODY" \
--label "automation,self-heal" \
--base main
fi
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ bin/
.cursor

.DS_Store
*.log
46 changes: 46 additions & 0 deletions SELF_HEAL_SETUP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Self-Heal Auto-Repair Setup

This document describes the self-healing automation integrated into the repository to automatically detect, fix, and submit PRs for code drift and formatting issues.

## System Components

1. **scripts/healthcheck.mjs**: Validates the project health using tests, linters, types, and build scripts.
2. **scripts/self_heal.mjs**: Executes a sequential 6-step idempotent repair process (e.g. `npm ci`, `eslint --fix`, `prettier -w`, `vitest -u`, `typesync`, `npm update`). It validates health after each step and will exit immediately if a step succeeds and produces a git diff.
3. **scripts/compute_schedule.mjs**: Dynamically computes an optimal run cadence based on recent git telemetry (commit volume).
4. **GitHub Actions**:
- `self-heal.yml`: Runs the repair pipeline triggered by schedule, manual dispatch, or CI failures. Submits an automated PR using `gh pr create`.
- `compute-schedule.yml`: Recomputes the ideal schedule periodically and submits a PR if adjustments are needed.

## Triggers

The self-healing workflow is triggered via three different mechanisms:
1. **Scheduled (`schedule`)**: Proactive monitoring that runs on a cadence determined by `compute_schedule.mjs` to detect codebase drift.
2. **Reactive (`workflow_run`)**: Triggered immediately when the main `ci` workflow fails on the default branch.
3. **Manual (`workflow_dispatch`)**: Can be manually triggered from the Actions tab for immediate execution and repair.

## Self-Scheduling Logic

The `scripts/compute_schedule.mjs` script gathers recent git commit frequency (e.g., last 30 days) and assigns a tiered schedule cadence:
- **Dormant** (0 commits): Monthly.
- **Low-churn** (< 10 commits): Weekly.
- **Standard** (10 - 50 commits): Twice a week.
- **Active** (50 - 150 commits): Daily.
- **High** (> 150 commits): Every 12 hours.

This automatically optimizes compute resources during inactive periods and maintains responsiveness during active development.

## Manual Override Instructions

If you need to forcefully override the schedule and prevent the automatic recalculation from changing it back, you can:
1. Modify `.github/self-heal-schedule.yml` directly with your desired `schedule` value.
2. Modify `.github/workflows/self-heal.yml` schedule cron line to match the value above, preserving the `# AUTO-UPDATED` inline comment marker at the end.
3. The oscillation guard or logic in `scripts/compute_schedule.mjs` can be adjusted if needed to ignore future automated updates.

## Reviewer Checklist

When reviewing a self-heal or schedule update PR, human reviewers must verify the following:
- [ ] **No unintended source logic changes**: The self-heal process is only intended for formatting, snapshots, and tooling updates. Confirm no business logic was modified.
- [ ] **All health checks pass**: Ensure all CI status checks (tests, linters, types) are green for the self-heal branch.
- [ ] **Review workflow logs**: Examine the artifact logs attached to the PR action run to understand why the drift occurred.
- [ ] **Check for duplicate PRs**: Ensure this does not duplicate a previously opened PR. (The automation attempts to catch this, but manual verification provides redundancy).
- [ ] **Verify valid schedule format**: For schedule update PRs, verify the cron expression is standard and valid.
25 changes: 25 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import eslintJs from "@eslint/js";
import tseslint from "typescript-eslint";
import globals from "globals";

export default tseslint.config(
eslintJs.configs.recommended,
...tseslint.configs.recommended,
{
ignores: ["build/**", "dist/**", "coverage/**", "bin/**", "node_modules/**"],
},
{
languageOptions: {
globals: {
...globals.node,
...globals.browser,
},
},
rules: {
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/ban-ts-comment": "off",
"preserve-caught-error": "off"
}
Comment on lines +21 to +23

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The rule preserve-caught-error is not a standard ESLint or @typescript-eslint rule. Including an invalid or unrecognized rule in the flat configuration will cause ESLint to throw an error and fail to run entirely, which will break the linting step in your healthchecks and CI. If you want to configure how unused caught errors are handled, you should configure the caughtErrors option of the standard no-unused-vars rule instead.

      "@typescript-eslint/ban-ts-comment": "off"
    }

}
);
Loading