feat(api): add /api/news/items/ raw news-items endpoint #29
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: 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" |