name: AI flag cleanup PR on: workflow_call: inputs: issue-number: description: "Flag completed issue number" required: true type: number model: description: "Model to use" required: true type: string api_key_env_name: description: "The name of the API key environment variable. For example, OPENAI_API_KEY, ANTHROPIC_API_KEY, etc. See more info: https://aider.chat/docs/llms.html" required: true type: string base-branch: description: "Base branch to create PR against (e.g. main)" required: false type: string default: ${{ github.event.repository.default_branch }} chat-timeout: description: "Timeout for flag cleanup, in minutes" required: false type: number default: 10 secrets: api_key_env_value: description: "The API key" required: true UNLEASH_BOT_APP_ID: required: true UNLEASH_BOT_PRIVATE_KEY: required: true permissions: pull-requests: write contents: write issues: write jobs: create-pull-request: runs-on: ubuntu-latest steps: - name: Generate GitHub App token id: app_token uses: actions/create-github-app-token@v2 with: app-id: ${{ secrets.UNLEASH_BOT_APP_ID }} private-key: ${{ secrets.UNLEASH_BOT_PRIVATE_KEY }} - name: Checkout uses: actions/checkout@v4 with: ref: ${{ github.ref }} token: ${{ steps.app_token.outputs.token }} fetch-depth: 0 persist-credentials: true - name: Get issue uses: actions/github-script@v7 id: get_issue with: github-token: ${{ steps.app_token.outputs.token }} script: | console.log('Fetching issue #${{ inputs.issue-number }}') const { owner, repo } = context.repo; const result = await github.rest.issues.get({ owner, repo, issue_number: ${{ inputs.issue-number }} }); return { title: result.data.title.replace(/"/g, "'").replace(/`/g, '\\`'), body: result.data.body.replace(/"/g, "'").replace(/`/g, '\\`') }; - name: Extract flag name id: extract_flag run: | read -r TITLE <<'EOF' ${{ fromJson(steps.get_issue.outputs.result).title }} EOF if [[ "$TITLE" =~ Flag[[:space:]]([a-zA-Z0-9_-]+)[[:space:]]marked ]]; then echo "flag-name=${BASH_REMATCH[1]}" >> $GITHUB_OUTPUT else echo "โŒ Could not extract flag name from title: $TITLE" >&2 exit 1 fi - name: Create a new branch id: create_branch uses: actions/github-script@v7 env: FLAG_NAME: ${{ steps.extract_flag.outputs.flag-name }} with: github-token: ${{ steps.app_token.outputs.token }} result-encoding: string script: | const kebab = (s) => s .toLowerCase().trim() .replace(/[^\w\s-]/g, '') .replace(/\s+/g, '-') .replace(/^-+|-+$/g, ''); const { owner, repo } = context.repo; const base = '${{ inputs.base-branch }}'; const baseBranch = await github.rest.repos.getBranch({ owner, repo, branch: base }); const branchName = `chore/unleash-ai-${kebab(process.env.FLAG_NAME)}-flag-cleanup`; const refFull = `refs/heads/${branchName}`; const refShort = `heads/${branchName}`; // remove stale ref if it exists without branch const existing = await github.rest.git.getRef({ owner, repo, ref: refShort }).catch(() => null); if (existing) { try { await github.rest.repos.getBranch({ owner, repo, branch: branchName }); return branchName; } catch { await github.rest.git.deleteRef({ owner, repo, ref: refShort }); } } // create new await github.rest.git.createRef({ owner, repo, ref: refFull, sha: baseBranch.data.commit.sha }); return branchName; - name: Check out new branch uses: actions/checkout@v4 with: ref: ${{ steps.create_branch.outputs.result }} token: ${{ steps.app_token.outputs.token }} fetch-depth: 0 persist-credentials: true - name: Configure Git run: | git config --global --add safe.directory "$GITHUB_WORKSPACE" git config --global user.email "194219037+unleash-bot[bot]@users.noreply.github.com" git config --global user.name "unleash-bot" - name: Install ripgrep run: sudo apt-get update && sudo apt-get install -y ripgrep - name: Find files id: find_files run: | FLAG="${{ steps.extract_flag.outputs.flag-name }}" mapfile -d '' FILES < <(rg -0 -l "$FLAG" .) if [[ -z "$FILES" ]]; then echo "โŒ No files found for flag '$FLAG'" exit 1 fi printf '%s\0' "${FILES[@]}" > file_list.bin echo "file_list=./file_list.bin" >> $GITHUB_OUTPUT - name: Create prompt id: create_prompt run: | ISSUE_BODY="${{ fromJson(steps.get_issue.outputs.result).body }}" cat <<'EOF' > cleanup_prompt.txt Based on the issue description below, refactor the codebase to permanently apply the desired outcome for this feature flag (e.g. enable, keep variant, or discard), by removing all conditional checks and dead branches, preserving only the correct code path. After making the changes, provide a **Markdown summary** of what was changed, written for a developer reviewing the PR. Keep it clear, focused, and readable. Use the exact following format (including start & end separator lines, headings, bullets, emojis): \`\`\`md === AI Flag Cleanup Summary Start === ## ๐Ÿงน AI Flag Cleanup Summary (Short summary of the changes made) (Include any details that you think are critical for the reviewer to know, if any, prefixing them with an appropriate emoji) ### ๐Ÿšฎ Removed - **(Category)** - (list of removed items) ### ๐Ÿ›  Kept - **(Category)** - (list of kept items) ### ๐Ÿ“ Why (Your reasoning for the changes made, including any relevant context or decisions that do not belong at the top of the summary.) === AI Flag Cleanup Summary End === \`\`\` --- Issue Description --- $ISSUE_BODY EOF echo "prompt-file=cleanup_prompt.txt" >> $GITHUB_OUTPUT - name: Set up Python uses: actions/setup-python@v4 with: python-version: '3.12' - name: Install build tools & Aider run: | python -m pip install --upgrade pip pip install aider-chat - name: Run Aider CLI and capture summary id: run_aider timeout-minutes: ${{ inputs.chat-timeout }} env: GIT_AUTHOR_NAME: 'unleash-bot' GIT_AUTHOR_EMAIL: '194219037+unleash-bot[bot]@users.noreply.github.com' GIT_COMMITTER_NAME: 'unleash-bot' GIT_COMMITTER_EMAIL: '194219037+unleash-bot[bot]@users.noreply.github.com' ${{ inputs.api_key_env_name }}: ${{ secrets.api_key_env_value }} run: | mapfile -d '' FILES < <(cat "${{ steps.find_files.outputs.file_list }}") aider --model "${{ inputs.model }}" \ --yes \ --message-file cleanup_prompt.txt \ --no-attribute-author \ --no-attribute-committer \ --no-attribute-co-authored-by \ "${FILES[@]}" \ | tee aider_output.txt SUMMARY=$(sed -n '/=== AI Flag Cleanup Summary Start ===/,/=== AI Flag Cleanup Summary End ===/{ /=== AI Flag Cleanup Summary Start ===/d /=== AI Flag Cleanup Summary End ===/d p }' aider_output.txt) echo "summary<> $GITHUB_OUTPUT echo "$SUMMARY" >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT - name: Push commit run: git push -u origin HEAD - name: Create Pull Request uses: actions/github-script@v7 with: github-token: ${{ steps.app_token.outputs.token }} result-encoding: string script: | const { owner, repo } = context.repo; const branch = '${{ steps.create_branch.outputs.result }}'; const flag = '${{ steps.extract_flag.outputs.flag-name }}'; const summary = ${{ toJson(steps.run_aider.outputs.summary) }}; const body = [ `This PR cleans up the ${flag} flag. These changes were automatically generated by AI and should be reviewed carefully.`, '', `Fixes #${{ inputs.issue-number }}`, '', summary ].join('\n'); const { data: prs } = await github.rest.pulls.list({ owner, repo, state: 'open', per_page: 100 }); const existing = prs.find(p => p.head.ref === branch); if (existing) { await github.rest.pulls.update({ owner, repo, pull_number: existing.number, body }); console.log(`Updated PR #${existing.number}: ${existing.html_url}`); return existing; } const { data: pr } = await github.rest.pulls.create({ owner, repo, title: `chore(AI): ${flag} flag cleanup`, head: branch, base: '${{ inputs.base-branch }}', body }); await github.rest.issues.addLabels({ owner, repo, issue_number: pr.number, labels: ['unleash-ai-flag-cleanup'] }); console.log(`Created PR #${pr.number}: ${pr.html_url}`); return pr;