From eabde92dbf49a07ea2032e1f994341b750a38c34 Mon Sep 17 00:00:00 2001 From: Patrick Easters Date: Sat, 9 May 2026 13:41:18 -0400 Subject: [PATCH 1/4] Lazy session creation: skip session I/O for unauthenticated requests Cookie.get() now checks for an existing session cookie before accessing cherrypy.session, so anonymous visitors no longer generate session files. Also mocks sync_roles in admin tests to avoid live BigQuery calls. Co-Authored-By: Claude Sonnet 4.6 --- tests/CPtest.py | 7 ++++++- tests/admin_test.py | 7 +++++-- webBase.py | 14 +++++++------- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/tests/CPtest.py b/tests/CPtest.py index ab9045c..6a181be 100644 --- a/tests/CPtest.py +++ b/tests/CPtest.py @@ -1,3 +1,4 @@ +import contextlib from unittest.mock import patch import cherrypy @@ -5,6 +6,7 @@ from cherrypy.lib.sessions import RamSession from checkMeIn import CheckMeIn +from webBase import Cookie class CPTest(helper.CPWebCase): @@ -31,4 +33,7 @@ def patch_session(self, username='admin', barcode='100091', role=0xFF): sess_mock['username'] = username sess_mock['barcode'] = barcode sess_mock['role'] = role - return patch('cherrypy.session', sess_mock, create=True) + stack = contextlib.ExitStack() + stack.enter_context(patch('cherrypy.session', sess_mock, create=True)) + stack.enter_context(patch.object(Cookie, '_session_exists', return_value=True)) + return stack diff --git a/tests/admin_test.py b/tests/admin_test.py index fee0223..cb18fa5 100644 --- a/tests/admin_test.py +++ b/tests/admin_test.py @@ -1,3 +1,5 @@ +from unittest.mock import patch + import CPtest @@ -61,7 +63,7 @@ def test_users(self): self.assertStatus('200 OK') def test_changeAccess(self): - with self.patch_session(): + with self.patch_session(), patch('webAdminStation.sync_roles'): self.getPage( "/admin/changeAccess?barcode=100091&admin=1&keyholder=1") self.assertStatus('303 See Other') @@ -76,8 +78,9 @@ def test_emptyBuilding(self): self.getPage("/admin/emptyBuilding") def test_addUser(self): - with self.patch_session(): + with self.patch_session(), patch('webAdminStation.sync_roles'): self.getPage("/admin/addUser?user=Fred&barcode=100093") + self.assertStatus('200 OK') def test_addUserDuplicate(self): with self.patch_session(): diff --git a/webBase.py b/webBase.py index b6f2278..9a28fc1 100644 --- a/webBase.py +++ b/webBase.py @@ -7,14 +7,14 @@ class Cookie(object): def __init__(self, name): self.name = name - def get(self, default=''): - result = default - result = cherrypy.session.get(self.name) - if not result: - self.set(default) - result = default + def _session_exists(self): + session_name = cherrypy.config.get('tools.sessions.name', 'session_id') + return session_name in cherrypy.request.cookie - return result + def get(self, default=''): + if not self._session_exists(): + return default + return cherrypy.session.get(self.name, default) def set(self, value): cherrypy.session[self.name] = value From abaedc08ba224d42129236fad7cd5e9d7f56035c Mon Sep 17 00:00:00 2001 From: Patrick Easters Date: Sun, 10 May 2026 07:46:45 -0400 Subject: [PATCH 2/4] Add container deployment with GitHub Actions CI/CD Dockerfile and container.conf for running the app on a Container-Optimized OS VM. GitHub Actions workflows run tests on every push/PR and deploy to staging (auto) then production (requires approval) on merges to master. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/deploy.yml | 90 ++++++++++++++++++++++++++++++++++++ .github/workflows/test.yml | 25 ++++++++++ Dockerfile | 15 ++++++ container.conf | 30 ++++++++++++ 4 files changed, 160 insertions(+) create mode 100644 .github/workflows/deploy.yml create mode 100644 .github/workflows/test.yml create mode 100644 Dockerfile create mode 100644 container.conf diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..182b0ce --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,90 @@ +name: Deploy + +on: + push: + branches: [master] + +env: + GCP_REGION: us-central1 + AR_REPO: checkmein + IMAGE: checkmein + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + + steps: + - uses: actions/checkout@v4 + + - name: Authenticate to GCP + uses: google-github-actions/auth@v2 + with: + workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }} + service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }} + + - name: Set up gcloud + uses: google-github-actions/setup-gcloud@v2 + + - name: Configure Docker for Artifact Registry + run: gcloud auth configure-docker ${{ env.GCP_REGION }}-docker.pkg.dev --quiet + + - name: Build and push image + run: | + IMAGE_TAG=${{ env.GCP_REGION }}-docker.pkg.dev/${{ secrets.GCP_PROJECT }}/${{ env.AR_REPO }}/${{ env.IMAGE }} + docker build -t $IMAGE_TAG:${{ github.sha }} -t $IMAGE_TAG:latest . + docker push $IMAGE_TAG:${{ github.sha }} + docker push $IMAGE_TAG:latest + + deploy-staging: + needs: build + runs-on: ubuntu-latest + environment: staging # GCE_INSTANCE and GCE_ZONE set as environment variables in GitHub + permissions: + contents: read + id-token: write + + steps: + - name: Authenticate to GCP + uses: google-github-actions/auth@v2 + with: + workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }} + service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }} + + - name: Set up gcloud + uses: google-github-actions/setup-gcloud@v2 + + - name: Deploy to staging + run: | + IMAGE_TAG=${{ env.GCP_REGION }}-docker.pkg.dev/${{ secrets.GCP_PROJECT }}/${{ env.AR_REPO }}/${{ env.IMAGE }}:${{ github.sha }} + gcloud compute instances update-container ${{ vars.GCE_INSTANCE }} \ + --zone=${{ vars.GCE_ZONE }} \ + --container-image=$IMAGE_TAG + + deploy-prod: + needs: deploy-staging + runs-on: ubuntu-latest + environment: production # GCE_INSTANCE and GCE_ZONE set as environment variables in GitHub + # Configure required reviewers in the production environment settings + permissions: + contents: read + id-token: write + + steps: + - name: Authenticate to GCP + uses: google-github-actions/auth@v2 + with: + workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }} + service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }} + + - name: Set up gcloud + uses: google-github-actions/setup-gcloud@v2 + + - name: Deploy to production + run: | + IMAGE_TAG=${{ env.GCP_REGION }}-docker.pkg.dev/${{ secrets.GCP_PROJECT }}/${{ env.AR_REPO }}/${{ env.IMAGE }}:${{ github.sha }} + gcloud compute instances update-container ${{ vars.GCE_INSTANCE }} \ + --zone=${{ vars.GCE_ZONE }} \ + --container-image=$IMAGE_TAG diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..42aca49 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,25 @@ +name: Test + +on: + push: + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.10" + cache: pip + + - name: Install dependencies + run: pip install -r requirements.txt + + - name: Run tests + working-directory: tests + run: | + mkdir -p ../testData + pytest diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b0a2461 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +FROM python:3.10-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +# These directories are mount points for the persistent disk at runtime +RUN mkdir -p data sessions + +EXPOSE 8447 + +CMD ["python", "checkMeIn.py", "container.conf"] diff --git a/container.conf b/container.conf new file mode 100644 index 0000000..5eed9cc --- /dev/null +++ b/container.conf @@ -0,0 +1,30 @@ +[global] +# Bind to all interfaces so Caddy (separate container) can reach us +server.socket_host : '0.0.0.0' +server.socket_port : 8447 +database.path : 'data/' +database.name : 'checkMeIn.db' +sync.token : 'robotsarecool' +bigquery.credentials : '' +bigquery.project : '' +bigquery.dataset : 'checkmein' + +[/] +tools.staticdir.root : os.path.abspath(os.getcwd()) +tools.sessions.on : True +tools.sessions.storage_class : cherrypy.lib.sessions.FileSession +tools.sessions.storage_path : os.path.join(os.getcwd(), 'sessions') +tools.sessions.timeout : 60 * 24 * 365 +tools.sessions.httponly : True + +[/favicon.ico] +tools.staticfile.on : True +tools.staticfile.filename : os.path.join(os.getcwd(), 'static/favicon.ico') + +[/robots.txt] +tools.staticfile.on : True +tools.staticfile.filename : os.path.join(os.getcwd(), 'static/robots.txt') + +[/static] +tools.staticdir.on : True +tools.staticdir.dir : 'static' From c44d448784247096c5a8ce3fdc62a75e1f25c6bc Mon Sep 17 00:00:00 2001 From: Patrick Easters Date: Sun, 10 May 2026 08:18:51 -0400 Subject: [PATCH 3/4] Fix CI test discovery: set PYTHONPATH for root and tests modules Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/test.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 42aca49..cb309e9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,7 +19,8 @@ jobs: run: pip install -r requirements.txt - name: Run tests - working-directory: tests + env: + PYTHONPATH: ${{ github.workspace }}:${{ github.workspace }}/tests run: | - mkdir -p ../testData - pytest + mkdir -p testData + pytest tests/ From 7bef0ca67a4040437ef0024a0c0573f79f61c53e Mon Sep 17 00:00:00 2001 From: Patrick Easters Date: Sun, 10 May 2026 12:28:44 -0400 Subject: [PATCH 4/4] Replace deprecated update-container with SSH-based docker deploy Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/deploy.yml | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 182b0ce..81fe7bc 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -59,9 +59,20 @@ jobs: - name: Deploy to staging run: | IMAGE_TAG=${{ env.GCP_REGION }}-docker.pkg.dev/${{ secrets.GCP_PROJECT }}/${{ env.AR_REPO }}/${{ env.IMAGE }}:${{ github.sha }} - gcloud compute instances update-container ${{ vars.GCE_INSTANCE }} \ + gcloud compute ssh ${{ vars.GCE_INSTANCE }} \ --zone=${{ vars.GCE_ZONE }} \ - --container-image=$IMAGE_TAG + --tunnel-through-iap \ + --command=" + docker pull $IMAGE_TAG && \ + docker stop checkmein || true && \ + docker rm checkmein || true && \ + docker run -d --name checkmein \ + --restart always \ + -v /mnt/checkmein-data/db:/app/data \ + -v /mnt/checkmein-data/sessions:/app/sessions \ + -p 8447:8447 \ + $IMAGE_TAG + " deploy-prod: needs: deploy-staging @@ -85,6 +96,17 @@ jobs: - name: Deploy to production run: | IMAGE_TAG=${{ env.GCP_REGION }}-docker.pkg.dev/${{ secrets.GCP_PROJECT }}/${{ env.AR_REPO }}/${{ env.IMAGE }}:${{ github.sha }} - gcloud compute instances update-container ${{ vars.GCE_INSTANCE }} \ + gcloud compute ssh ${{ vars.GCE_INSTANCE }} \ --zone=${{ vars.GCE_ZONE }} \ - --container-image=$IMAGE_TAG + --tunnel-through-iap \ + --command=" + docker pull $IMAGE_TAG && \ + docker stop checkmein || true && \ + docker rm checkmein || true && \ + docker run -d --name checkmein \ + --restart always \ + -v /mnt/checkmein-data/db:/app/data \ + -v /mnt/checkmein-data/sessions:/app/sessions \ + -p 8447:8447 \ + $IMAGE_TAG + "