A scheduled Cloud Run Function that logs out of your work platforms and emails you an end-of-shift summary, automatically, at shift end.
- Python 3.12
functions-framework(Cloud Run Functions / Cloud Functions, 2nd gen, HTTP-triggered)google-cloud-secret-managerfor credentials (no.envsecrets)- Cloud Scheduler (invokes the function on a cron schedule via OIDC auth)
smtplib(stdlib) for sending the summary email over SMTP
- Install Python 3.11+ and create a virtual environment:
python -m venv venv venv\Scripts\activate # Windows source venv/bin/activate # Mac/Linux
- Install the project in editable mode with dev dependencies:
pip install -e ".[dev]" - Copy
.env.exampleto.envand fill in the non-secret values (see below — there's exactly one credential this app needs, and it does not go in.env).
See .env.example for the full list with comments. Summary:
GCP_PROJECT_ID— the project holding the Secret Manager secret.PLATFORM_NAMES— comma-separated list of platforms to log out of.SUMMARY_EMAIL_TO— where the summary email goes.SMTP_USERNAME— the Gmail address the summary is sent from (not secret, just an address).SMTP_HOST/SMTP_PORT— optional, default to Gmail's SMTP settings.CREDENTIALS_SECRET_ID— the name of the shared Secret Manager secret holding this and other builds' credentials as one JSON blob (see API Keymaster). Defaults toapp-credentials. The password itself is never an environment variable or a file on disk — see Data Handling below.SMTP_CREDENTIAL_KEY— which key inside that blob holds this build's Gmail App Password. Defaults tosmtp_password.
pip install -e ".[dev]"
pytest -vTo boot the function locally (it will try to reach real Secret Manager and SMTP, so this needs valid gcloud credentials and a real secret to fully succeed):
functions-framework --target=shift_closer --port=8080Not deployed yet. deploy/deploy.sh deploys the function, sets up a dedicated invoker service account, and creates the Cloud Scheduler job, in one script. It expects the shared app-credentials secret to already exist with a smtp_password key set (created via API Keymaster's deploy script or manually) and will fail fast with a clear message if it doesn't. Review the variables at the top of that file (project ID, region, schedule, timezone) before running it.
- The only credential this app uses is a Gmail App Password, read from the
smtp_passwordkey of the sharedapp-credentialsSecret Manager secret — never in a file, environment variable dump, or log line. That secret also holds credentials for other builds in this series; this app only ever reads its own key out of it. - The Cloud Function's own runtime service account is granted read-only access to that one shared secret (
roles/secretmanager.secretAccessor), nothing broader. - "Logging out of platforms" is currently mocked (no real platform is targeted yet) — no platform credentials are collected, stored, or transmitted by this app at all.
- The summary email contains only: shift end time, and each platform's name + logout success/failure. No personal data beyond the recipient's own email address (which the user configures) is ever included.
- All logs are structured JSON via Cloud Logging; secret values are never logged, only secret IDs (names) when fetching them.
The function is one HTTP-triggered Cloud Run Function (shift_closer), invoked by Cloud Scheduler on a cron trigger. On each run it: logs out of every configured platform (currently mocked — see Notes), fetches the SMTP app password from Secret Manager, builds a plain-text summary, and sends it over SMTP.
src/shift_closer/config.pyreads and validates all non-secret configuration from environment variables. Fails fast with a clear error if anything required is missing.src/shift_closer/secrets.pyis the only module that talks to Secret Manager.get_secret()fetches a raw secret's plaintext;get_credential()fetches the sharedapp-credentialsJSON blob and pulls out this build's one key (smtp_passwordby default). Both take a Secret Manager client as an optional injectable parameter specifically so tests never need real GCP credentials.src/shift_closer/platform_client.pycurrently mocks the logout call — there's no real target platform yet (this build started as a "could I build this at all" exploration, not tied to an actual job). TheLogoutResultshape andlogout_from_all_platforms()'s per-platform failure isolation are written so swapping in a real HTTP call per platform is a contained change to one function's body, not a redesign.src/shift_closer/email_service.pybuilds the summary text and sends it via stdlibsmtplib— no third-party email SDK, to keep the dependency footprint minimal for a single scheduled email.src/shift_closer/main.pyorchestrates the above and exposes thefunctions_framework.http-decoratedshift_closerentry point.main.py(repo root) is a thin shim: Google's Python Functions buildpack requiresmain.pyat the deployment source root, but this project keeps a propersrc//tests/layout, so the root file just addssrc/to the import path and re-exports the real function.deploy/deploy.shdocuments (and automates, once run) the exactgcloudsequence: enable APIs, create the secret, deploy the function, create a least-privilege invoker service account, grant it invoke access, and create the Scheduler job with OIDC auth (not a public/unauthenticated endpoint).
- This is a practice/portfolio build, not tied to a real job. There's no real "work platform" to log out of yet, so
platform_client.pyis intentionally mocked. The architecture (config → secrets → platform calls → email) is real and deployable; only the platform integration itself is a stand-in. - Not deployed yet. Run
deploy/deploy.shonce the target project is ready. - Schedule is a placeholder.
deploy/deploy.shdefaults to0 4 * * *(4:00 AM daily,America/New_York) — adjustSCHEDULE/TIME_ZONEin that file to match the actual shift end time before deploying. AGENTS.md,BUILD_NOTES.md, and.claude/are intentionally excluded from this repo (see.gitignore) — local build-process files, not part of the shipped function.