Prune expired sessions #8
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
| # Daily cleanup: delete chat sessions whose auto_delete_at has passed — platform transcript | |
| # first, then the DB row (lik-ui/scripts/prune_sessions.py). Runs unattended on a schedule | |
| # with no internet-facing endpoint and no long-lived shared secret: GitHub OIDC assumes the | |
| # SSM-read role, fetches the shared Anthropic key + DB master password from SSM, and connects | |
| # to the public Lightsail Postgres directly. See | |
| # docs/plans/2026-07-28-001-feat-session-auto-delete-plan.md. | |
| # | |
| # Prerequisites (set once in repo Settings, on the `prod` environment): | |
| # - Variable AWS_SSM_READ_ROLE_ARN = the github_ssm_read_role_arn Terraform output. | |
| # - Variable AWS_REGION = us-east-1. | |
| # - Variable SSM_PREFIX = the Terraform var.ssm_prefix (e.g. /ik-arch/prod). Required, set on | |
| # the environment (resolves per the job's `environment:`), alongside AWS_REGION and the role. | |
| # No LIK_UI_DB_* variables are needed: the DB password + Anthropic key are read from SSM, the DB | |
| # target (instance + db name) from the Terraform-authored $SSM_PREFIX/config/ params, and the DB | |
| # host/port/master-user are discovered from the Lightsail instance. All at run time — never GitHub | |
| # secrets/variables. | |
| name: Prune expired sessions | |
| on: | |
| schedule: | |
| - cron: "17 8 * * *" # daily at 08:17 UTC | |
| workflow_dispatch: {} # manual run for verification / catch-up | |
| permissions: | |
| id-token: write # required for OIDC | |
| contents: read | |
| issues: write # so a failed run can open a tracking issue (this is the only alerting) | |
| # Never let two cleanups overlap (a slow run + the next tick). | |
| concurrency: | |
| group: prune-sessions | |
| cancel-in-progress: false | |
| jobs: | |
| prune: | |
| runs-on: ubuntu-latest | |
| # Fail fast instead of hanging: an unattended run that stalls on a slow DB/API otherwise | |
| # holds the concurrency group and starves the next scheduled tick. | |
| timeout-minutes: 15 | |
| # The prod environment scopes the vars/role below and its `main` branch policy; scheduled | |
| # runs execute on the default branch, which satisfies it. Env-scoped vars only resolve | |
| # when the job declares this. | |
| environment: prod | |
| env: | |
| SSM_PREFIX: ${{ vars.SSM_PREFIX }} | |
| LIK_UI_ENV: prod | |
| LIK_UI_DB_SSLMODE: require | |
| # DB_INSTANCE + LIK_UI_DB_NAME are no longer hardcoded here: they are read at run time from | |
| # the Terraform-authored $SSM_PREFIX/config/ params (infra/config.tf), so a CI copy can | |
| # never drift from infra. | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v7 | |
| - name: Configure AWS credentials (OIDC) | |
| uses: aws-actions/configure-aws-credentials@v6 | |
| with: | |
| role-to-assume: ${{ vars.AWS_SSM_READ_ROLE_ARN }} | |
| aws-region: ${{ vars.AWS_REGION }} | |
| - name: Fetch config + secrets from SSM | |
| run: | | |
| # Non-secret config (String params) — resolve the DB target together, one source. | |
| # A missing param makes get-parameter exit non-zero, which aborts the step (bash -e): | |
| # a "staging" run can never half-resolve to the prod instance, or vice versa. | |
| inst=$(aws ssm get-parameter --name "$SSM_PREFIX/config/DB_INSTANCE" \ | |
| --query Parameter.Value --output text) | |
| echo "DB_INSTANCE=$inst" >> "$GITHUB_ENV" | |
| uidb=$(aws ssm get-parameter --name "$SSM_PREFIX/config/LIK_UI_DB_NAME" \ | |
| --query Parameter.Value --output text) | |
| echo "LIK_UI_DB_NAME=$uidb" >> "$GITHUB_ENV" | |
| # Secrets (SecureString) — masked before they touch the log. | |
| key=$(aws ssm get-parameter --name "$SSM_PREFIX/shared/ANTHROPIC_API_KEY" \ | |
| --with-decryption --query Parameter.Value --output text) | |
| echo "::add-mask::$key" | |
| echo "LIK_UI_ANTHROPIC_API_KEY=$key" >> "$GITHUB_ENV" | |
| pw=$(aws ssm get-parameter --name "$SSM_PREFIX/shared/DB_MASTER_PASSWORD" \ | |
| --with-decryption --query Parameter.Value --output text) | |
| echo "::add-mask::$pw" | |
| echo "LIK_UI_DB_PASSWORD=$pw" >> "$GITHUB_ENV" | |
| - name: Discover DB host/port/user from Lightsail | |
| run: | | |
| read -r host port user < <(aws lightsail get-relational-database \ | |
| --relational-database-name "$DB_INSTANCE" \ | |
| --query 'relationalDatabase.[masterEndpoint.address,masterEndpoint.port,masterUsername]' \ | |
| --output text) | |
| if [ -z "$host" ] || [ "$host" = "None" ]; then | |
| echo "::error::could not resolve Lightsail endpoint for $DB_INSTANCE"; exit 1 | |
| fi | |
| { echo "LIK_UI_DB_HOST=$host"; echo "LIK_UI_DB_PORT=$port"; echo "LIK_UI_DB_USER=$user"; } >> "$GITHUB_ENV" | |
| - name: Prune expired sessions | |
| working-directory: lik-ui | |
| run: uv run --frozen python scripts/prune_sessions.py | |
| # This is the only alerting for an otherwise-invisible unattended job: a failed run (bad | |
| # credentials, DB unreachable, a partial-failure non-zero exit) opens a tracking issue so | |
| # a silent stop-pruning doesn't go unnoticed for weeks. | |
| - name: Open a tracking issue on failure | |
| if: failure() | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| run_url="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" | |
| gh issue create \ | |
| --title "prune-sessions cron failed" \ | |
| --body "The daily session-cleanup workflow failed: $run_url | |
| Expired sessions were not pruned this run. Investigate before the next scheduled tick." \ | |
| || echo "::warning::prune-sessions failed and the tracking issue could not be created" |