Skip to content

fix: harden Phase 4 completeness evidence #710

fix: harden Phase 4 completeness evidence

fix: harden Phase 4 completeness evidence #710

Workflow file for this run

name: Nightly Jeonghan Fanfic Digest
on:
workflow_dispatch:
schedule:
# 22:00 Tehran = 18:30 UTC
- cron: "30 18 * * *"
push:
branches:
- main
- "agent/**"
paths:
- "app/fic_digest.py"
- "app/fic_state.py"
- "app/ai.py"
- "app/message_delivery.py"
- "app/telegram_cloud_state.py"
- "app/x_client.py"
- "app/source_modes.py"
- "app/config.py"
- "config/sources.json"
- "config/jeonghan_priority_x_sources.json"
- "app/telegram.py"
- "tools/state_backup.py"
- "docs/**"
- "tests/**"
- "requirements.txt"
- ".github/workflows/fic-digest.yml"
pull_request:
branches:
- main
paths:
- "app/fic_digest.py"
- "app/fic_state.py"
- "app/ai.py"
- "app/message_delivery.py"
- "app/telegram_cloud_state.py"
- "app/x_client.py"
- "app/source_modes.py"
- "app/config.py"
- "config/sources.json"
- "config/jeonghan_priority_x_sources.json"
- "app/telegram.py"
- "tools/state_backup.py"
- "docs/**"
- "tests/**"
- "requirements.txt"
- ".github/workflows/fic-digest.yml"
permissions:
contents: read
actions: write
concurrency:
group: ${{ (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && 'jeonghan-daily-review-bot-runtime' || format('nightly-jeonghan-fic-validation-{0}', github.ref) }}
cancel-in-progress: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
jobs:
send-digest:
runs-on: ubuntu-latest
timeout-minutes: 25
env:
PYTHONUNBUFFERED: "1"
PIP_DISABLE_PIP_VERSION_CHECK: "1"
STATE_BACKUP_KEY: ${{ secrets.STATE_BACKUP_KEY }}
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Python
uses: actions/setup-python@v6
with:
python-version: "3.11"
cache: pip
- name: Install
run: |
python -m pip install --upgrade "pip>=26.1.2"
python -m pip install -r requirements.txt
python -m pip check
- name: Derive stable encrypted recovery key
if: >-
(github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') &&
env.STATE_BACKUP_KEY == ''
env:
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
shell: bash
run: |
set -euo pipefail
key="$(python - <<'PY'
import os
from app.telegram_cloud_state import ensure_process_backup_key
token = os.environ.get("TELEGRAM_BOT_TOKEN", "").strip()
if not token:
raise SystemExit("TELEGRAM_BOT_TOKEN is required to derive the encrypted recovery key")
ensure_process_backup_key(token)
print(os.environ["STATE_BACKUP_KEY"])
PY
)"
echo "::add-mask::$key"
echo "STATE_BACKUP_KEY=$key" >> "$GITHUB_ENV"
- name: Report encrypted recovery disabled
if: >-
(github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') &&
env.STATE_BACKUP_KEY == ''
run: echo "::warning::Encrypted state recovery is disabled because STATE_BACKUP_KEY is not configured; Actions cache remains best-effort only."
- name: Restore shared private review database cache
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
uses: actions/cache/restore@v5
with:
path: .state/private-review.sqlite3
key: jeonghan-private-review-v1-${{ runner.os }}-${{ github.run_id }}
restore-keys: |
jeonghan-private-review-v1-${{ runner.os }}-
- name: Restore newest valid encrypted recovery database when cache is missing
if: >-
(github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') &&
env.STATE_BACKUP_KEY != '' &&
hashFiles('.state/private-review.sqlite3') == ''
env:
GH_TOKEN: ${{ github.token }}
shell: bash
run: |
set -euo pipefail
work=/tmp/private-state-restore
zip=/tmp/private-state-backup.zip
trap 'rm -rf "$work" "$zip"' EXIT
mapfile -t artifact_ids < <(gh api -H 'Accept: application/vnd.github+json' \
"/repos/${GITHUB_REPOSITORY}/actions/artifacts?name=private-state-backup&per_page=100" \
--jq '[.artifacts[] | select(.expired == false)] | sort_by(.created_at) | reverse | .[].id')
if [ "${#artifact_ids[@]}" -eq 0 ]; then
echo "No encrypted recovery artifact exists yet; starting from cache/empty fic state."
exit 0
fi
selected=""
for artifact_id in "${artifact_ids[@]}"; do
rm -rf "$work" "$zip"
mkdir -p "$work"
if ! gh api -H 'Accept: application/vnd.github+json' \
"/repos/${GITHUB_REPOSITORY}/actions/artifacts/${artifact_id}/zip" > "$zip"; then
echo "::warning::Could not download encrypted recovery artifact ${artifact_id}; trying older backup."
continue
fi
if ! unzip -q "$zip" -d "$work"; then
echo "::warning::Could not unpack encrypted recovery artifact ${artifact_id}; trying older backup."
continue
fi
candidate="$work/private-state-backup.enc"
if [ -f "$candidate" ] && python -m tools.state_backup validate --input "$candidate" --require private-review.sqlite3; then
selected="$candidate"
echo "Selected newest valid encrypted recovery artifact ${artifact_id}."
break
fi
echo "::warning::Encrypted recovery artifact ${artifact_id} did not validate; trying older backup."
done
if [ -z "$selected" ]; then
echo "::error::Encrypted recovery artifacts exist but none authenticated successfully; refusing to run with potentially lost private state."
exit 1
fi
python -m tools.state_backup restore --input "$selected" --state-dir .state
- name: Validate fanfic and shared bot code
run: |
python -m compileall -q app tests tools
python -m app --check
python -m unittest discover -s tests -p "test_*.py" -v
- name: Build and send two separate nightly lists
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
env:
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
TELEGRAM_ADMIN_USER_ID: ${{ secrets.TELEGRAM_ADMIN_USER_ID }}
TELEGRAM_REVIEW_CHAT_ID: ${{ secrets.TELEGRAM_REVIEW_CHAT_ID }}
X_COOKIE: ${{ secrets.X_COOKIE }}
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
GEMINI_MODEL: gemini-3.5-flash-lite
run: python -m app.fic_digest
- name: Checkpoint shared private review database
if: >-
always() &&
(github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') &&
hashFiles('.state/private-review.sqlite3') != ''
run: |
python - <<'PY'
import sqlite3
from pathlib import Path
path = Path('.state/private-review.sqlite3')
conn = sqlite3.connect(path, timeout=15)
try:
result = conn.execute('PRAGMA quick_check').fetchone()
if not result or str(result[0]).lower() != 'ok':
raise SystemExit('private-review.sqlite3 failed quick_check')
conn.execute('PRAGMA wal_checkpoint(TRUNCATE)').fetchall()
finally:
conn.close()
PY
- name: Create authenticated encrypted recovery backup
if: >-
always() &&
(github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') &&
env.STATE_BACKUP_KEY != '' &&
hashFiles('.state/private-review.sqlite3') != ''
run: python -m tools.state_backup encrypt --state-dir .state --output .state/private-state-backup.enc
- name: Upload encrypted recovery backup
if: >-
always() &&
(github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') &&
env.STATE_BACKUP_KEY != '' &&
hashFiles('.state/private-state-backup.enc') != ''
uses: actions/upload-artifact@v4
with:
name: private-state-backup
path: .state/private-state-backup.enc
if-no-files-found: error
retention-days: 3
compression-level: 0
- name: Save shared private review database cache
if: >-
always() &&
(github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') &&
hashFiles('.state/private-review.sqlite3') != ''
uses: actions/cache/save@v5
with:
path: .state/private-review.sqlite3
key: jeonghan-private-review-v1-${{ runner.os }}-${{ github.run_id }}
- name: Resume live assistant after fanfic digest
if: >-
success() &&
(github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') &&
github.ref == 'refs/heads/main'
env:
GH_TOKEN: ${{ github.token }}
shell: bash
run: |
set -euo pipefail
gh_api_retry() {
local attempt output
for attempt in 1 2 3; do
if output="$(gh api "$@")"; then
printf '%s' "$output"
return 0
fi
echo "GitHub API attempt ${attempt}/3 failed; retrying." >&2
sleep $((attempt * 2))
done
return 1
}
gh_workflow_retry() {
local attempt
for attempt in 1 2 3; do
if gh workflow run "$@"; then
return 0
fi
echo "GitHub workflow dispatch attempt ${attempt}/3 failed; retrying." >&2
sleep $((attempt * 2))
done
return 1
}
active=0
for status in pending queued in_progress; do
count="$(
gh_api_retry -H 'Accept: application/vnd.github+json' \
"/repos/${GITHUB_REPOSITORY}/actions/workflows/main.yml/runs?status=${status}&per_page=100" |
jq '[.workflow_runs[] | select(.head_branch == "main")] | length'
)"
active=$((active + count))
done
if [ "$active" -gt 0 ]; then
echo "A live assistant pass is already queued or running."
exit 0
fi
gh_workflow_retry main.yml --ref main -f mode=live
echo "Queued the next live assistant pass after the fanfic digest."