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 * * *"
rationale: "Bootstrap schedule"
lastUpdated: 0
103 changes: 103 additions & 0 deletions .github/workflows/compute-schedule.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
name: Compute Self-Heal Schedule

on:
schedule:
- cron: "0 0 * * 0" # Runs weekly to evaluate the self-heal schedule
workflow_dispatch:

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

jobs:
compute-schedule:
runs-on: ubuntu-latest
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: '22'
cache: 'npm'

- name: Check for duplicate Schedule Update PRs
id: check_pr
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
RECENT_PR=$(gh pr list --label "self-heal-schedule" --state open --json createdAt -q '.[0].createdAt')
if [ ! -z "$RECENT_PR" ] && [ "$RECENT_PR" != "null" ]; then
echo "An open schedule update PR already exists. Skipping run."
echo "skip=true" >> $GITHUB_OUTPUT
else
echo "skip=false" >> $GITHUB_OUTPUT
fi

- name: Install dependencies
if: steps.check_pr.outputs.skip == 'false'
run: npm ci

- name: Compute New Schedule
if: steps.check_pr.outputs.skip == 'false'
id: compute
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
node scripts/compute_schedule.mjs

- name: Update Workflow with sed
if: steps.check_pr.outputs.skip == 'false'
run: |
# Read the new schedule
NEW_CRON=$(grep -oP 'schedule: "\K[^"]+' .github/self-heal-schedule.yml || echo "")
if [ ! -z "$NEW_CRON" ]; then
# Replace the schedule in self-heal.yml specifically targeting the # AUTO-UPDATED marker line
sed -i "s|.*# AUTO-UPDATED| - cron: \"$NEW_CRON\" # AUTO-UPDATED|" .github/workflows/self-heal.yml
fi

- name: Setup Git Identity
if: steps.check_pr.outputs.skip == 'false'
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"

- name: Create Pull Request
if: steps.check_pr.outputs.skip == 'false'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if git diff --quiet; then
echo "No schedule changes needed."
exit 0
fi

BRANCH_NAME="selfheal-schedule-$(date +%s)"
git checkout -b $BRANCH_NAME

# Only stage allowed files
for path in .github/workflows/self-heal.yml .github/self-heal-schedule.yml; do
git add "$path" 2>/dev/null || true
done

if git diff --cached --quiet; then
echo "No allowed files were changed."
exit 0
fi

git commit -m "Update self-heal cadence"
git push -u origin $BRANCH_NAME

gh pr create \
--title "[Self-Heal Schedule] Update cadence" \
--body "Auto-generated PR to update the self-healing schedule based on recent repository telemetry.

_Generated by Compute Schedule Workflow_" \
--label "self-heal-schedule" \
--label "automation"
192 changes: 192 additions & 0 deletions .github/workflows/self-heal.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
name: Self-Heal Repair Pipeline

on:
schedule:
- cron: "0 0 * * *" # 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:
# Run if manual dispatch OR scheduled OR (ci workflow failed)
if: >-
!startsWith(github.ref_name, 'selfheal-') &&
(
github.event_name == 'workflow_dispatch' ||
github.event_name == 'schedule' ||
(github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'failure')
)
runs-on: ubuntu-latest
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: '22'
cache: 'npm'

- name: Check for duplicate Self-Heal PRs
id: check_pr
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
RECENT_PR=$(gh pr list --label "self-heal" --state open --json createdAt -q '.[0].createdAt')
if [ ! -z "$RECENT_PR" ] && [ "$RECENT_PR" != "null" ]; then
echo "An open self-heal PR already exists. Skipping run."
echo "skip=true" >> $GITHUB_OUTPUT
else
echo "skip=false" >> $GITHUB_OUTPUT
fi

- name: Close Stale PRs
if: steps.check_pr.outputs.skip == 'false'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
DATE=$(date -d "7 days ago" +%Y-%m-%d)
STALE_PRS=$(gh pr list --label "self-heal" --state open --search "created:<$DATE" --json number -q '.[].number')
for PR in $STALE_PRS; do
gh pr close $PR --comment "Closing stale self-heal PR"
done

- name: Pre-Repair Healthcheck
if: steps.check_pr.outputs.skip == 'false'
run: node scripts/healthcheck.mjs > pre-check.log 2>&1 || echo "Pre-check failed, as expected"

- name: Run Self-Heal Pipeline
id: selfheal
if: steps.check_pr.outputs.skip == 'false'
run: |
if node scripts/self_heal.mjs; then
echo "success=true" >> $GITHUB_OUTPUT
else
echo "success=false" >> $GITHUB_OUTPUT
exit 1
fi

- name: Post-Repair Healthcheck
id: post
if: steps.selfheal.outputs.success == 'true'
run: node scripts/healthcheck.mjs > post-check.log 2>&1

- name: Upload Logs
if: always()
uses: actions/upload-artifact@v4
with:
name: self-heal-logs
path: |
pre-check.log
repair.log
post-check.log

- name: Check Diff & Entropy
id: check_diff
if: steps.post.outcome == 'success'
run: |
if git diff --quiet; then
echo "No changes made."
echo "diff=false" >> $GITHUB_OUTPUT
else
# Filter files strictly to allowed directories/files, rejecting .env, ci.yml, secrets
ALLOWED_FILES=$(git diff --name-only | grep -E '^(src/|tests/|package\.json|package-lock\.json|scripts/|eslint\.config\.mjs)')
FORBIDDEN_FILES=$(git diff --name-only | grep -E '^(\.github/workflows/ci\.yml|\.env|secrets/)')

