Skip to content

feat: support github app cross-repo token #14

feat: support github app cross-repo token

feat: support github app cross-repo token #14

name: Monthly Optimization Planner

Check failure on line 1 in .github/workflows/monthly_optimization_planner.yml

View workflow run for this annotation

GitHub Actions / .github/workflows/monthly_optimization_planner.yml

Invalid workflow file

(Line: 31, Col: 13): Unrecognized named-value: 'secrets'. Located at position 40 within expression: vars.CROSS_REPO_GITHUB_APP_ID != '' && secrets.CROSS_REPO_GITHUB_APP_PRIVATE_KEY != ''
"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/