name: Dev CI Fixer on: workflow_run: workflows: [Deploy] types: [completed] workflow_dispatch: permissions: actions: read contents: write issues: write pull-requests: write concurrency: dev-ci-fixer jobs: fix: if: | github.repository == 'anomalyco/models.dev' && ( github.event_name == 'workflow_dispatch' || ( github.event.workflow_run.conclusion == 'failure' && github.event.workflow_run.head_branch == 'dev' ) ) runs-on: ubuntu-latest env: GH_REPO: ${{ github.repository }} GH_TOKEN: ${{ github.token }} FAILED_RUN_ID: ${{ github.event.workflow_run.id }} FAILED_RUN_URL: ${{ github.event.workflow_run.html_url }} FAILED_WORKFLOW: ${{ github.event.workflow_run.name }} steps: - name: Check run budget id: budget run: | set -euo pipefail cutoff="$(date -u -d '8 hours ago' '+%Y-%m-%dT%H:%M:%SZ')" open_pr="$(gh pr list --state open --search "label:ci-fixer" --json number --limit 100 --jq '.[0].number // empty')" if [ -n "$open_pr" ]; then echo "run=false" >> "$GITHUB_OUTPUT" echo "Skipping because ci-fixer PR #$open_pr is already open." exit 0 fi recent_pr="$(gh pr list --state all --search "label:ci-fixer" --json number,createdAt --limit 100 --jq "map(select(.createdAt >= \"$cutoff\")) | .[0].number // empty")" if [ -n "$recent_pr" ]; then echo "run=false" >> "$GITHUB_OUTPUT" echo "Skipping because ci-fixer PR #$recent_pr was created within the last 8 hours." exit 0 fi echo "run=true" >> "$GITHUB_OUTPUT" - name: Compute budget key id: budget-key if: steps.budget.outputs.run == 'true' run: | hour="$(date -u '+%H')" bucket=$((10#$hour / 8)) echo "key=ci-fixer-$(date -u '+%Y%m%d')-$bucket" >> "$GITHUB_OUTPUT" - name: Check budget marker id: budget-cache if: steps.budget.outputs.run == 'true' uses: actions/cache/restore@v4 with: path: .ci-fixer-budget key: ${{ steps.budget-key.outputs.key }} lookup-only: true - name: Create budget marker if: steps.budget.outputs.run == 'true' && steps.budget-cache.outputs.cache-hit != 'true' run: | mkdir -p .ci-fixer-budget date -u '+%Y-%m-%dT%H:%M:%SZ' > .ci-fixer-budget/created-at - name: Save budget marker if: steps.budget.outputs.run == 'true' && steps.budget-cache.outputs.cache-hit != 'true' uses: actions/cache/save@v4 with: path: .ci-fixer-budget key: ${{ steps.budget-key.outputs.key }} - name: Checkout code if: steps.budget.outputs.run == 'true' && steps.budget-cache.outputs.cache-hit != 'true' uses: actions/checkout@v4 with: ref: dev - name: Install opencode if: steps.budget.outputs.run == 'true' && steps.budget-cache.outputs.cache-hit != 'true' run: curl -fsSL https://opencode.ai/install | bash - name: Collect failed logs if: steps.budget.outputs.run == 'true' && steps.budget-cache.outputs.cache-hit != 'true' run: | set -euo pipefail LOG_FILE="$RUNNER_TEMP/dev-ci-failure.log" echo "LOG_FILE=$LOG_FILE" >> "$GITHUB_ENV" if [ -n "${FAILED_RUN_ID:-}" ]; then gh run view "$FAILED_RUN_ID" --log-failed > "$LOG_FILE" || gh run view "$FAILED_RUN_ID" --log > "$LOG_FILE" else echo "Manual dev CI fixer dispatch; no failed workflow_run logs are available." > "$LOG_FILE" fi max_bytes=80000 if [ "$(wc -c < "$LOG_FILE")" -gt "$max_bytes" ]; then tail -c "$max_bytes" "$LOG_FILE" > "$LOG_FILE.tail" mv "$LOG_FILE.tail" "$LOG_FILE" fi - name: Run CI fixer if: steps.budget.outputs.run == 'true' && steps.budget-cache.outputs.cache-hit != 'true' env: OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} OPENCODE_PERMISSION: '{"bash":"deny"}' run: | set -o pipefail RESPONSE_FILE="$RUNNER_TEMP/ci-fixer-response.md" echo "RESPONSE_FILE=$RESPONSE_FILE" >> "$GITHUB_ENV" { cat </dev/null 2>&1 || true gh label create ci-fixer --color "D93F0B" --description "Automated fix for failed dev CI" >/dev/null 2>&1 || true PR_BODY="$RUNNER_TEMP/ci-fixer-pr-body.md" { echo "Automated fix for failed dev CI." echo echo "Failed run: $FAILED_RUN_URL" echo if [ -s "$RESPONSE_FILE" ]; then cat "$RESPONSE_FILE" fi } > "$PR_BODY" gh pr create --base dev --head "$BRANCH" --title "$TITLE" --body-file "$PR_BODY" --label automation --label ci-fixer