if [ ! -z "$FORBIDDEN_FILES" ]; then
echo "Changes detected in forbidden files. Aborting."
exit 1
fi

if [ -z "$ALLOWED_FILES" ]; then
echo "No allowed files were changed."
echo "diff=false" >> $GITHUB_OUTPUT
exit 0
fi

echo "diff=true" >> $GITHUB_OUTPUT

for path in $ALLOWED_FILES; do
git add "$path" 2>/dev/null || true
done

if git diff --cached -E -i '(password|secret|key|token)[=:]' ; then
echo "Potential secrets detected in diff. Aborting PR creation."
exit 1
fi

# Meaningful diff check (fail if it's only whitespace and healthcheck was green)
# Not fully perfect but basic safeguard
if git diff --cached --quiet; then
echo "No meaningful changes made after adding allowed files."
echo "diff=false" >> $GITHUB_OUTPUT
exit 0
fi
fi

- name: Setup Git Identity
if: steps.check_diff.outputs.diff == 'true'
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"

- name: Create Pull Request
if: steps.check_diff.outputs.diff == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
BRANCH_NAME="selfheal-$(date +%s)"
git checkout -b $BRANCH_NAME
git commit -m "Auto-repair via Self-Heal Pipeline"
git push -u origin $BRANCH_NAME

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

DRIFT_SUMMARY=$(git log -1 --stat)
SCHEDULE_CONFIG=$(cat .github/self-heal-schedule.yml || echo "Unknown schedule")

gh pr create \
--title "$TITLE" \
--body "Auto-generated repair PR.

**Trigger:** $TRIGGER

**Drift Summary:**
\`\`\`
$DRIFT_SUMMARY
\`\`\`

**Current Schedule Rationale:**
\`\`\`yaml
$SCHEDULE_CONFIG
\`\`\`

Please review the attached logs in the Artifacts section.

_Generated by the Self-Heal Pipeline requested via Claude Code / Jules_" \
--label "self-heal" \
--label "automation"
39 changes: 39 additions & 0 deletions SELF_HEAL_SETUP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Self-Heal Automation Setup

This repository uses an adaptive, automated self-healing pipeline to maintain code health, detect drift, and perform necessary repairs via pull requests.

## How It Works

The system is powered by two main GitHub Actions workflows and a suite of scripts.

1. **Triggers:**
* **Proactive (Scheduled):** Periodically runs to catch codebase drift (e.g., outdated lockfiles, new linting rules).
* **Reactive (CI Failure):** Runs automatically when the primary `ci` workflow fails on the default branch.
* **Manual:** Can be manually triggered via `workflow_dispatch`.

2. **Idempotent Repair Pipeline (`scripts/self_heal.mjs`):**
When triggered, the pipeline attempts a 6-step repair process:
1. Rebuild/Reinstall dependencies.
2. Lint and format auto-fix.
3. Snapshot regeneration for tests.
4. Type stubs update (`typesync`).
5. Dependency re-resolve.
6. Static asset regeneration (builds).

After each step, a **Healthcheck** (`scripts/healthcheck.mjs`) validates the state. If the build passes and diffs are generated, it halts and succeeds.

3. **Adaptive Scheduling (`scripts/compute_schedule.mjs`):**
A secondary workflow (`compute-schedule.yml`) runs weekly to monitor repository telemetry (like PR velocity). It recalculates the optimal frequency for the scheduled trigger to avoid unnecessary CI usage during dormant periods.

## Manual Overrides

If you need to manually change the self-heal schedule and prevent the adaptive logic from overriding it:
1. Edit `.github/self-heal-schedule.yml`.
2. Ensure the `lastUpdated` timestamp is set to a future epoch time or a very recent time to delay the next re-computation.
3. The `# AUTO-UPDATED` marker in `.github/workflows/self-heal.yml` must remain intact for future automation.

## Reviewer Checklist for Self-Heal PRs
When reviewing a PR generated by this automation:
- [ ] Verify the diff does not include logical source code changes (only formatting, snapshots, deps).
- [ ] Review the artifact logs (`repair.log`, `pre-check.log`, `post-check.log`) attached to the workflow run.
- [ ] Ensure the PR resolves the immediate issue or drift before merging.
28 changes: 28 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import js from "@eslint/js";
import tseslint from "typescript-eslint";
import globals from "globals";

export default tseslint.config(
{
ignores: ["build/**", "dist/**", "coverage/**", "bin/**", "node_modules/**"]
},
js.configs.recommended,
...tseslint.configs.recommended,
{
languageOptions: {
globals: {
...globals.node,
...globals.browser,
...globals.jest,
...globals.vitest
}
},
rules: {
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }],
"@typescript-eslint/ban-ts-comment": "off",
"prefer-const": "off",
"preserve-caught-error": "off"
Comment on lines +24 to +25

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 a non-existent rule in the configuration will cause ESLint to throw an error and crash, which in turn will fail the healthcheck script. You should remove this line.

      "prefer-const": "off"

}
}
);
Loading