feat: support github app cross-repo token #14
Workflow file for this run
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: Monthly Optimization Planner | ||
|
Check failure on line 1 in .github/workflows/monthly_optimization_planner.yml
|
||
| "on": | ||
| workflow_dispatch: | ||
| inputs: | ||
| upstream_run_id: | ||
| description: "AI review run id from CryptoLeaderRotation" | ||
| required: true | ||
| downstream_run_id: | ||
| description: "AI review run id from BinancePlatform" | ||
| required: true | ||
| downstream_repo: | ||
| description: "Downstream execution repo" | ||
| required: true | ||
| default: "QuantStrategyLab/BinancePlatform" | ||
| jobs: | ||
| planner: | ||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| actions: write | ||
| contents: read | ||
| issues: write | ||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@v6 | ||
| - name: Create GitHub App token for cross-repo access | ||
| id: cross_repo_app_token | ||
| if: vars.CROSS_REPO_GITHUB_APP_ID != '' && secrets.CROSS_REPO_GITHUB_APP_PRIVATE_KEY != '' | ||
| uses: actions/create-github-app-token@v2 | ||
| with: | ||
| app-id: ${{ vars.CROSS_REPO_GITHUB_APP_ID }} | ||
| private-key: ${{ secrets.CROSS_REPO_GITHUB_APP_PRIVATE_KEY }} | ||
| owner: ${{ github.repository_owner }} | ||
| repositories: | | ||
| CryptoLeaderRotation | ||
| BinancePlatform | ||
| CryptoStrategies | ||
| permission-actions: write | ||
| permission-issues: write | ||
| - name: Resolve cross-repo access token | ||
| id: cross_repo_token | ||
| env: | ||
| APP_TOKEN: ${{ steps.cross_repo_app_token.outputs.token }} | ||
| FALLBACK_TOKEN: ${{ secrets.CROSS_REPO_GITHUB_TOKEN }} | ||
| run: | | ||
| if [ -n "$APP_TOKEN" ]; then | ||
| echo "token=$APP_TOKEN" >> "$GITHUB_OUTPUT" | ||
| echo "source=github_app" >> "$GITHUB_OUTPUT" | ||
| elif [ -n "$FALLBACK_TOKEN" ]; then | ||
| echo "token=$FALLBACK_TOKEN" >> "$GITHUB_OUTPUT" | ||
| echo "source=personal_access_token" >> "$GITHUB_OUTPUT" | ||
| else | ||
| echo "Missing cross-repo credentials. Configure GitHub App secrets or CROSS_REPO_GITHUB_TOKEN." >&2 | ||
| exit 1 | ||
| fi | ||
| - name: Download upstream AI review artifact | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| run: | | ||
| mkdir -p data/input/upstream | ||
| gh run download "${{ inputs.upstream_run_id }}" \ | ||
| --repo "${GITHUB_REPOSITORY}" \ | ||
| --dir data/input/upstream | ||
| - name: Download downstream AI review artifact | ||
| env: | ||
| GH_TOKEN: ${{ steps.cross_repo_token.outputs.token }} | ||
| run: | | ||
| mkdir -p data/input/downstream | ||
| gh run download "${{ inputs.downstream_run_id }}" \ | ||
| --repo "${{ inputs.downstream_repo }}" \ | ||
| --dir data/input/downstream | ||
| - name: Resolve downloaded artifact paths | ||
| id: artifact_paths | ||
| run: | | ||
| UPSTREAM_DIR=$(find data/input/upstream -mindepth 1 -maxdepth 1 -type d | head -1) | ||
| DOWNSTREAM_DIR=$(find data/input/downstream -mindepth 1 -maxdepth 1 -type d | head -1) | ||
| if [ -z "${UPSTREAM_DIR}" ] || [ -z "${DOWNSTREAM_DIR}" ]; then | ||
| echo "Failed to resolve downloaded artifact directories" >&2 | ||
| exit 1 | ||
| fi | ||
| { | ||
| echo "upstream_dir=${UPSTREAM_DIR}" | ||
| echo "downstream_dir=${DOWNSTREAM_DIR}" | ||
| } >> "${GITHUB_OUTPUT}" | ||
| - name: Prepare upstream review payload | ||
| run: | | ||
| python3 scripts/build_ai_review_payload.py \ | ||
| --source-repo "${GITHUB_REPOSITORY}" \ | ||
| --review-kind upstream_selector \ | ||
| --issue-context-file "${{ steps.artifact_paths.outputs.upstream_dir }}/issue_context.json" \ | ||
| --secondary-review-file "${{ steps.artifact_paths.outputs.upstream_dir }}/secondary_review.json" \ | ||
| --run-url "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${{ inputs.upstream_run_id }}" \ | ||
| --output-file data/output/prepared/upstream_review_payload.json | ||
| - name: Prepare downstream review payload | ||
| run: | | ||
| python3 scripts/build_ai_review_payload.py \ | ||
| --source-repo "${{ inputs.downstream_repo }}" \ | ||
| --review-kind execution_runtime \ | ||
| --issue-context-file "${{ steps.artifact_paths.outputs.downstream_dir }}/issue_context.json" \ | ||
| --secondary-review-file "${{ steps.artifact_paths.outputs.downstream_dir }}/secondary_review.json" \ | ||
| --run-url "${GITHUB_SERVER_URL}/${{ inputs.downstream_repo }}/actions/runs/${{ inputs.downstream_run_id }}" \ | ||
| --output-file data/output/prepared/downstream_review_payload.json | ||
| - name: Build monthly optimization plan | ||
| run: | | ||
| python3 scripts/build_monthly_optimization_plan.py \ | ||
| --upstream-review-file data/output/prepared/upstream_review_payload.json \ | ||
| --downstream-review-file data/output/prepared/downstream_review_payload.json \ | ||
| --output-dir data/output/monthly_optimization | ||
| - name: Append optimization summary | ||
| run: cat data/output/monthly_optimization/optimization_summary.md >> "$GITHUB_STEP_SUMMARY" | ||
| - name: Create monthly optimization issue | ||
| id: optimization_issue | ||
| run: | | ||
| issue_output=$(python3 scripts/post_monthly_optimization_issue.py \ | ||
| --repo "${GITHUB_REPOSITORY}" \ | ||
| --plan-file data/output/monthly_optimization/optimization_plan.json \ | ||
| --summary-file data/output/monthly_optimization/optimization_summary.md) | ||
| echo "$issue_output" | ||
| issue_number=$(printf '%s\n' "$issue_output" | awk -F= '/^issue_number=/{print $2}' | tail -1) | ||
| if [ -z "$issue_number" ]; then | ||
| echo "Failed to capture optimization issue number" >&2 | ||
| exit 1 | ||
| fi | ||
| echo "issue_number=$issue_number" >> "$GITHUB_OUTPUT" | ||
| env: | ||
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| - name: Fan out CryptoLeaderRotation task issue | ||
| run: | | ||
| python3 scripts/fanout_monthly_optimization_tasks.py \ | ||
| --plan-file data/output/monthly_optimization/optimization_plan.json \ | ||
| --owner-repo CryptoLeaderRotation \ | ||
| --repo "${GITHUB_REPOSITORY}" \ | ||
| --planner-issue-url "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/issues/${{ steps.optimization_issue.outputs.issue_number }}" \ | ||
| --output-file data/output/monthly_optimization/fanout/crypto_leader_rotation.json | ||
| env: | ||
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| - name: Fan out CryptoStrategies task issue | ||
| run: | | ||
| python3 scripts/fanout_monthly_optimization_tasks.py \ | ||
| --plan-file data/output/monthly_optimization/optimization_plan.json \ | ||
| --owner-repo CryptoStrategies \ | ||
| --repo "QuantStrategyLab/CryptoStrategies" \ | ||
| --planner-issue-url "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/issues/${{ steps.optimization_issue.outputs.issue_number }}" \ | ||
| --output-file data/output/monthly_optimization/fanout/crypto_strategies.json \ | ||
| --allow-permission-skip | ||
| env: | ||
| GITHUB_TOKEN: ${{ steps.cross_repo_token.outputs.token }} | ||
| - name: Fan out BinancePlatform task issue | ||
| run: | | ||
| python3 scripts/fanout_monthly_optimization_tasks.py \ | ||
| --plan-file data/output/monthly_optimization/optimization_plan.json \ | ||
| --owner-repo BinancePlatform \ | ||
| --repo "${{ inputs.downstream_repo }}" \ | ||
| --planner-issue-url "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/issues/${{ steps.optimization_issue.outputs.issue_number }}" \ | ||
| --output-file data/output/monthly_optimization/fanout/binance_platform.json \ | ||
| --allow-permission-skip | ||
| env: | ||
| GITHUB_TOKEN: ${{ steps.cross_repo_token.outputs.token }} | ||
| - name: Append fanout summary | ||
| run: | | ||
| python3 - <<'PY' >> "$GITHUB_STEP_SUMMARY" | ||
| import json | ||
| from pathlib import Path | ||
| result_dir = Path("data/output/monthly_optimization/fanout") | ||
| print("\n## Repo Task Fanout") | ||
| for path in sorted(result_dir.glob("*.json")): | ||
| data = json.loads(path.read_text(encoding="utf-8")) | ||
| line = f"- **{data['owner_repo']}** `{data['status']}`" | ||
| if data.get("issue_url"): | ||
| line += f": {data['issue_url']}" | ||
| elif data.get("reason"): | ||
| line += f": {data['reason']}" | ||
| print(line) | ||
| PY | ||
| - name: Resolve upstream experiment validation target | ||
| id: upstream_experiment_target | ||
| run: | | ||
| python3 - <<'PY' | ||
| import json | ||
| import os | ||
| from pathlib import Path | ||
| fanout = json.loads( | ||
| Path("data/output/monthly_optimization/fanout/crypto_leader_rotation.json").read_text(encoding="utf-8") | ||
| ) | ||
| plan = json.loads( | ||
| Path("data/output/monthly_optimization/optimization_plan.json").read_text(encoding="utf-8") | ||
| ) | ||
| actions = plan.get("repo_action_summary", {}).get("CryptoLeaderRotation", {}).get("actions", []) | ||
| should_dispatch = bool(fanout.get("issue_number")) and fanout.get("status") in {"created", "updated"} and any( | ||
| action.get("experiment_only") for action in actions | ||
| ) | ||
| with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as output: | ||
| print(f"should_dispatch={'true' if should_dispatch else 'false'}", file=output) | ||
| print(f"issue_number={fanout.get('issue_number') or ''}", file=output) | ||
| PY | ||
| - name: Dispatch CryptoLeaderRotation experiment validation | ||
| if: steps.upstream_experiment_target.outputs.should_dispatch == 'true' | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| run: | | ||
| gh workflow run experiment_validation.yml \ | ||
| -f issue_number="${{ steps.upstream_experiment_target.outputs.issue_number }}" | ||
| - name: Resolve downstream experiment validation target | ||
| id: downstream_experiment_target | ||
| run: | | ||
| python3 - <<'PY' | ||
| import json | ||
| import os | ||
| from pathlib import Path | ||
| fanout = json.loads( | ||
| Path("data/output/monthly_optimization/fanout/binance_platform.json").read_text(encoding="utf-8") | ||
| ) | ||
| plan = json.loads( | ||
| Path("data/output/monthly_optimization/optimization_plan.json").read_text(encoding="utf-8") | ||
| ) | ||
| actions = plan.get("repo_action_summary", {}).get("BinancePlatform", {}).get("actions", []) | ||
| should_dispatch = bool(fanout.get("issue_number")) and fanout.get("status") in {"created", "updated"} and any( | ||
| action.get("experiment_only") for action in actions | ||
| ) | ||
| with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as output: | ||
| print(f"should_dispatch={'true' if should_dispatch else 'false'}", file=output) | ||
| print(f"issue_number={fanout.get('issue_number') or ''}", file=output) | ||
| PY | ||
| - name: Best-effort label BinancePlatform issue for experiment validation | ||
| if: steps.downstream_experiment_target.outputs.should_dispatch == 'true' | ||
| env: | ||
| GH_TOKEN: ${{ steps.cross_repo_token.outputs.token }} | ||
| TARGET_REPO: ${{ inputs.downstream_repo }} | ||
| ISSUE_NUMBER: ${{ steps.downstream_experiment_target.outputs.issue_number }} | ||
| run: | | ||
| set +e | ||
| gh label create experiment-validation --repo "$TARGET_REPO" --color 1D76DB --description "Trigger experiment validation for monthly optimization tasks" --force | ||
| label_status=$? | ||
| gh issue edit "$ISSUE_NUMBER" --repo "$TARGET_REPO" --add-label experiment-validation | ||
| issue_status=$? | ||
| set -e | ||
| if [ "$label_status" -ne 0 ] || [ "$issue_status" -ne 0 ]; then | ||
| echo "Downstream experiment-validation label update skipped for $TARGET_REPO#$ISSUE_NUMBER." >> "$GITHUB_STEP_SUMMARY" | ||
| fi | ||
| - name: Dispatch BinancePlatform experiment validation | ||
| if: steps.downstream_experiment_target.outputs.should_dispatch == 'true' | ||
| env: | ||
| GH_TOKEN: ${{ steps.cross_repo_token.outputs.token }} | ||
| run: | | ||
| set +e | ||
| gh workflow run experiment_validation.yml \ | ||
| -R "${{ inputs.downstream_repo }}" \ | ||
| -f issue_number="${{ steps.downstream_experiment_target.outputs.issue_number }}" | ||
| status=$? | ||
| set -e | ||
| if [ "$status" -ne 0 ]; then | ||
| echo "Downstream experiment validation dispatch skipped: ${{ steps.cross_repo_token.outputs.source }} token likely needs Actions write on ${{ inputs.downstream_repo }}." >> "$GITHUB_STEP_SUMMARY" | ||
| fi | ||
| - name: Upload planner artifact | ||
| uses: actions/upload-artifact@v7 | ||
| with: | ||
| name: monthly-optimization-plan-${{ inputs.upstream_run_id }}-${{ inputs.downstream_run_id }} | ||
| path: data/output/monthly_optimization/ | ||