3838 required : false
3939 type : string
4040 default : " "
41+ layout :
42+ description : " Message layout. v1 = current production; sections_v2 = NOTIFY-04 four-section report (try with dry_run first)."
43+ required : false
44+ type : choice
45+ default : v1
46+ options :
47+ - v1
48+ - sections_v2
4149 include_github_evidence :
4250 description : " Query allowlisted platform workflow conclusions (counts stay unknown unless candidates supply them)."
4351 required : true
7987 DIGEST_CANDIDATES_JSON : ${{ secrets.DIGEST_CANDIDATES_JSON }}
8088 # Optional path override (Environment variable); relative to workspace.
8189 DIGEST_CANDIDATES_PATH_VAR : ${{ vars.DIGEST_CANDIDATES_PATH || '' }}
90+ # NOTIFY-04 B2: producer store (unset = disabled). Production value:
91+ # gs://qsl-runtime-logs-shared/digest-candidates (reader SA: prefix-conditioned objectViewer)
92+ # Layout: <uri>/<platform_id>/<business_day>/<target>.json
93+ DIGEST_CANDIDATES_GCS_URI : ${{ vars.DIGEST_CANDIDATES_GCS_URI || '' }}
94+ DIGEST_CANDIDATES_READER_WIF_PROVIDER : ${{ vars.DIGEST_CANDIDATES_READER_WIF_PROVIDER || vars.DIGEST_GCP_WORKLOAD_IDENTITY_PROVIDER || 'projects/252919773759/locations/global/workloadIdentityPools/github-actions/providers/github-main' }}
95+ DIGEST_CANDIDATES_READER_SERVICE_ACCOUNT : ${{ vars.DIGEST_CANDIDATES_READER_SERVICE_ACCOUNT || vars.DIGEST_GCP_WORKLOAD_IDENTITY_SERVICE_ACCOUNT || 'qsl-artifact-evidence@longbridgequant.iam.gserviceaccount.com' }}
8296 # GCP project that hosts a sentinel bot-token replica (LongBridge path).
8397 GCP_PROJECT_ID : ${{ vars.DIGEST_GCP_PROJECT_ID || 'longbridgequant' }}
8498 GCP_WORKLOAD_IDENTITY_PROVIDER : ${{ vars.DIGEST_GCP_WORKLOAD_IDENTITY_PROVIDER || 'projects/252919773759/locations/global/workloadIdentityPools/github-actions/providers/github-main' }}
98112 INPUT_DRY_RUN : ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run || false }}
99113 INPUT_BUSINESS_DAY : ${{ inputs.business_day || '' }}
100114 INPUT_LOCALE : ${{ inputs.locale || '' }}
115+ INPUT_LAYOUT : ${{ inputs.layout || 'v1' }}
101116 INPUT_INCLUDE_GITHUB : ${{ github.event_name != 'workflow_dispatch' && true || inputs.include_github_evidence }}
102117 run : |
103118 set -euo pipefail
@@ -113,6 +128,15 @@ jobs:
113128 fi
114129 echo "business_day=$INPUT_BUSINESS_DAY" >> "$GITHUB_OUTPUT"
115130 echo "locale=$INPUT_LOCALE" >> "$GITHUB_OUTPUT"
131+ # Scheduled runs stay on v1 until the NOTIFY-04 switch-over is approved.
132+ if [[ "${{ github.event_name }}" == "schedule" ]]; then
133+ echo "layout=v1" >> "$GITHUB_OUTPUT"
134+ else
135+ case "$INPUT_LAYOUT" in
136+ sections_v2) echo "layout=sections_v2" >> "$GITHUB_OUTPUT" ;;
137+ *) echo "layout=v1" >> "$GITHUB_OUTPUT" ;;
138+ esac
139+ fi
116140 if [[ "$INPUT_INCLUDE_GITHUB" == "true" ]]; then
117141 echo "include_github=true" >> "$GITHUB_OUTPUT"
118142 else
@@ -152,6 +176,50 @@ jobs:
152176 echo "Wire steps: docs/digest-candidates-wiring.zh-CN.md"
153177 fi
154178
179+ - name : Authenticate to Google Cloud for producer candidates (optional)
180+ id : candidates_gcp_auth
181+ if : ${{ env.DIGEST_CANDIDATES_GCS_URI != '' }}
182+ continue-on-error : true
183+ uses : google-github-actions/auth@v3
184+ with :
185+ workload_identity_provider : ${{ env.DIGEST_CANDIDATES_READER_WIF_PROVIDER }}
186+ service_account : ${{ env.DIGEST_CANDIDATES_READER_SERVICE_ACCOUNT }}
187+
188+ - name : Set up gcloud for producer candidates (optional)
189+ if : ${{ steps.candidates_gcp_auth.outcome == 'success' }}
190+ uses : google-github-actions/setup-gcloud@v3
191+
192+ - name : Fetch producer candidates from private store (optional)
193+ if : ${{ env.DIGEST_CANDIDATES_GCS_URI != '' }}
194+ env :
195+ INPUT_BUSINESS_DAY : ${{ steps.opts.outputs.business_day }}
196+ AUTH_OUTCOME : ${{ steps.candidates_gcp_auth.outcome }}
197+ run : |
198+ set -euo pipefail
199+ # Never echo row contents (may carry equity); counts only.
200+ if [[ "$AUTH_OUTCOME" != "success" ]]; then
201+ echo "Producer candidates store configured but authentication failed; continuing without it." >&2
202+ echo "::warning::digest candidates store auth failed"
203+ exit 0
204+ fi
205+ day="${INPUT_BUSINESS_DAY:-$(TZ=America/New_York date +%F)}"
206+ fetched="data/input/daily-digest/fetched"
207+ mkdir -p "$fetched"
208+ uri="${DIGEST_CANDIDATES_GCS_URI%/}"
209+ found=0
210+ while IFS= read -r object; do
211+ [[ "$object" == *"/${day}/"*.json ]] || continue
212+ rel="${object#"${uri}/"}"
213+ mkdir -p "$fetched/$(dirname "$rel")"
214+ gcloud storage cp "$object" "$fetched/$rel" --no-user-output-enabled
215+ found=$((found + 1))
216+ done < <(gcloud storage ls "${uri}/*/${day}/*.json" 2>/dev/null || true)
217+ echo "Producer candidate files for ${day}: ${found}"
218+ base="${DIGEST_CANDIDATES_PATH:-}"
219+ out="data/input/daily-digest/candidates-merged.json"
220+ python3 python/scripts/merge_digest_candidates.py ${base:+--base "$base"} --input-dir "$fetched" --output "$out"
221+ echo "DIGEST_CANDIDATES_PATH=$out" >> "$GITHUB_ENV"
222+
155223 - name : Authenticate to Google Cloud for sentinel secret (optional)
156224 id : gcp_auth
157225 if : ${{ env.TELEGRAM_TOKEN == '' && env.TELEGRAM_TOKEN_SECRET_NAME != '' && steps.opts.outputs.dry_run != 'true' }}
@@ -177,6 +245,7 @@ jobs:
177245 args=(
178246 python3 python/scripts/send_daily_digest_telegram.py
179247 --write-receipt data/output/daily-digest/receipt.json
248+ --layout "${{ steps.opts.outputs.layout }}"
180249 )
181250 if [[ -n "${{ steps.opts.outputs.locale }}" ]]; then
182251 args+=(--locale "${{ steps.opts.outputs.locale }}")
@@ -211,6 +280,10 @@ jobs:
211280 print(f"- status: `{receipt.get('status')}`")
212281 print(f"- business_day: `{receipt.get('business_day')}`")
213282 print(f"- locale: `{receipt.get('locale')}`")
283+ print(f"- layout: `{receipt.get('layout')}`")
284+ window = receipt.get("window") or {}
285+ if window:
286+ print(f"- window (Asia/Shanghai): `{window.get('start_local')}` → `{window.get('end_local')}`")
214287 print(f"- evidence_status: `{receipt.get('evidence_status')}`")
215288 print(f"- run_count: `{receipt.get('run_count')}`")
216289 print(f"- total_fills: `{receipt.get('total_fills')}`")
@@ -228,6 +301,7 @@ jobs:
228301 print(f" - github_sources_matched: `{coverage.get('github_sources_matched')}`")
229302 print(f" - has_source_failures: `{coverage.get('has_source_failures')}`")
230303 print(f" - is_complete_for_verified_idle: `{coverage.get('is_complete_for_verified_idle')}`")
304+ print(f" - stale_candidates_count: `{coverage.get('stale_candidates_count')}`")
231305 failures = receipt.get("failures") or coverage.get("github_failures") or []
232306 if failures:
233307 print("- failures:")
0 commit comments