Process repos #609
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
name: Run ai-security-analyzer in dir mode on config changes | |
on: | |
push: | |
paths: | |
- '**/**/**/config.json' | |
jobs: | |
get_config_files: | |
runs-on: ubuntu-latest | |
outputs: | |
config_files: ${{ steps.get_config_files.outputs.config_files }} | |
steps: | |
- name: Checkout code | |
uses: actions/checkout@v4 | |
- name: Get list of changed config.json files | |
id: get_config_files | |
uses: actions/github-script@v6 | |
with: | |
script: | | |
const commitSHA = context.sha; | |
const response = await github.rest.repos.getCommit({ | |
owner: context.repo.owner, | |
repo: context.repo.repo, | |
ref: commitSHA, | |
}); | |
const changedFiles = response.data.files.map(f => f.filename); | |
const configFilesChanged = changedFiles.filter(f => f.endsWith('config.json')); | |
if (configFilesChanged.length === 0) { | |
throw new Error('No changed config.json files found'); | |
} | |
core.setOutput('config_files', JSON.stringify(configFilesChanged)); | |
analyze: | |
needs: get_config_files | |
runs-on: ubuntu-latest | |
continue-on-error: true | |
strategy: | |
fail-fast: false | |
matrix: | |
config_file: ${{ fromJson(needs.get_config_files.outputs.config_files) }} | |
steps: | |
- name: Checkout repository | |
uses: actions/checkout@v4 | |
- name: Set up git user | |
run: | | |
git config user.name "GitHub Action" | |
git config user.email "[email protected]" | |
- name: Read config.json | |
id: read_config | |
uses: actions/github-script@v6 | |
with: | |
script: | | |
const fs = require('fs'); | |
const path = require('path'); | |
const configFile = '${{ matrix.config_file }}'; | |
const configPath = path.join(process.env.GITHUB_WORKSPACE, configFile); | |
const configContent = fs.readFileSync(configPath, 'utf8'); | |
const config = JSON.parse(configContent); | |
let mode = config.mode; | |
let target = config.repo_url; | |
let agent_model = config.agent_model || 'gpt-4o'; | |
core.setOutput('mode', mode); | |
core.setOutput('target', target); | |
core.setOutput('repo_url', config.repo_url || ''); | |
core.setOutput('agent_provider', config.agent_provider || 'openai'); | |
core.setOutput('agent_model', agent_model); | |
core.setOutput('agent_temperature', config.agent_temperature || 0.0); | |
core.setOutput('analyzer_args', config.analyzer_args || ''); | |
const configDir = path.dirname(configFile); | |
core.setOutput('config_dir', configDir); | |
// Sanitize agent_model for filesystem safety | |
const sanitized_model = agent_model.replace(/[\/\\:*?"<>|]/g, '-'); | |
// Determine the output subdirectory with sanitized model name | |
const output_subdir = `${new Date().toISOString().slice(0, 10)}-${sanitized_model}`; | |
core.setOutput('output_subdir', output_subdir); | |
// Handle agent_prompt_types | |
const default_agent_prompt_types = ["sec-design", "threat-modeling", "attack-surface", "attack-tree", "mitigations"]; | |
let agent_prompt_types = config.agent_prompt_types; | |
if (!Array.isArray(agent_prompt_types) || agent_prompt_types.length === 0) { | |
agent_prompt_types = default_agent_prompt_types; | |
} | |
core.setOutput('agent_prompt_types', JSON.stringify(agent_prompt_types)); | |
- name: Clone target repository | |
run: | | |
git clone https://github.com/${{ steps.read_config.outputs.repo_url }} target_repo | |
shell: bash | |
- name: Record Start Time | |
id: record_start_time | |
shell: bash | |
run: | | |
echo "start_datetime=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT | |
- name: Run ai-security-analyzer via Docker for each agent_prompt_type | |
id: run_analyzer | |
env: | |
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} | |
LANGCHAIN_API_KEY: ${{ secrets.LANGCHAIN_API_KEY }} | |
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }} | |
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} | |
run: | | |
AGENT_PROMPT_TYPES='${{ steps.read_config.outputs.agent_prompt_types }}' | |
AGENT_PROVIDER='${{ steps.read_config.outputs.agent_provider }}' | |
AGENT_MODEL='${{ steps.read_config.outputs.agent_model }}' | |
AGENT_TEMPERATURE='${{ steps.read_config.outputs.agent_temperature }}' | |
MODE='${{ steps.read_config.outputs.mode }}' | |
TARGET='${{ steps.read_config.outputs.target }}' | |
# Convert JSON array to bash array | |
agent_prompt_types=$(echo "$AGENT_PROMPT_TYPES" | jq -r '.[]') | |
mkdir -p output target_repo | |
# Define maximum retries and delay between retries | |
MAX_RETRIES=3 | |
RETRY_DELAY=10 | |
for prompt_type in $agent_prompt_types; do | |
echo "Running ai-security-analyzer with agent_prompt_type: $prompt_type" | |
output_file="/output/${prompt_type}.md" | |
# Initialize retry counter | |
retries=0 | |
success=false | |
while [ $retries -lt $MAX_RETRIES ]; do | |
# Temporarily disable 'set -e' to handle command failures manually | |
set +e | |
# Run docker command and redirect output to log file | |
docker run --rm \ | |
--user $(id -u):$(id -g) \ | |
-v "${{ github.workspace }}/target_repo:/code" \ | |
-v "${{ github.workspace }}/output:/output" \ | |
-e OPENAI_API_KEY \ | |
-e GOOGLE_API_KEY \ | |
-e OPENROUTER_API_KEY \ | |
ghcr.io/xvnpw/ai-security-analyzer:latest \ | |
$MODE -v -t /code \ | |
-o "$output_file" \ | |
${{ steps.read_config.outputs.analyzer_args }} \ | |
--agent-provider "$AGENT_PROVIDER" \ | |
--agent-model "$AGENT_MODEL" \ | |
--agent-temperature "$AGENT_TEMPERATURE" \ | |
--checkpoint-dir /code/.checkpoints \ | |
--agent-prompt-type "$prompt_type" >> ./output/ai-security-analyzer.log 2>&1 | |
# Capture the exit code of the docker run command | |
exit_code=$? | |
cat ./output/ai-security-analyzer.log | |
# Re-enable 'set -e' to make the script exit on errors outside this block | |
set -e | |
if [ $exit_code -eq 0 ]; then | |
success=true | |
break | |
else | |
echo "ai-security-analyzer failed with exit code $exit_code. Retrying in $RETRY_DELAY seconds..." | |
sleep $RETRY_DELAY | |
retries=$((retries + 1)) | |
fi | |
done | |
if [ "$success" = false ]; then | |
echo "ai-security-analyzer failed after $MAX_RETRIES attempts." | |
exit 1 | |
fi | |
done | |
shell: bash | |
- name: Calculate total token count | |
id: calculate_total_token_count | |
run: | | |
actual_token_usage=$(grep -oP 'Actual token usage: \K\d+' "./output/ai-security-analyzer.log" | awk '{sum += $1} END {print sum}') | |
echo "actual_token_usage=$actual_token_usage" >> $GITHUB_OUTPUT | |
shell: bash | |
- name: Record End Time | |
id: record_end_time | |
shell: bash | |
run: | | |
echo "end_datetime=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT | |
- name: Move output files to output directory | |
env: | |
AGENT_PROMPT_TYPES: ${{ steps.read_config.outputs.agent_prompt_types }} | |
run: | | |
mkdir -p ${{ steps.read_config.outputs.config_dir }}/${{ steps.read_config.outputs.output_subdir }} | |
cp -r output/* ${{ steps.read_config.outputs.config_dir }}/${{ steps.read_config.outputs.output_subdir }}/ | |
shell: bash | |
- name: Create output-metadata.json | |
uses: actions/github-script@v6 | |
with: | |
script: | | |
const fs = require('fs'); | |
const path = require('path'); | |
const outputDir = path.join( | |
process.env.GITHUB_WORKSPACE, | |
'${{ steps.read_config.outputs.config_dir }}', | |
'${{ steps.read_config.outputs.output_subdir }}' | |
); | |
const metadata = { | |
repo_url: process.env.REPO_URL, | |
analyzer_args: process.env.ANALYZER_ARGS, | |
start_datetime: process.env.START_DATETIME, | |
end_datetime: process.env.END_DATETIME, | |
agent_provider: process.env.AGENT_PROVIDER, | |
agent_model: process.env.AGENT_MODEL, | |
agent_temperature: process.env.AGENT_TEMPERATURE, | |
agent_prompt_types: JSON.parse(process.env.AGENT_PROMPT_TYPES), | |
actual_token_usage: process.env.ACTUAL_TOKEN_USAGE, | |
mode: process.env.MODE, | |
}; | |
fs.mkdirSync(outputDir, { recursive: true }); | |
fs.writeFileSync(path.join(outputDir, 'output-metadata.json'), JSON.stringify(metadata, null, 2)); | |
env: | |
REPO_URL: ${{ steps.read_config.outputs.repo_url }} | |
ANALYZER_ARGS: ${{ steps.read_config.outputs.analyzer_args }} | |
START_DATETIME: ${{ steps.record_start_time.outputs.start_datetime }} | |
END_DATETIME: ${{ steps.record_end_time.outputs.end_datetime }} | |
AGENT_PROVIDER: ${{ steps.read_config.outputs.agent_provider }} | |
AGENT_MODEL: ${{ steps.read_config.outputs.agent_model }} | |
AGENT_TEMPERATURE: ${{ steps.read_config.outputs.agent_temperature }} | |
AGENT_PROMPT_TYPES: ${{ steps.read_config.outputs.agent_prompt_types }} | |
ACTUAL_TOKEN_USAGE: ${{ steps.calculate_total_token_count.outputs.actual_token_usage }} | |
MODE: ${{ steps.read_config.outputs.mode }} | |
- name: Clean up target_repo | |
run: | | |
rm -rf target_repo/ output/ | |
shell: bash | |
- name: Commit changes | |
id: commit | |
uses: EndBug/add-and-commit@v9 | |
continue-on-error: true | |
with: | |
message: 'Add security documentation generated by ai-security-analyzer for ${{ matrix.config_file }}' | |
add: ${{ steps.read_config.outputs.config_dir }}/${{ steps.read_config.outputs.output_subdir }}/ | |
pull: '--rebase --autostash' | |
- name: Retry commit on failure | |
if: steps.commit.outcome == 'failure' | |
run: | | |
for i in {1..3}; do | |
echo "Attempt $i to commit changes..." | |
git pull --rebase --autostash | |
if git push; then | |
echo "Commit successful on attempt $i" | |
exit 0 | |
fi | |
sleep $((i * 5)) # Exponential backoff: 5s, 10s, 15s | |
done | |
echo "Failed to commit after 3 attempts" | |
exit 1 | |
shell: bash | |
trigger_workflow: | |
if: contains(needs.*.result, 'success') # Run if at least one analyze job succeeded | |
needs: analyze | |
runs-on: ubuntu-latest | |
steps: | |
- name: Trigger second workflow | |
env: | |
PAT_SECRET: ${{ secrets.PAT_SECRET }} | |
run: | | |
curl -X POST -H "Authorization: token $PAT_SECRET" \ | |
-H "Accept: application/vnd.github.v3+json" \ | |
https://api.github.com/repos/xvnpw/sec-docs/actions/workflows/process-repos-dir.yaml/dispatches \ | |
-d '{"ref":"main"}' |