Skip to content

build(deps): bump the actions-minor-patch group with 2 updates (#390) #68

build(deps): bump the actions-minor-patch group with 2 updates (#390)

build(deps): bump the actions-minor-patch group with 2 updates (#390) #68

name: concierge-tests
on:
push:
paths: ["Concierge/**", ".github/workflows/concierge-tests.yml"]
pull_request:
paths: ["Concierge/**", ".github/workflows/concierge-tests.yml"]
workflow_dispatch:
env:
# Shared droplet coordinates as repo-level Actions variables (see heartbeat/backend),
# 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 }}
CONCIERGE_HOME: /home/deploy/fingpt/concierge
jobs:
test:
runs-on: ubuntu-latest
# Least-privilege GITHUB_TOKEN (CodeQL actions/missing-workflow-permissions); matches
# the sibling heartbeat-tests.yml. This job only checks out + runs tests — no writes.
permissions:
contents: read
strategy:
fail-fast: false
matrix:
# 3.13 matches the droplet (Fedora 42 ships it — prod parity); 3.12 keeps the
# dev box covered. Both legs must pass before deploy runs (deploy needs: test).
python-version: ["3.12", "3.13"]
defaults:
run:
working-directory: Concierge
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: ${{ matrix.python-version }}
# Cache the pip download/wheel dir, keyed on the requirements hash + Python
# version, so each matrix leg skips re-downloading deps on a cache hit.
cache: pip
cache-dependency-path: Concierge/requirements.txt
- run: pip install -r requirements.txt
- run: python -m pytest -q
deploy:
# Runs only for commits ON main: a normal push/merge to main, or a manual
# workflow_dispatch targeting main (which intentionally re-deploys main HEAD and
# briefly restarts the live bot). PR runs (ref refs/pull/N/merge) and dispatches on
# any non-main branch are skipped — non-main code can never reach the droplet.
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/heartbeat).
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: concierge-deploy
cancel-in-progress: false
permissions:
contents: read
steps:
- 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
# Unlike the heartbeat (a single stdlib script), Concierge is a package + a
# long-lived systemd --user service. The droplet fetches the whole repo tree at
# this exact commit (public repo; the immutable commit SHA is the integrity
# anchor), refreshes the code in place while preserving the venv (.venv/) and the
# durable identity store (data/), reinstalls deps idempotently, byte-compiles as a
# gate, re-installs the unit (so unit edits ship too), then restarts the service.
# The env file (.env.concierge, holds the bot token) is droplet config and stays
# manually managed — see "Deploy" in Concierge/README.md.
- name: Deploy Concierge to droplet
if: ${{ steps.deploy-gate.outputs.enabled == 'true' }}
# SHA-pinned (supply-chain): this action receives the prod SSH deploy key, so a
# moved/compromised tag must not be able to redirect it. 334f925 == v0.1.10.
uses: appleboy/ssh-action@334f9259f2f8eb3376d33fa4c684fff373f2c2a6
env:
REPO: ${{ github.repository }}
COMMIT_SHA: ${{ github.sha }}
with:
host: ${{ env.DROPLET_HOST }}
username: ${{ env.DROPLET_USER }}
key: ${{ secrets.DEPLOY_SSH_KEY }}
port: 22
envs: REPO,COMMIT_SHA,CONCIERGE_HOME
script: |
set -euo pipefail
# systemctl --user needs the session bus addressable from this non-login shell.
export XDG_RUNTIME_DIR="/run/user/$(id -u)"
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
curl -fsSL --retry 3 --retry-delay 5 --retry-all-errors \
"https://codeload.github.com/$REPO/tar.gz/$COMMIT_SHA" -o "$tmp/src.tgz"
tar -xzf "$tmp/src.tgz" -C "$tmp"
src="$(echo "$tmp"/*/Concierge)"
test -f "$src/concierge/__main__.py" # sanity: package present at this commit
# BUILD GATE — validate the STAGED tree before touching the live dir, so a
# broken commit can never replace the running code. pip installs the new
# requirements into the persistent venv; compileall recurses every module (a
# py_compile glob would skip subpackages). Both run against $src and, under
# set -e, abort here — with the live tree still intact — if either fails.
"$CONCIERGE_HOME/.venv/bin/python" -m pip install -q -r "$src/requirements.txt"
"$CONCIERGE_HOME/.venv/bin/python" -m compileall -q "$src/concierge"
# Refresh code: clear the deploy dir EXCEPT runtime state (.venv/ = interpreter
# + deps, data/ = durable identity store), then lay down the validated tree.
# Wiping the whole dir — not just the three code subdirs — means upstream file
# renames and deletions are reflected, not left as stragglers. The env file
# lives outside this dir (/home/deploy/fingpt/envs/.env.concierge), untouched.
find "$CONCIERGE_HOME" -mindepth 1 -maxdepth 1 \
! -name .venv ! -name data -exec rm -rf {} +
cp -a "$src/." "$CONCIERGE_HOME/"
find "$CONCIERGE_HOME" \( -path "$CONCIERGE_HOME/.venv" -o -path "$CONCIERGE_HOME/data" \) -prune \
-o -type d -exec chmod 755 {} +
find "$CONCIERGE_HOME" \( -path "$CONCIERGE_HOME/.venv" -o -path "$CONCIERGE_HOME/data" \) -prune \
-o -type f -exec chmod 644 {} +
# -D creates the unit dir on a fresh droplet; enable is idempotent and marks the
# unit wanted at user-systemd startup (reboot survival also needs linger, which
# is already enabled for the deploy user on this droplet).
install -D -m 644 "$CONCIERGE_HOME/systemd/concierge.service" "$HOME/.config/systemd/user/concierge.service"
systemctl --user daemon-reload
systemctl --user enable concierge.service
# Stamp the cutover instant so the smoke test reads only THIS run's logs, never
# a prior run's stale "connected to Gateway" line (epoch math = POSIX-portable).
restart_epoch="$(date +%s)"
systemctl --user restart concierge.service
# Smoke test: the unit must stay active AND the bot must actually reach the
# Discord Gateway (a clean import can still fail to authenticate). Poll up to
# ~30s — the Gateway handshake lands a few seconds after the process starts.
deadline=$((restart_epoch + 30))
while [ "$(date +%s)" -lt "$deadline" ]; do
if ! systemctl --user is-active --quiet concierge.service; then
echo "FAIL: concierge.service is not active after restart" >&2
journalctl --user -u concierge.service --since "@$restart_epoch" -n 40 --no-pager || true
exit 1
fi
# Capture then match with a pipe-free `case`: piping journalctl into grep -q
# would hand journalctl a SIGPIPE the moment grep matches, and under pipefail
# that 141 would mask a real match as a false-negative deploy failure.
logs="$(journalctl --user -u concierge.service --since "@$restart_epoch" --no-pager 2>/dev/null || true)"
case "$logs" in
*"connected to Gateway"*)
journalctl --user -u concierge.service --since "@$restart_epoch" -n 8 --no-pager || true
echo "Deployed Concierge @ $COMMIT_SHA — active and connected to the Gateway."
exit 0 ;;
esac
sleep 2
done
echo "FAIL: service active but no Gateway connection within 30s" >&2
journalctl --user -u concierge.service --since "@$restart_epoch" -n 40 --no-pager || true
exit 1