1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-08-04 13:48:56 +02:00

chore: AI flag cleanup extended experiment (#10254)

Experimenting more with AI flag cleanup.
This commit is contained in:
Nuno Góis 2025-07-01 09:04:34 +01:00 committed by GitHub
parent 661fd6febf
commit 6503deea9b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 271 additions and 11 deletions

258
.github/workflows/ai-flag-cleanup-pr.yml vendored Normal file
View File

@ -0,0 +1,258 @@
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
permissions:
pull-requests: write
contents: write
issues: read
jobs:
create-pull-request:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.ref }}
- name: Create a new branch
uses: actions/github-script@v7
id: create_branch
with:
script: |
const kebabCase = (str) => {
return str
.toLowerCase()
.trim()
.replace(/[^\w\s-]/g, '') // Remove invalid characters
.replace(/\s+/g, '-') // Replace spaces with dashes
.replace(/^-+|-+$/g, ''); // Remove leading/trailing dashes
};
const fixBranchUrl = (url) => url
.replace(/\/git\/commits/, '/commit')
.replace(/api.github.com\/repos/, 'github.com');
// New branch should be based on the base-branch, so we need to get its SHA
const baseBranch = await github.rest.repos.getBranch({
owner: context.repo.owner,
repo: context.repo.repo,
branch: '${{ inputs.base-branch }}'
});
const { repo, owner } = context.repo;
const branchName = 'feature/aider-' + kebabCase(context.payload.issue.title);
const refName = `refs/heads/${branchName}`
const refShortName = `heads/${branchName}`
// Get existing ref if exists
const existingRef = await github.rest.git.getRef({
owner,
repo,
ref: refShortName
}).catch(() => null);
if (existingRef) {
try {
// If there's a branch for this ref, return the ref
await github.rest.repos.getBranch({
owner,
repo,
branch: branchName
});
console.log(`Branch ${branchName} already exists with SHA ${existingRef.data.object.sha}`);
console.log(`Branch URL: ${fixBranchUrl(existingRef.data.object.url)}`);
return { ref: existingRef.data.ref }
} catch (e) {
console.error(e);
// State recovery: If there's a ref but no branch, delete the ref and create a new branch
// This can happen if the branch was deleted manually. The ref will still exist.
console.log(`Branch ${branchName} doesn't exist, deleting ref ${refShortName}`);
await github.rest.git.deleteRef({
owner,
repo,
ref: refShortName
});
}
}
// Create branch
const result = await github.rest.git.createRef({
owner,
repo,
ref: refName,
sha: baseBranch.data.commit.sha
});
console.log(`Created branch ${branchName} with SHA ${result.data.object.sha}`);
console.log(`Branch URL: ${fixBranchUrl(result.data.object.url)}`);
return { ref: result.data.ref }
- name: Get issue
uses: actions/github-script@v7
id: get_issue
with:
script: |
console.log('Fetching issue #${{ inputs.issue-number }}')
const { repo, owner } = context.repo;
const result = await github.rest.issues.get({
owner,
repo,
issue_number: ${{ inputs.issue-number }}
});
console.log(`Fetched issue #${result.data.number}: ${result.data.title}`)
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: |
TITLE="${{ fromJson(steps.get_issue.outputs.result).title }}"
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"
exit 1
fi
- name: Install ripgrep
run: sudo apt-get update && sudo apt-get install -y ripgrep
- name: Find files using the flag
id: find_files
run: |
FLAG="${{ steps.extract_flag.outputs.flag-name }}"
FILES=$(rg -0 -l "$FLAG" . | xargs -0 -I{} printf '"%s" ' "{}")
if [[ -z "$FILES" ]]; then
echo "❌ No files found using flag '$FLAG'."
exit 1
fi
echo "file_args=$FILES" >> "$GITHUB_OUTPUT"
- name: Create prompt
uses: actions/github-script@v7
id: create_prompt
with:
result-encoding: string
script: |
const body = `${{ fromJson(steps.get_issue.outputs.result).body }}`;
return `Apply all necessary changes to clean up this feature flag based on the issue description below.
After making the changes, provide a Markdown summary of what was changed, written for a developer reviewing the PR. These changes should be under a "AI Flag Cleanup Summary" section.
Explain:
- What was removed
- What was kept
- Why these changes were made
Write a natural summary in proper Markdown. Keep it clear, focused, and readable.
--- Issue Description ---
${body}`;
- name: Apply cleanup with Aider
uses: mirrajabi/aider-github-action@v1.1.0
timeout-minutes: ${{ inputs.chat-timeout }}
with:
branch: ${{ fromJson(steps.create_branch.outputs.result).ref }}
model: ${{ inputs.model }}
aider_args: '--yes ${{ steps.find_files.outputs.file_args }} --message "${{ steps.create_prompt.outputs.result }}"'
api_key_env_name: ${{ inputs.api_key_env_name }}
api_key_env_value: ${{ secrets.api_key_env_value }}
- name: Extract summary from Aider output
id: extract_summary
run: |
SUMMARY=$(awk '/AI Flag Cleanup Summary/ {found=1} found' .aider.chat.history.md)
echo "summary<<EOF" >> $GITHUB_OUTPUT
echo "$SUMMARY" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Create Pull Request
uses: actions/github-script@v7
with:
script: |
const { repo, owner } = context.repo;
const branchRef = '${{ fromJson(steps.create_branch.outputs.result).ref }}'
const flagName = '${{ steps.extract_flag.outputs.flag-name }}';
// If PR already exists, return it
const pulls = await github.rest.pulls.list({
owner,
repo,
state: 'open',
per_page: 100
});
const existingPR = pulls.data.find((pr) => pr.head.ref === branchRef);
if (existingPR) {
console.log(`PR #${existingPR.number} already exists: ${existingPR.html_url}`);
return existingPR;
}
const newPR = await github.rest.pulls.create({
title: `[AI] ${flagName} flag cleanup`,
owner,
repo,
head: branchRef,
base: 'refs/heads/${{ inputs.base-branch }}',
body: [
`This PR cleans up the ${flagName} flag. These changes were automatically generated by AI and should be reviewed carefully.`,
'',
`Fixes #${{ inputs.issue-number }}`,
'',
`${{ steps.extract_summary.outputs.summary }}`
].join('\n')
});
github.rest.issues.addLabels({
owner,
repo,
issue_number: newPR.data.number,
labels: ['unleash-ai-flag-cleanup']
});
console.log(`Created PR #${newPR.data.number}: ${newPR.data.html_url}`);
- name: Upload aider chat history
uses: actions/upload-artifact@v4
with:
name: aider-chat-output
path: ".aider.chat.history.md"

View File

@ -1,22 +1,24 @@
name: AI flag cleanup
on:
issues:
types: [labeled]
permissions:
pull-requests: write
contents: write
issues: read
workflow_dispatch:
inputs:
issue-number:
description: 'Flag completed issue number'
required: true
type: number
jobs:
flag-cleanup:
uses: mirrajabi/aider-github-workflows/.github/workflows/aider-issue-to-pr.yml@v1.0.0
if: github.event.label.name == 'unleash-flag-completed'
if: |
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'issues' && github.event.label.name == 'unleash-flag-completed')
uses: ./.github/workflows/ai-flag-cleanup-pr.yml
with:
issue-number: ${{ github.event.issue.number }}
base-branch: ${{ github.event.repository.default_branch }}
chat-timeout: 10
api_key_env_name: GEMINI_API_KEY
issue-number: ${{ github.event.issue.number || inputs.issue-number }}
model: gemini
api_key_env_name: GEMINI_API_KEY
secrets:
api_key_env_value: ${{ secrets.GEMINI_API_KEY }}