Strategy Optimization Watcher #43
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: Strategy Optimization Watcher | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| source_repo: | |
| description: "Repository that owns strategy metrics and receives optimization issues" | |
| required: true | |
| default: "QuantStrategyLab/CryptoLivePoolPipelines" | |
| source_ref: | |
| description: "Source repository ref to inspect" | |
| required: false | |
| default: "main" | |
| metrics_path: | |
| description: "JSON metrics payload path inside the source repository" | |
| required: false | |
| default: "data/output/strategy_metrics.json" | |
| dry_run: | |
| description: "Do not create GitHub issues" | |
| required: false | |
| type: boolean | |
| default: true | |
| schedule: | |
| - cron: "17 3 * * *" | |
| permissions: | |
| contents: read | |
| issues: write | |
| concurrency: | |
| group: strategy-optimization-watcher-${{ github.event.inputs.source_repo || vars.STRATEGY_WATCH_SOURCE_REPO || 'QuantStrategyLab/CryptoLivePoolPipelines' }} | |
| cancel-in-progress: false | |
| jobs: | |
| strategy-optimization-watcher: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| env: | |
| FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" | |
| SOURCE_REPO: ${{ github.event.inputs.source_repo || vars.STRATEGY_WATCH_SOURCE_REPO || 'QuantStrategyLab/CryptoLivePoolPipelines' }} | |
| SOURCE_REF: ${{ github.event.inputs.source_ref || vars.STRATEGY_WATCH_SOURCE_REF || 'main' }} | |
| METRICS_PATH: ${{ github.event.inputs.metrics_path || vars.STRATEGY_WATCH_METRICS_PATH || 'data/output/strategy_metrics.json' }} | |
| STRATEGY_WATCH_DRY_RUN: ${{ github.event_name == 'workflow_dispatch' && format('{0}', inputs.dry_run) || vars.STRATEGY_WATCH_DRY_RUN || 'true' }} | |
| ALLOWED_SOURCE_REPOS: ${{ vars.STRATEGY_WATCH_ALLOWED_SOURCE_REPOS || 'QuantStrategyLab/CryptoLivePoolPipelines' }} | |
| ALLOWED_SOURCE_REFS: ${{ vars.STRATEGY_WATCH_ALLOWED_SOURCE_REFS || 'main' }} | |
| steps: | |
| - name: Checkout Bridge | |
| uses: actions/checkout@v6.0.3 | |
| with: | |
| path: bridge | |
| persist-credentials: false | |
| - name: Detect GitHub App Credentials | |
| id: app_credentials | |
| env: | |
| APP_ID: ${{ vars.CROSS_REPO_GITHUB_APP_ID }} | |
| APP_PRIVATE_KEY: ${{ secrets.CROSS_REPO_GITHUB_APP_PRIVATE_KEY }} | |
| run: | | |
| set -euo pipefail | |
| if [ -n "${APP_ID:-}" ] && [ -n "${APP_PRIVATE_KEY:-}" ]; then | |
| echo "available=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "available=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Resolve Source Repository Name | |
| id: source_repo | |
| run: | | |
| set -euo pipefail | |
| if [[ ! "${SOURCE_REPO}" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]]; then | |
| echo "Invalid SOURCE_REPO: ${SOURCE_REPO}. Expected owner/name." >&2 | |
| exit 1 | |
| fi | |
| allowed_match=false | |
| IFS=',' read -ra allowed_repos <<< "${ALLOWED_SOURCE_REPOS}" | |
| for allowed_repo in "${allowed_repos[@]}"; do | |
| allowed_repo="${allowed_repo//[[:space:]]/}" | |
| if [ "${allowed_repo}" = "${SOURCE_REPO}" ]; then | |
| allowed_match=true | |
| break | |
| fi | |
| done | |
| if [ "${allowed_match}" != "true" ]; then | |
| echo "SOURCE_REPO is not allowed for strategy watcher: ${SOURCE_REPO}" >&2 | |
| exit 1 | |
| fi | |
| ref_allowed=false | |
| IFS=',' read -ra allowed_refs <<< "${ALLOWED_SOURCE_REFS}" | |
| for allowed_ref in "${allowed_refs[@]}"; do | |
| allowed_ref="${allowed_ref//[[:space:]]/}" | |
| if [ "${allowed_ref}" = "${SOURCE_REF}" ]; then | |
| ref_allowed=true | |
| break | |
| fi | |
| done | |
| if [ "${ref_allowed}" != "true" ]; then | |
| echo "SOURCE_REF is not allowed for strategy watcher: ${SOURCE_REF}" >&2 | |
| exit 1 | |
| fi | |
| owner="${SOURCE_REPO%%/*}" | |
| repository="${SOURCE_REPO#*/}" | |
| echo "owner=${owner}" >> "$GITHUB_OUTPUT" | |
| echo "repository=${repository}" >> "$GITHUB_OUTPUT" | |
| - name: Create GitHub App Token For Source Repository | |
| id: source_app_token | |
| if: steps.app_credentials.outputs.available == 'true' | |
| continue-on-error: true | |
| uses: actions/create-github-app-token@v3.2.0 | |
| with: | |
| app-id: ${{ vars.CROSS_REPO_GITHUB_APP_ID }} | |
| private-key: ${{ secrets.CROSS_REPO_GITHUB_APP_PRIVATE_KEY }} | |
| owner: ${{ steps.source_repo.outputs.owner }} | |
| repositories: ${{ steps.source_repo.outputs.repository }} | |
| permission-contents: read | |
| permission-issues: write | |
| - name: Verify Source Repository Token | |
| env: | |
| SOURCE_APP_TOKEN: ${{ steps.source_app_token.outputs.token }} | |
| run: | | |
| set -euo pipefail | |
| if [ "${SOURCE_REPO}" != "${GITHUB_REPOSITORY}" ] && [ -z "${SOURCE_APP_TOKEN:-}" ]; then | |
| echo "Cross-repository strategy watcher requires CROSS_REPO_GITHUB_APP_ID and CROSS_REPO_GITHUB_APP_PRIVATE_KEY." >&2 | |
| exit 1 | |
| fi | |
| - name: Checkout Source Metrics | |
| uses: actions/checkout@v6.0.3 | |
| with: | |
| repository: ${{ env.SOURCE_REPO }} | |
| ref: ${{ env.SOURCE_REF }} | |
| path: source | |
| token: ${{ steps.source_app_token.outputs.token || github.token }} | |
| persist-credentials: false | |
| - name: Fetch strategy metrics from latest Monthly Publish artifact | |
| id: fetch-metrics | |
| env: | |
| GH_TOKEN: ${{ steps.source_app_token.outputs.token || github.token }} | |
| run: | | |
| set -euo pipefail | |
| echo "Looking for latest successful Monthly Publish run on ${SOURCE_REF} in ${SOURCE_REPO}..." | |
| WORKFLOW_FILE="monthly_publish.yml" | |
| # Restrict to the canonical branch so we never consume metrics from a | |
| # feature-branch or PR workflow run. | |
| RUN_ID=$(gh run list --repo "${SOURCE_REPO}" --workflow "${WORKFLOW_FILE}" --branch "${SOURCE_REF}" --status success --limit 1 --json databaseId --jq '.[0].databaseId // ""') | |
| if [ -z "${RUN_ID}" ]; then | |
| echo "No successful Monthly Publish run on ${SOURCE_REF} in ${SOURCE_REPO} — metrics not available yet" | |
| echo "downloaded=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| echo "Found run ${RUN_ID}, downloading artifacts..." | |
| mkdir -p source/data/output | |
| # Artifact download failures (expired artifacts, missing bundles) are | |
| # expected when the monthly publish hasn't run recently or doesn't | |
| # produce the file yet. Let the watcher script skip gracefully. | |
| if ! gh run download "${RUN_ID}" --repo "${SOURCE_REPO}" --dir source/data/output/_artifacts 2>/dev/null; then | |
| echo "Artifact download from run ${RUN_ID} failed (may have expired or not contain the expected bundle)" | |
| echo "downloaded=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| # The artifact name is dynamic; search for strategy_metrics.json in all downloaded artifacts | |
| METRICS_FILE=$(find source/data/output/_artifacts -name "strategy_metrics.json" -type f 2>/dev/null | head -1) | |
| if [ -n "${METRICS_FILE}" ]; then | |
| cp "${METRICS_FILE}" source/data/output/strategy_metrics.json | |
| echo "Downloaded strategy_metrics.json from run ${RUN_ID}" | |
| echo "downloaded=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "strategy_metrics.json not found in Monthly Publish artifacts — source repo may not generate it yet" | |
| echo "downloaded=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Run Strategy Optimization Watcher | |
| env: | |
| GH_TOKEN: ${{ steps.source_app_token.outputs.token || github.token }} | |
| STRATEGY_WATCH_SOURCE_ROOT: ${{ github.workspace }}/source | |
| STRATEGY_WATCH_METRICS_PATH: ${{ env.METRICS_PATH }} | |
| STRATEGY_WATCH_SOURCE_REPO: ${{ env.SOURCE_REPO }} | |
| working-directory: bridge | |
| run: | | |
| set -euo pipefail | |
| mkdir -p data/output/strategy_optimization_watcher | |
| python scripts/run_strategy_optimization_watcher.py | tee data/output/strategy_optimization_watcher/result.json | |
| - name: Upload watcher diagnostics | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: strategy-optimization-watcher-${{ github.run_id }} | |
| path: bridge/data/output/strategy_optimization_watcher/ | |
| if-no-files-found: warn |