Skip to content

Generate Platform Install Missions #505

Generate Platform Install Missions

Generate Platform Install Missions #505

name: Generate Platform Install Missions
on:
schedule:
- cron: '0 0,6,12,18 * * *' # 4x daily (midnight, 6am, noon, 6pm UTC)
workflow_dispatch:
inputs:
platforms:
description: 'Comma-separated platform names (empty = all)'
required: false
batch_size:
description: 'Platforms per batch'
required: false
default: '20'
dry_run:
description: 'Dry run (no PR created)'
required: false
type: boolean
default: false
force_regenerate:
description: 'Overwrite existing missions'
required: false
type: boolean
default: false
permissions:
contents: read # default read-only; writes scoped to job below
jobs:
plan:
permissions:
contents: read
pull-requests: read
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.plan.outputs.matrix }}
batch_count: ${{ steps.plan.outputs.batch_count }}
is_targeted: ${{ steps.plan.outputs.is_targeted }}
target_platforms: ${{ steps.sanitize.outputs.target_platforms }}
batch_size: ${{ steps.sanitize.outputs.batch_size }}
dry_run: ${{ steps.sanitize.outputs.dry_run }}
force_regenerate: ${{ steps.sanitize.outputs.force_regenerate }}
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Sanitize workflow inputs
id: sanitize
env:
RAW_TARGET_PLATFORMS: ${{ inputs.platforms || '' }}
RAW_BATCH_SIZE: ${{ inputs.batch_size || '20' }}
RAW_DRY_RUN: ${{ inputs.dry_run && 'true' || 'false' }}
RAW_FORCE_REGENERATE: ${{ inputs.force_regenerate && 'true' || 'false' }}
run: |
sanitize_csv() {
local value="$1"
if [ -z "$value" ]; then
printf '%s' ''
return 0
fi
if printf '%s' "$value" | grep -Eq '^[A-Za-z0-9._/-]+(,[A-Za-z0-9._/-]+)*$'; then
printf '%s' "$value"
return 0
fi
echo "::error::Invalid comma-separated input: $value"
exit 1
}
case "$RAW_BATCH_SIZE" in
''|*[!0-9]*)
echo '::error::batch_size must be a positive integer'
exit 1
;;
esac
if [ "$RAW_BATCH_SIZE" -lt 1 ]; then
echo '::error::batch_size must be greater than zero'
exit 1
fi
case "$RAW_DRY_RUN" in
true|false) ;;
*)
echo '::error::dry_run must be true or false'
exit 1
;;
esac
case "$RAW_FORCE_REGENERATE" in
true|false) ;;
*)
echo '::error::force_regenerate must be true or false'
exit 1
;;
esac
printf 'target_platforms=%s\n' "$(sanitize_csv "$RAW_TARGET_PLATFORMS")" >> "$GITHUB_OUTPUT"
printf 'batch_size=%s\n' "$RAW_BATCH_SIZE" >> "$GITHUB_OUTPUT"
printf 'dry_run=%s\n' "$RAW_DRY_RUN" >> "$GITHUB_OUTPUT"
printf 'force_regenerate=%s\n' "$RAW_FORCE_REGENERATE" >> "$GITHUB_OUTPUT"
- name: Plan batches
id: plan
env:
BATCH_SIZE: ${{ steps.sanitize.outputs.batch_size }}
TARGET: ${{ steps.sanitize.outputs.target_platforms }}
run: |
cd scripts && npm ci --silent
if [ -n "$TARGET" ]; then
COUNT=$(echo "$TARGET" | tr ',' '\n' | wc -l | tr -d ' ')
echo "is_targeted=true" >> "$GITHUB_OUTPUT"
else
COUNT=$(node --input-type=module -e "
import { K8S_PLATFORMS } from './k8s-platforms.mjs';
import { OTHER_PROJECTS } from './other-projects.mjs';
import { readdirSync, existsSync } from 'fs';
const dir = '../fixes/platform-install';
const existing = existsSync(dir) ? readdirSync(dir).filter(f=>f.endsWith('.json')).length : 0;
const total = K8S_PLATFORMS.length + OTHER_PROJECTS.length;
const toGen = total - existing;
console.log(Math.max(toGen, 0));
" 2>/dev/null || echo "40")
echo "is_targeted=false" >> "$GITHUB_OUTPUT"
fi
BATCHES=$(( (COUNT + BATCH_SIZE - 1) / BATCH_SIZE ))
BATCHES=$(( BATCHES > 3 ? 3 : BATCHES )) # cap at 3 parallel
[ "$BATCHES" -lt 1 ] && BATCHES=1
# Build matrix JSON
INDICES="["
for i in $(seq 0 $((BATCHES - 1))); do
[ "$i" -gt 0 ] && INDICES="$INDICES,"
INDICES="$INDICES$i"
done
INDICES="$INDICES]"
echo "matrix=$INDICES" >> "$GITHUB_OUTPUT"
echo "batch_count=$BATCHES" >> "$GITHUB_OUTPUT"
echo "Planning: $COUNT platforms in $BATCHES batch(es) of $BATCH_SIZE"
generate:
permissions:
contents: read
pull-requests: read
needs: plan
runs-on: ubuntu-latest
strategy:
matrix:
batch_index: ${{ fromJson(needs.plan.outputs.matrix) }}
max-parallel: 2
fail-fast: false
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: '20'
- name: Install dependencies
run: cd scripts && npm ci --silent
- name: Create fixes directory
run: mkdir -p fixes/platform-install
- name: Generate platform missions
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
LLM_TOKEN: ${{ secrets.GH_MODELS_PAT }}
TARGET_PLATFORMS: ${{ needs.plan.outputs.target_platforms }}
BATCH_INDEX: ${{ matrix.batch_index }}
BATCH_SIZE: ${{ needs.plan.outputs.batch_size }}
DRY_RUN: ${{ needs.plan.outputs.dry_run }}
FORCE_REGENERATE: ${{ needs.plan.outputs.force_regenerate }}
run: node scripts/generate-platform-missions.mjs
- name: Upload artifacts
if: ${{ needs.plan.outputs.dry_run != 'true' }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: platform-missions-batch-${{ matrix.batch_index }}
path: |
fixes/platform-install/*.json
platform-report-*.md
collect:
permissions:
contents: write
pull-requests: write
needs: [plan, generate]
if: ${{ needs.plan.outputs.dry_run != 'true' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: '20'
- name: Install dependencies
run: cd scripts && npm ci --silent
- name: Download all batch artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: platform-missions-batch-*
merge-multiple: true
- name: Move mission files to fixes dir
run: |
mkdir -p fixes/platform-install
# Move any missions that ended up at repo root
find . -maxdepth 1 -name 'platform-*.json' -exec mv {} fixes/platform-install/ \;
echo "Platform missions:"
ls -la fixes/platform-install/platform-*.json 2>/dev/null | wc -l
- name: Schema validation
id: schema
run: |
ERRORS=0
for f in fixes/platform-install/platform-*.json; do
[ -f "$f" ] || continue
node --input-type=module -e "
import { validateMissionExport } from './scripts/scanner.mjs';
import { readFileSync } from 'fs';
const m = JSON.parse(readFileSync('$f','utf-8'));
const r = validateMissionExport(m);
if (!r.valid) { console.error('FAIL: $f', r.errors); process.exit(1); }
" || ERRORS=$((ERRORS + 1))
done
echo "schema_errors=$ERRORS" >> "$GITHUB_OUTPUT"
[ "$ERRORS" -gt 0 ] && echo "⚠️ $ERRORS schema errors" || echo "✅ All schemas valid"
- name: Security scan
id: security
run: |
BLOCKED=0
for f in fixes/platform-install/platform-*.json; do
[ -f "$f" ] || continue
node --input-type=module -e "
import { scanForSensitiveData, scanForMaliciousContent } from './scripts/scanner.mjs';
import { readFileSync } from 'fs';
const content = JSON.parse(readFileSync('$f','utf-8'));
const s = scanForSensitiveData(content);
const m = scanForMaliciousContent(content);
if (s.findings.length > 0 || m.findings.length > 0) {
console.error('BLOCKED: $f', JSON.stringify(s.findings), JSON.stringify(m.findings));
process.exit(1);
}
" || {
echo "🚫 Removing blocked file: $f"
rm "$f"
BLOCKED=$((BLOCKED + 1))
}
done
echo "security_blocked=$BLOCKED" >> "$GITHUB_OUTPUT"
- name: Rebuild index
run: node scripts/build-index.mjs
- name: Collect reports
run: |
echo "# Platform Mission Generation — Collect Report" > collect-report.md
echo "" >> collect-report.md
echo "Generated: $(date -u +%Y-%m-%dT%H:%M:%SZ)" >> collect-report.md
echo "" >> collect-report.md
NEW_COUNT=$(ls fixes/platform-install/platform-*.json 2>/dev/null | wc -l | tr -d ' ')
echo "## Results" >> collect-report.md
echo "- New platform missions: $NEW_COUNT" >> collect-report.md
echo "- Schema errors: ${{ steps.schema.outputs.schema_errors || '0' }}" >> collect-report.md
echo "- Security blocked: ${{ steps.security.outputs.security_blocked || '0' }}" >> collect-report.md
echo "" >> collect-report.md
# Append individual reports (use nullglob to handle no matches gracefully)
shopt -s nullglob
for f in platform-report-*.md; do
cat "$f" >> collect-report.md
done
shopt -u nullglob
- name: Create PR
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
NEW_COUNT=$(ls fixes/platform-install/platform-*.json 2>/dev/null | wc -l | tr -d ' ')
[ "$NEW_COUNT" -eq 0 ] && echo "No new missions, skipping PR" && exit 0
BRANCH="platform-install-gen-$(date +%Y%m%d)-$((RANDOM % 10000))"
git checkout -b "$BRANCH"
git add fixes/platform-install/ fixes/index.json
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git commit -s -m "✨ Platform install mission generation $(date +%Y-%m-%d)
Generated $NEW_COUNT platform install missions.
Includes managed K8s services, distributions, and cluster operators."
git push origin "$BRANCH"
AVG_SCORE=$(node -e "
const fs = require('fs');
const path = 'fixes/platform-install';
const files = fs.readdirSync(path).filter(f=>f.startsWith('platform-')&&f.endsWith('.json'));
if (!files.length) { console.log(0); process.exit(); }
const scores = files.map(f => JSON.parse(fs.readFileSync(path+'/'+f,'utf-8')).metadata?.qualityScore || 0);
console.log(Math.round(scores.reduce((a,b)=>a+b,0)/scores.length));
")
DRAFT_FLAG=""
[ "$AVG_SCORE" -lt 50 ] && DRAFT_FLAG="--draft"
gh pr create \
--title "✨ Platform install mission generation $(date +%Y-%m-%d)" \
--body "$(cat collect-report.md)" \
--base master \
$DRAFT_FLAG