Skip to content

feat(api): add /api/news/items/ raw news-items endpoint #29

feat(api): add /api/news/items/ raw news-items endpoint

feat(api): add /api/news/items/ raw news-items endpoint #29

Workflow file for this run

name: Heartbeat CI
on:
push:
branches:
- main
paths:
- "Heartbeat/**"
- ".github/workflows/heartbeat-tests.yml"
# Main/backend/api/signals_views.py is deliberately NOT listed here,
# only on pull_request below: this trigger also gates the deploy job,
# whose sole condition is the main-ref check (paths scope the workflow
# RUN, not a job), so a Django-only merge would redeploy the droplet's
# heartbeat scripts — silently shipping any held-back rollout on a
# commit that touched no Heartbeat file.
pull_request:
paths:
- "Heartbeat/**"
- ".github/workflows/heartbeat-tests.yml"
# The backend vendors a port of news_signals.py's validation gate
# (clean_text/validation_gate -> _clean_text/_validate_items) and
# Heartbeat/tests/test_port_parity.py is what pins the two copies
# together, so a port-side edit must run this suite. PR-time is the
# right gate: the point is to make drift impossible to MERGE, and
# deploy skips on PR refs anyway.
- "Main/backend/api/signals_views.py"
workflow_dispatch:
env:
# Shared droplet coordinates as repo-level Actions variables (see backend/concierge),
# so a host/user change is one repo setting, not an edit across three workflows.
DROPLET_HOST: ${{ vars.DROPLET_HOST }}
DROPLET_USER: ${{ vars.DROPLET_USER }}
HEARTBEAT_HOME: /home/deploy/fingpt/heartbeat
jobs:
test:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
# The heartbeat is stdlib-only by design (see Heartbeat/README.md):
# the runner's system python3 is all it needs — no setup, no deps.
- name: Run heartbeat unit tests
working-directory: Heartbeat
run: python3 -m unittest discover -s tests -v
deploy:
# Tests run everywhere; the droplet only ever tracks main. (PR runs have
# ref refs/pull/N/merge, dispatches on a branch have that branch's ref —
# both fall through to skipped.)
if: ${{ github.ref == 'refs/heads/main' }}
runs-on: ubuntu-latest
needs: test
# DEPLOY_SSH_KEY is an ENVIRONMENT secret (Production: deployment branch
# policy = main only) -- non-main refs cannot release the prod SSH key even
# if the ref-gate above is edited away (same stanza as backend/concierge).
environment: Production
# Serialize deploys so two merges in quick succession can't land
# out of order and leave the droplet on the older commit.
concurrency:
group: heartbeat-deploy
cancel-in-progress: false
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Determine deploy eligibility
id: deploy-gate
env:
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
run: |
set -euo pipefail
if [[ -n "${DEPLOY_SSH_KEY:-}" ]]; then
echo "enabled=true" >> "$GITHUB_OUTPUT"
else
echo "enabled=false" >> "$GITHUB_OUTPUT"
fi
- name: Compute artifact checksums
id: artifact
run: |
echo "sha256=$(sha256sum Heartbeat/news_heartbeat.py | cut -d' ' -f1)" >> "$GITHUB_OUTPUT"
echo "signals_sha256=$(sha256sum Heartbeat/news_signals.py | cut -d' ' -f1)" >> "$GITHUB_OUTPUT"
# The droplet fetches the script straight from this commit (the repo is
# public), verifies it against the checksum computed from the checkout
# above, byte-compiles it, then installs via an atomic same-directory
# rename so the heartbeat timer can never observe a half-written file.
# The systemd units and env file are droplet config and stay manually
# managed — see "Deploy" in Heartbeat/README.md.
- name: Deploy heartbeat script to droplet
if: ${{ steps.deploy-gate.outputs.enabled == 'true' }}
uses: appleboy/ssh-action@334f9259f2f8eb3376d33fa4c684fff373f2c2a6 # v0.1.10, SHA-pinned (holds the prod SSH key)
env:
REPO: ${{ github.repository }}
COMMIT_SHA: ${{ github.sha }}
EXPECTED_SHA256: ${{ steps.artifact.outputs.sha256 }}
EXPECTED_SIGNALS_SHA256: ${{ steps.artifact.outputs.signals_sha256 }}
with:
host: ${{ env.DROPLET_HOST }}
username: ${{ env.DROPLET_USER }}
key: ${{ secrets.DEPLOY_SSH_KEY }}
port: 22
envs: REPO,COMMIT_SHA,EXPECTED_SHA256,HEARTBEAT_HOME,EXPECTED_SIGNALS_SHA256
script: |
set -euo pipefail
# deliberately no `local`: bash runs the EXIT trap after the
# function returns, so the trap must see a global $tmp
deploy_one() {
name=$1 expected_sha=$2
tmp=$(mktemp "$HEARTBEAT_HOME/.$name.deploy.XXXXXX")
trap 'rm -f "$tmp"' EXIT
curl -fsSL --retry 3 --retry-delay 5 --retry-all-errors "https://raw.githubusercontent.com/$REPO/$COMMIT_SHA/Heartbeat/$name.py" -o "$tmp"
echo "$expected_sha $tmp" | sha256sum -c -
python3 -m py_compile "$tmp"
grep -m1 '^VERSION' "$tmp"
rm -rf "$HEARTBEAT_HOME/__pycache__"
chmod 644 "$tmp"
mv "$tmp" "$HEARTBEAT_HOME/$name.py"
trap - EXIT
echo "Deployed $name.py @ $COMMIT_SHA"
}
deploy_one news_heartbeat "$EXPECTED_SHA256"
deploy_one news_signals "$EXPECTED_SIGNALS_SHA256"