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
4 changes: 4 additions & 0 deletions .github/self-heal-schedule.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# AUTO-UPDATED
schedule: "0 0 * * *"
reason: "standard"
last_computed: "2023-01-01T00:00:00Z"
98 changes: 98 additions & 0 deletions .github/workflows/compute-schedule.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
name: Compute Self-Heal Schedule

on:
schedule:
- cron: "0 0 * * 0" # Runs weekly to evaluate cadence
workflow_dispatch:

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

permissions:
contents: write
pull-requests: write

jobs:
compute-schedule:
runs-on: ubuntu-latest
timeout-minutes: 5

steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
fetch-depth: 0 # Need git history for telemetry

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

- name: Install dependencies
run: npm ci || npm install

- name: Compute New Schedule
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: node scripts/compute_schedule.mjs

- name: Update GitHub Actions Cron
run: |
node -e "
import fs from 'fs';
import yaml from 'js-yaml';

try {
const scheduleData = yaml.load(fs.readFileSync('.github/self-heal-schedule.yml', 'utf-8'));
const newCron = scheduleData.schedule;

if (newCron) {
let workflowStr = fs.readFileSync('.github/workflows/self-heal.yml', 'utf-8');
workflowStr = workflowStr.replace(/- cron: \".*\" # AUTO-UPDATED/g, \`- cron: \"\${newCron}\" # AUTO-UPDATED\`);
fs.writeFileSync('.github/workflows/self-heal.yml', workflowStr, 'utf-8');
console.log('Successfully updated self-heal.yml schedule to:', newCron);
}
} catch(e) {
console.error('Failed to update schedule in workflow file', e);
process.exit(1);
}
" --input-type=module

- name: Check for Changes
id: git-check
run: |
if [ -z "$(git status --porcelain)" ]; then
echo "No schedule changes needed."
echo "changed=false" >> $GITHUB_OUTPUT
exit 0
fi
echo "changed=true" >> $GITHUB_OUTPUT

- name: Create PR
if: steps.git-check.outputs.changed == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Check for duplicate open schedule PRs
OPEN_PRS=$(gh pr list --label self-heal-schedule --state open --json number -q 'length')
if [ "$OPEN_PRS" -gt 0 ]; then
echo "There is already an open schedule update PR. Skipping."
exit 0
fi

BRANCH_NAME="selfheal-schedule-$(date +%s)"
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git checkout -b "$BRANCH_NAME"

git add .github/self-heal-schedule.yml .github/workflows/self-heal.yml
git commit -m "Auto-update self-heal schedule based on telemetry"
git push origin "$BRANCH_NAME"

gh pr create \
--title "[Self-Heal Schedule] Update cadence" \
--body "Updates the self-healing CI pipeline schedule based on recent repository telemetry." \
--label "self-heal-schedule" \
--head "$BRANCH_NAME" \
--base main
142 changes: 142 additions & 0 deletions .github/workflows/self-heal.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
name: Self-Heal Auto-Repair

on:
schedule:
- cron: "0 0 * * *" # AUTO-UPDATED - Fallback, normally overridden by logic if sed used, but we read this via logic or the runner uses it directly. Wait, GitHub Actions schedule MUST be statically in the YML.
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:
name: Run Self-Heal Pipeline
if: |
!startsWith(github.ref_name, 'selfheal-') &&
github.ref_name == 'main' &&
(github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'failure')
runs-on: ubuntu-latest
timeout-minutes: 15

steps:
- name: Checkout Repository
uses: actions/checkout@v4

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

- name: Install Base Dependencies
run: npm ci || npm install

- name: Clean up Stale PRs
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Close self-heal PRs older than 7 days
STALE_PRS=$(gh pr list --label self-heal --state open --json number,createdAt -q '.[] | select(.createdAt < (now - 604800 | todate)) | .number')
for pr in $STALE_PRS; do
echo "Closing stale PR #$pr"
gh pr close "$pr" -m "Auto-closing stale self-heal PR."
done

- name: Run Pre-Healthcheck
id: pre_healthcheck
run: |
node scripts/healthcheck.mjs > pre_healthcheck.log 2>&1 || echo "status=unhealthy" >> $GITHUB_OUTPUT
continue-on-error: true

- name: Run Repair Pipeline
id: repair
run: |
node scripts/self_heal.mjs > repair.log 2>&1
# self_heal.mjs exits 0 if health is good and diff exists.
# It exits 1 if failed to repair or if no diffs.

- name: Upload Logs
uses: actions/upload-artifact@v4
with:
name: self-heal-logs-${{ github.run_id }}
path: |
pre_healthcheck.log
repair.log
retention-days: 7

- name: Create PR
if: steps.repair.outcome == 'success'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Double check diff
if [ -z "$(git status --porcelain)" ]; then
echo "No diff found. Exiting."
exit 0
fi

# Scan for secrets (entropy/patterns) - simple heuristic
if git diff | grep -iE 'api_key|token|secret|password'; then
echo "Potential secrets in diff! Aborting PR creation."
exit 1
fi

# Check for duplicate open selfheal PRs
OPEN_PRS=$(gh pr list --label self-heal --state open --json number -q 'length')
if [ "$OPEN_PRS" -gt 0 ]; then
echo "There is already an open self-heal PR. Skipping."
exit 0
fi

BRANCH_NAME="selfheal-$(date +%s)"
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git checkout -b "$BRANCH_NAME"

# Stage specific paths explicitly
for path in src/ tests/ scripts/ package.json package-lock.json; do
git add "$path" 2>/dev/null || true
done

# Check if we staged anything
if [ -z "$(git diff --cached)" ]; then
echo "No changes in allowed paths. Exiting."
exit 0
fi

git commit -m "Auto-repair drift and CI failures"
git push origin "$BRANCH_NAME"

TRIGGER_NAME=""
if [ "${{ github.event_name }}" = "schedule" ]; then
TRIGGER_NAME="Scheduled"
elif [ "${{ github.event_name }}" = "workflow_run" ]; then
TRIGGER_NAME="Reactive"
else
TRIGGER_NAME="Manual"
fi

# Mention execution permissions for scripts if they were added
chmod +x scripts/*.mjs 2>/dev/null || true

gh pr create \
--title "[Self-Heal $TRIGGER_NAME] Repair project drift" \
--body "This PR was automatically generated by the self-healing CI pipeline to repair failures or drift.

**Trigger Reason:** $TRIGGER_NAME
**Drift Summary:** The automated pipeline detected and repaired issues via idempotency runs (e.g. formatting, type stubs, snapshots, or dependencies).
**Logs:** [View Artifacts](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})

Please review the changes and ensure no unintended side effects were introduced before merging. This PR was generated by an LLM-assisted workflow session." \
--label "self-heal" \
--head "$BRANCH_NAME" \
--base main
38 changes: 38 additions & 0 deletions SELF_HEAL_SETUP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Self-Healing CI Pipeline

This repository implements a self-adapting repair pipeline to fix CI failures and project drift automatically.

## Triggers

1. **Scheduled:** Runs on a self-computed cadence based on repository activity to catch drift.
2. **Reactive:** Triggers automatically whenever the main CI workflow fails on `main`.
3. **Manual:** Can be triggered manually via `workflow_dispatch` in the Actions tab.

## The Repair Pipeline (6 Steps)

The pipeline is idempotent and uses existing tooling:

1. **Rebuild/Reinstall:** Clean installation of dependencies (`npm ci`).
2. **Auto-Format:** Runs `prettier` to fix formatting drift.
3. **Update Snapshots:** Runs tests to update Vitest snapshots.
4. **Update Type Stubs:** Synchronizes TypeScript definitions.
5. **Update Lockfile:** Updates dependencies.
6. **Build Project:** Runs the build command.

After each step, a health check is performed. If the project becomes healthy and there is a diff, it safely generates a Pull Request for human review. It fails closed to prevent erroneous code pushes.

## Schedule Computation

The `compute_schedule.mjs` script runs weekly. It analyzes the recent `git log` to determine commit frequency and adjusts the cron schedule for the proactive self-heal runs automatically. A PR is opened if the schedule needs to be updated.

## Manual Overrides

If you wish to override the schedule manually:
1. Edit `.github/self-heal-schedule.yml`.
2. Commit the changes. The pipeline respects manual changes up to the next periodic recompute.

## Reviewer Checklist for Self-Heal PRs
- [ ] Review the drift summary.
- [ ] Ensure no secret files or tokens were inadvertently modified.
- [ ] Check if the changes match expected snapshot/formatting updates.
- [ ] Approve and merge manually (auto-merge is disabled by design).
29 changes: 23 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,10 @@
"@types/which": "^3.0.4",
"@vitest/coverage-v8": "3.1.1",
"esbuild": "^0.25.2",
"js-yaml": "^4.1.1",
"multer": "1.4.5-lts.1",
"openai": "^4.91.1",
"prettier": "^3.8.3",
"tsx": "^4.19.3",
"typescript": "^5.8.2",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The self_heal.mjs script relies on typesync (line 55), but it is not listed in the project's dependencies. Adding it to devDependencies ensures it is available in the CI environment and avoids the overhead and potential security risks of downloading it via npx on every run.

Suggested change
"typescript": "^5.8.2",
"typescript": "^5.8.2",
"typesync": "^0.11.1",

"vitest": "^3.1.1"
Expand Down
Loading