fix(strategy): mirror strict canary provenance checks #3
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: Reusable Drift Check | ||
| on: | ||
| workflow_call: | ||
| inputs: | ||
| strategy_domain: | ||
| required: true | ||
| type: string | ||
| snapshot_repository: | ||
| required: true | ||
| type: string | ||
| snapshot_checkout_path: | ||
| required: true | ||
| type: string | ||
| snapshot_repository_ref: | ||
| required: false | ||
| type: string | ||
| default: "main" | ||
| python_version: | ||
| required: false | ||
| type: string | ||
| default: "3.11" | ||
| quant_platform_kit_ref: | ||
| required: false | ||
| type: string | ||
| default: "main" | ||
| ai_gateway_service_url: | ||
| required: false | ||
| type: string | ||
| default: "" | ||
| lifecycle_performance_bucket: | ||
| required: false | ||
| type: string | ||
| default: "" | ||
| caller_event_name: | ||
| required: true | ||
| type: string | ||
| caller_pr_head_repository: | ||
| required: false | ||
| type: string | ||
| default: "" | ||
| lifecycle_preflight_artifact: | ||
| required: false | ||
| type: string | ||
| default: "" | ||
| secrets: | ||
| codex_audit_service_url: | ||
| required: false | ||
| snapshot_repository_token: | ||
| required: false | ||
| permissions: | ||
| contents: read | ||
| issues: write | ||
| id-token: write | ||
| jobs: | ||
| drift: | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 15 | ||
| env: | ||
| FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" | ||
| STRATEGY_DOMAIN: ${{ inputs.strategy_domain }} | ||
| QUANT_PROJECTS_ROOT: ${{ github.workspace }}/external | ||
| LIFECYCLE_PERFORMANCE_BUCKET: ${{ inputs.lifecycle_performance_bucket || vars.LIFECYCLE_PERFORMANCE_BUCKET || '' }} | ||
| LIFECYCLE_LOCAL_ROOT: ${{ github.workspace }}/data/lifecycle_store | ||
| CRYPTO_LIFECYCLE_PREFLIGHT_ROOT: ${{ runner.temp }}/lifecycle-preflight | ||
| steps: | ||
| - name: Validate trusted caller | ||
| env: | ||
| CALLER_REPOSITORY: ${{ github.repository }} | ||
| CALLER_EVENT_NAME: ${{ inputs.caller_event_name }} | ||
| CALLER_PR_HEAD_REPOSITORY: ${{ inputs.caller_pr_head_repository }} | ||
| SNAPSHOT_REPOSITORY: ${{ inputs.snapshot_repository }} | ||
| shell: bash | ||
| run: | | ||
| set -euo pipefail | ||
| caller_repo="$CALLER_REPOSITORY" | ||
| caller_event="$CALLER_EVENT_NAME" | ||
| caller_pr_head_repo="$CALLER_PR_HEAD_REPOSITORY" | ||
| snapshot_repo="$SNAPSHOT_REPOSITORY" | ||
| expected_snapshot_repo="" | ||
| case "$caller_repo" in | ||
| QuantStrategyLab/CnEquityStrategies) | ||
| expected_snapshot_repo="QuantStrategyLab/CnEquitySnapshotPipelines" | ||
| ;; | ||
| QuantStrategyLab/HkEquityStrategies) | ||
| expected_snapshot_repo="QuantStrategyLab/HkEquitySnapshotPipelines" | ||
| ;; | ||
| QuantStrategyLab/UsEquityStrategies) | ||
| expected_snapshot_repo="QuantStrategyLab/UsEquitySnapshotPipelines" | ||
| ;; | ||
| QuantStrategyLab/CryptoStrategies) | ||
| expected_snapshot_repo="QuantStrategyLab/CryptoLivePoolPipelines" | ||
| ;; | ||
| *) | ||
| echo "::error::Untrusted reusable workflow caller: $caller_repo" | ||
| exit 1 | ||
| ;; | ||
| esac | ||
| case "$caller_event" in | ||
| schedule|workflow_dispatch) | ||
| ;; | ||
| pull_request|pull_request_target) | ||
| if [ -n "$caller_pr_head_repo" ] && [ "$caller_pr_head_repo" != "$caller_repo" ]; then | ||
| echo "::error::Fork pull_request callers are not trusted: $caller_pr_head_repo" | ||
| else | ||
| echo "::error::pull_request callers are not trusted: $caller_repo" | ||
| fi | ||
| exit 1 | ||
| ;; | ||
| "") | ||
| echo "::error::caller_event_name must be provided by the caller workflow" | ||
| exit 1 | ||
| ;; | ||
| *) | ||
| echo "::error::Unsupported caller_event_name: $caller_event" | ||
| exit 1 | ||
| ;; | ||
| esac | ||
| if [ "$snapshot_repo" != "$expected_snapshot_repo" ]; then | ||
| echo "::error::snapshot_repository mismatch for caller $caller_repo: $snapshot_repo" | ||
| exit 1 | ||
| fi | ||
| - name: Checkout | ||
| uses: actions/checkout@v6 | ||
| - name: Checkout snapshot pipeline repo | ||
| uses: actions/checkout@v6 | ||
| with: | ||
| repository: ${{ inputs.snapshot_repository }} | ||
| ref: ${{ inputs.snapshot_repository_ref }} | ||
| token: ${{ secrets.snapshot_repository_token || github.token }} | ||
| path: ${{ inputs.snapshot_checkout_path }} | ||
| - name: Checkout QuantPlatformKit | ||
| uses: actions/checkout@v6 | ||
| with: | ||
| repository: QuantStrategyLab/QuantPlatformKit | ||
| ref: ${{ inputs.quant_platform_kit_ref }} | ||
| path: external/QuantPlatformKit | ||
| - name: Set up Python | ||
| uses: actions/setup-python@v6 | ||
| with: | ||
| python-version: ${{ inputs.python_version }} | ||
| - name: Install dependencies | ||
| run: | | ||
| set -euo pipefail | ||
| python -m pip install --upgrade pip | ||
| python -m pip install -e . pandas | ||
| python -m pip install --no-deps -e external/QuantPlatformKit | ||
| - name: Download lifecycle preflight artifact | ||
| if: inputs.lifecycle_preflight_artifact != '' | ||
| uses: actions/download-artifact@v4 | ||
| with: | ||
| name: ${{ inputs.lifecycle_preflight_artifact }} | ||
| path: ${{ runner.temp }}/lifecycle-preflight | ||
| - name: Restore lifecycle preflight inputs | ||
| if: inputs.lifecycle_preflight_artifact != '' | ||
| env: | ||
| PREFLIGHT_BUNDLE_ROOT: ${{ runner.temp }}/lifecycle-preflight | ||
| SNAPSHOT_CHECKOUT_PATH: ${{ inputs.snapshot_checkout_path }} | ||
| shell: bash | ||
| run: | | ||
| set -euo pipefail | ||
| case "$SNAPSHOT_CHECKOUT_PATH" in | ||
| external/*) ;; | ||
| *) echo "::error::snapshot_checkout_path must remain under external/"; exit 1 ;; | ||
| esac | ||
| if [[ "$SNAPSHOT_CHECKOUT_PATH" == *".."* ]]; then | ||
| echo "::error::snapshot_checkout_path must not contain traversal segments" | ||
| exit 1 | ||
| fi | ||
| if find "$PREFLIGHT_BUNDLE_ROOT" -type l -print -quit | grep -q .; then | ||
| echo "::error::lifecycle preflight artifact must not contain symlinks" | ||
| exit 1 | ||
| fi | ||
| lifecycle_source="$PREFLIGHT_BUNDLE_ROOT/data/lifecycle_store" | ||
| snapshot_source="$PREFLIGHT_BUNDLE_ROOT/$SNAPSHOT_CHECKOUT_PATH/data/output" | ||
| if [ ! -d "$lifecycle_source" ] || [ ! -d "$snapshot_source" ]; then | ||
| echo "::error::lifecycle preflight artifact is missing required allowlisted directories" | ||
| exit 1 | ||
| fi | ||
| snapshot_root="$GITHUB_WORKSPACE/$SNAPSHOT_CHECKOUT_PATH" | ||
| resolved_snapshot_root="$(realpath "$snapshot_root")" | ||
| case "$resolved_snapshot_root" in | ||
| "$GITHUB_WORKSPACE"/external/*) ;; | ||
| *) echo "::error::snapshot checkout resolved outside the external workspace"; exit 1 ;; | ||
| esac | ||
| if find "$snapshot_root" -type l -print -quit | grep -q .; then | ||
| echo "::error::snapshot checkout must not contain symlinks" | ||
| exit 1 | ||
| fi | ||
| snapshot_target="$snapshot_root/data/output" | ||
| rm -rf -- "$LIFECYCLE_LOCAL_ROOT" "$snapshot_target" | ||
| mkdir -p "$LIFECYCLE_LOCAL_ROOT" "$GITHUB_WORKSPACE/$SNAPSHOT_CHECKOUT_PATH/data/output" | ||
| cp -a "$lifecycle_source/." "$LIFECYCLE_LOCAL_ROOT/" | ||
| cp -a "$snapshot_source/." "$snapshot_target/" | ||
| echo "CRYPTO_LIFECYCLE_PREFLIGHT_ROOT=$PREFLIGHT_BUNDLE_ROOT" >> "$GITHUB_ENV" | ||
| - name: Build lifecycle performance snapshots | ||
| run: quant-lifecycle monitor --domain ${{ inputs.strategy_domain }} | ||
| - name: Validate lifecycle prerequisites | ||
| run: quant-lifecycle doctor --domain ${{ inputs.strategy_domain }} --require-snapshot --require-backtest --max-freshness-days 7 | ||
| - name: Run drift detection | ||
| run: quant-lifecycle drift --domain ${{ inputs.strategy_domain }} --no-alerts | ||
| - name: Sync drift alerts to GitHub Issues | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
| GITHUB_REPOSITORY: ${{ github.repository }} | ||
| run: | | ||
| python - <<'PY' | ||
| import os | ||
| from quant_platform_kit.strategy_lifecycle.codex_integration import create_issues_for_domain | ||
| owner, _, repository = os.environ["GITHUB_REPOSITORY"].partition("/") | ||
| if not owner or not repository: | ||
| raise RuntimeError("GITHUB_REPOSITORY must identify the reusable workflow caller") | ||
| os.environ["CODEX_AUDIT_ORG"] = owner | ||
| os.environ["CODEX_AUDIT_ORCHESTRATOR_REPO"] = repository | ||
| results = create_issues_for_domain("${{ inputs.strategy_domain }}") | ||
| created = [item for item in results if item.get("issue_url")] | ||
| print({"created_issues": len(created), "results": len(results)}) | ||
| PY | ||
| - name: Checkout AIAuditBridge | ||
| uses: actions/checkout@v6 | ||
| with: | ||
| repository: QuantStrategyLab/AIAuditBridge | ||
| ref: 59192b259bfbc4c959e7799ae514e2e5d4925498 | ||
| path: external/AIAuditBridge | ||
| - name: Dual-review critical drift | ||
| env: | ||
| AIAUDIT_BRIDGE_ROOT: external/AIAuditBridge | ||
| CODEX_AUDIT_SERVICE_URL: ${{ secrets.codex_audit_service_url }} | ||
| AI_GATEWAY_SERVICE_URL: ${{ inputs.ai_gateway_service_url }} | ||
| GH_TOKEN: ${{ github.token }} | ||
| run: | | ||
| set -euo pipefail | ||
| script="external/AIAuditBridge/scripts/run_drift_dual_review.py" | ||
| if [ ! -f "$script" ]; then | ||
| echo "::notice::dual-review scripts unavailable; skipping until AIAuditBridge is merged" | ||
| exit 0 | ||
| fi | ||
| if [ -z "${CODEX_AUDIT_SERVICE_URL:-}" ]; then | ||
| echo "::notice::CODEX_AUDIT_SERVICE_URL not configured; skipping dual-review dispatch" | ||
| exit 0 | ||
| fi | ||
| review_output="${RUNNER_TEMP}/drift-dual-review.json" | ||
| set +e | ||
| PYTHONPATH=external/AIAuditBridge python "$script" \ | ||
| --domain "${STRATEGY_DOMAIN}" --dispatch >"$review_output" | ||
| review_rc=$? | ||
| set -e | ||
| cat "$review_output" | ||
| if [ "$review_rc" -eq 3 ]; then | ||
| python - "$review_output" <<'PY' | ||
| import json | ||
| import sys | ||
| payload = json.load(open(sys.argv[1], encoding="utf-8")) | ||
| degraded = [item for item in payload.get("results", []) if item.get("degraded")] | ||
| if not payload.get("degraded") or not degraded: | ||
| raise SystemExit("exit code 3 requires degraded review results") | ||
| missing = [item for item in degraded if not item.get("dispatch", {}).get("github_issue")] | ||
| if missing: | ||
| raise SystemExit("degraded review requires a durable GitHub issue") | ||
| PY | ||
| echo "::warning::Dual review providers unavailable; durable issues created and critical drift alerts remain active" | ||
| exit 0 | ||
| fi | ||
| exit "$review_rc" | ||