Generate Platform Install Missions #505
This file contains hidden or 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: 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 |