Skip to content

Tuan - #132

Merged
Johnny-Dang merged 17 commits into
mainfrom
Tuan
Jul 15, 2026
Merged

Tuan#132
Johnny-Dang merged 17 commits into
mainfrom
Tuan

Conversation

@Johnny-Dang

@Johnny-Dang Johnny-Dang commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features

    • Added support for Firebase authentication and messaging in production deployments.
    • Updated Android sign-in configuration for additional release certificates.
    • Improved release packaging of native libraries.
  • Bug Fixes

    • Fixed crashes when installing or launching release APKs/AABs.
    • Fixed production Google Sign-In and FCM failures caused by unavailable Firebase credentials.
    • Improved deployment reliability with image pull retries and credential validation.
  • Chores

    • Updated the app version to 1.0.0+2.
    • Added safeguards to prevent signing keys and generated platform files from being committed.

Johnny-Dang and others added 6 commits July 14, 2026 09:12
These files are auto-generated by 'flutter pub get' and only
re-introduce noise diffs (especially CRLF warnings on Windows).
Add explicit patterns to .gitignore and remove from index.
Without explicit jniLibs.srcDirs, AGP mergeReleaseNativeLibs
only picks up libflutter.so, causing the release APK to crash
on launch with 'VM snapshot invalid' / SIGSEGV.

Also bump version 1.0.0+1 -> 1.0.0+2.

Refs: docs/issues.md (2026-07-14 incident)
Added patterns to ignore keystore and signing properties files to prevent accidental commits of sensitive information.
Added new OAuth client configurations in google-services.json for enhanced authentication support. Updated appId in firebase_options.dart to align with the new client settings.
Google sign-in (FirebaseAuth.VerifyIdTokenAsync) and FCM push
notifications were silently failing on production because the
firebase-adminsdk.json file was never mounted into the API container.
Program.cs has a File.Exists guard that skips FirebaseApp.Create()
when the credential path is missing, so no error was surfaced.

Changes:
- docker-compose.prod.yml: mount the host JSON to
  /etc/secrets/firebase-adminsdk.json (ro) and pass
  Firebase__CredentialPath via env (with hardcoded fallback).
- backend/scripts/deploy-server.sh: on every deploy, read the
  FIREBASE_CREDENTIALS_JSON Doppler secret (full JSON body, kept
  out of git because it holds a private_key) and write it to
  $APP_DIR/firebase-adminsdk.json with mode 600. Validate JSON
  parses and contains a private_key before pulling the new image,
  so a bad secret aborts the deploy without disrupting traffic.
  Same block added inside perform_rollback() so the rolled-back
  container also has working Firebase.
- docs/issues.md: record the diagnosis and fix attempt log.

Tested locally (YAML/shell lint clean). Next: push to Tuan to
trigger backend-cd.yml.

Co-authored-by: Cursor <cursoragent@cursor.com>
@vercel

vercel Bot commented Jul 15, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
menu-green-system Ready Ready Preview, Comment Jul 15, 2026 8:17pm

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@Johnny-Dang, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 39 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a3d97203-8006-41e5-b345-c2ef3bf475e5

📥 Commits

Reviewing files that changed from the base of the PR and between 5e85cc2 and 3122fc8.

📒 Files selected for processing (2)
  • backend/scripts/deploy-server.sh
  • docs/issues.md
📝 Walkthrough

Walkthrough

Flutter Android release packaging and Firebase configuration were updated, while production deployment now materializes and mounts validated Firebase credentials, retries image pulls, and filters multiline secrets. Backend CI branch triggers and Docker attestation settings were also changed.

Changes

Mobile release configuration

Layer / File(s) Summary
Android packaging and Firebase client configuration
frontend/android/app/build.gradle.kts, frontend/android/app/google-services.json, frontend/lib/firebase_options.dart, frontend/pubspec.yaml, frontend/.gitignore, .gitignore, docs/issues.md
Release packaging includes Flutter AOT libraries, Firebase Android OAuth metadata and app settings are updated, the build number is incremented, signing and generated artifacts are ignored, and release/device issue records are documented.

Production Firebase credential delivery

Layer / File(s) Summary
Credential materialization and container wiring
backend/scripts/deploy-server.sh, docker-compose.prod.yml, docs/issues.md
Deployment and rollback materialize validated Firebase credentials with restricted permissions, prevent multiline secret leakage into .env, mount the credential file into the API container, and document the production Firebase issue.
Deployment image pull recovery
backend/scripts/deploy-server.sh
Image pulls retry three times, restart the container runtime after repeated failures, and perform one final pull before deployment aborts.

Backend CI workflow settings

Layer / File(s) Summary
CI trigger and image build controls
.github/workflows/backend-ci.yml
Push triggers are limited to main and Tuan, and Docker provenance and SBOM attestations are disabled.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Doppler
  participant DeployScript
  participant Compose
  participant API
  Doppler->>DeployScript: Provide Firebase credentials
  DeployScript->>DeployScript: Validate and write credential file
  DeployScript->>Compose: Start production service
  Compose->>API: Mount file and set credential path
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title is just the branch/name "Tuan" and does not describe the changeset. Use a concise title that states the main change, such as adding Firebase credential deployment fixes and Android packaging updates.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch Tuan

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
frontend/lib/firebase_options.dart (1)

17-19: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Avoid manually editing generated files.

firebase_options.dart is typically generated by the flutterfire configure CLI tool. Any manual changes made here, such as customizing the UnsupportedError message, will be overwritten the next time the tool is run.

If you need a custom message or behavior, consider wrapping the initialization logic in a separate configuration class or service rather than modifying this generated file.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/lib/firebase_options.dart` around lines 17 - 19, Revert the manual
customization in the generated firebase_options.dart file, including the
localized UnsupportedError message, and restore the output expected from
flutterfire configure. If custom initialization behavior is required, implement
it in a separate configuration class or service rather than modifying the
generated file.
docker-compose.prod.yml (1)

29-36: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use a relative path for the host volume mount.

Hardcoding the absolute host path /home/ubuntu/apps/menugreen/firebase-adminsdk.json couples the docker-compose.prod.yml file to a specific server directory structure and username.

Since deploy-server.sh places the file in $APP_DIR alongside the compose file, you can use a relative path. This ensures the compose file remains portable and behaves correctly regardless of the server's absolute directory path.

🛠 Proposed fix
     volumes:
       # Firebase Admin SDK credentials — read-only mount from the host.
       # File is materialized at deploy time by deploy-server.sh from the
       # Doppler secret FIREBASE_CREDENTIALS_JSON, so it's never stored in git.
       # Without this mount, FirebaseAuth.VerifyIdTokenAsync (Google sign-in)
       # and FirebaseMessaging.SendAsync (FCM push notifications) silently
       # fail because FirebaseApp.DefaultInstance == null.
-      - /home/ubuntu/apps/menugreen/firebase-adminsdk.json:/etc/secrets/firebase-adminsdk.json:ro
+      - ./firebase-adminsdk.json:/etc/secrets/firebase-adminsdk.json:ro
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docker-compose.prod.yml` around lines 29 - 36, Update the Firebase Admin SDK
volume entry in the compose service to use the credential file’s relative path
alongside the compose file instead of the hardcoded /home/ubuntu/apps/menugreen
host path, while preserving the container destination and read-only mode.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/scripts/deploy-server.sh`:
- Around line 196-198: Update both Firebase credential materialization sites in
backend/scripts/deploy-server.sh at lines 196-198 and 548-550: replace echo with
printf '%s\n' to preserve JSON and private_key formatting, and change the
root-owned 600 permissions to a mode or ownership that allows the non-root .NET
container user to read the file.

---

Nitpick comments:
In `@docker-compose.prod.yml`:
- Around line 29-36: Update the Firebase Admin SDK volume entry in the compose
service to use the credential file’s relative path alongside the compose file
instead of the hardcoded /home/ubuntu/apps/menugreen host path, while preserving
the container destination and read-only mode.

In `@frontend/lib/firebase_options.dart`:
- Around line 17-19: Revert the manual customization in the generated
firebase_options.dart file, including the localized UnsupportedError message,
and restore the output expected from flutterfire configure. If custom
initialization behavior is required, implement it in a separate configuration
class or service rather than modifying the generated file.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ef97497d-5705-4576-a226-50c526cac93a

📥 Commits

Reviewing files that changed from the base of the PR and between cbf657f and 0a31067.

📒 Files selected for processing (15)
  • .gitignore
  • backend/scripts/deploy-server.sh
  • docker-compose.prod.yml
  • docs/issues.md
  • frontend/.gitignore
  • frontend/android/app/build.gradle.kts
  • frontend/android/app/google-services.json
  • frontend/lib/firebase_options.dart
  • frontend/linux/flutter/generated_plugin_registrant.cc
  • frontend/linux/flutter/generated_plugin_registrant.h
  • frontend/linux/flutter/generated_plugins.cmake
  • frontend/pubspec.yaml
  • frontend/windows/flutter/generated_plugin_registrant.cc
  • frontend/windows/flutter/generated_plugin_registrant.h
  • frontend/windows/flutter/generated_plugins.cmake
💤 Files with no reviewable changes (6)
  • frontend/windows/flutter/generated_plugin_registrant.h
  • frontend/linux/flutter/generated_plugins.cmake
  • frontend/linux/flutter/generated_plugin_registrant.h
  • frontend/windows/flutter/generated_plugin_registrant.cc
  • frontend/windows/flutter/generated_plugins.cmake
  • frontend/linux/flutter/generated_plugin_registrant.cc

Comment thread backend/scripts/deploy-server.sh
Johnny-Dang and others added 2 commits July 16, 2026 00:51
The previous deploy died at docker compose up with:

  failed to read .env: line 22: unexpected character "}"
  in variable name "}="

Root cause: the while-read loop that copies Doppler secrets into
$APP_DIR/.env has no skip rule for FIREBASE_CREDENTIALS_JSON, so
each continuation line of the multi-line JSON (spaces, commas,
the closing '}') gets echoed verbatim into .env. Docker compose
then refuses to parse the file and never starts the container.

Service went DOWN because the rollback path also had no local
rollback tag (cold deploy) and no :previous / :main-<oldsha>
image on Hub — script exited 1.

Fix: skip FIREBASE_CREDENTIALS_JSON= in both the main path and
the rollback path of the .env loop. The Firebase JSON is handled
by the materialize block above (echo > $APP_DIR/firebase-adminsdk.json),
not via env var — the .NET SDK only accepts a file path.

This commit alone should bring the service back up because the
image on Hub still contains the previously working code; the only
broken thing was the new deploy script.

Co-authored-by: Cursor <cursoragent@cursor.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/backend-cd.yml:
- Around line 58-66: Prevent command injection in the Resolve SHA step by
mapping inputs.sha and github.event.workflow_run.head_sha to environment
variables, using those variables in the shell, and using the built-in GITHUB_SHA
variable for the fallback. In .github/workflows/backend-cd.yml lines 84-85, also
map steps.resolve_sha.outputs.SHA to an environment variable before echoing it
to GITHUB_ENV.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e2a683a9-3fc3-4baa-9f0e-7797a84ed679

📥 Commits

Reviewing files that changed from the base of the PR and between 0a31067 and 4332c0a.

📒 Files selected for processing (3)
  • .github/workflows/backend-cd.yml
  • .github/workflows/backend-ci.yml
  • backend/scripts/deploy-server.sh
🚧 Files skipped from review as they are similar to previous changes (1)
  • backend/scripts/deploy-server.sh

Comment thread .github/workflows/backend-cd.yml Outdated
Comment on lines +58 to +66
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ] \
&& [ -n "${{ inputs.sha }}" ]; then
echo "SHA=${{ inputs.sha }}" >> "$GITHUB_OUTPUT"
elif [ "${{ github.event_name }}" = "workflow_run" ]; then
echo "SHA=${{ github.event.workflow_run.head_sha }}" >> "$GITHUB_OUTPUT"
else
echo "SHA=${{ github.sha }}" >> "$GITHUB_OUTPUT"
fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Prevent command injection by using environment variables.

Using GitHub Actions ${{ ... }} expression syntax inside a run block allows template injection. If an attacker triggers the workflow_dispatch event with a specially crafted sha input (e.g., "; malicious command; echo "), or if upstream outputs contain malicious characters, it could result in arbitrary code execution on the runner.

Instead of directly interpolating these values into the script, pass them safely as environment variables.

  • .github/workflows/backend-cd.yml#L58-L66: Map inputs.sha and github.event.workflow_run.head_sha to environment variables, and use the built-in $GITHUB_SHA for the fallback.
  • .github/workflows/backend-cd.yml#L84-L85: Map steps.resolve_sha.outputs.SHA to an environment variable before echoing it to $GITHUB_ENV.
🔒️ Proposed fixes

For the Resolve SHA step:

-        run: |
-          if [ "${{ github.event_name }}" = "workflow_dispatch" ] \
-             && [ -n "${{ inputs.sha }}" ]; then
-            echo "SHA=${{ inputs.sha }}" >> "$GITHUB_OUTPUT"
-          elif [ "${{ github.event_name }}" = "workflow_run" ]; then
-            echo "SHA=${{ github.event.workflow_run.head_sha }}" >> "$GITHUB_OUTPUT"
-          else
-            echo "SHA=${{ github.sha }}" >> "$GITHUB_OUTPUT"
-          fi
+        env:
+          INPUT_SHA: ${{ inputs.sha }}
+          RUN_SHA: ${{ github.event.workflow_run.head_sha }}
+        run: |
+          if [ "${{ github.event_name }}" = "workflow_dispatch" ] \
+             && [ -n "$INPUT_SHA" ]; then
+            echo "SHA=$INPUT_SHA" >> "$GITHUB_OUTPUT"
+          elif [ "${{ github.event_name }}" = "workflow_run" ]; then
+            echo "SHA=$RUN_SHA" >> "$GITHUB_OUTPUT"
+          else
+            echo "SHA=$GITHUB_SHA" >> "$GITHUB_OUTPUT"
+          fi

For the Export SHA to environment step:

       - name: Export SHA to environment
-        run: echo "SHA=${{ steps.resolve_sha.outputs.SHA }}" >> "$GITHUB_ENV"
+        env:
+          RESOLVED_SHA: ${{ steps.resolve_sha.outputs.SHA }}
+        run: echo "SHA=$RESOLVED_SHA" >> "$GITHUB_ENV"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ] \
&& [ -n "${{ inputs.sha }}" ]; then
echo "SHA=${{ inputs.sha }}" >> "$GITHUB_OUTPUT"
elif [ "${{ github.event_name }}" = "workflow_run" ]; then
echo "SHA=${{ github.event.workflow_run.head_sha }}" >> "$GITHUB_OUTPUT"
else
echo "SHA=${{ github.sha }}" >> "$GITHUB_OUTPUT"
fi
env:
INPUT_SHA: ${{ inputs.sha }}
RUN_SHA: ${{ github.event.workflow_run.head_sha }}
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ] \
&& [ -n "$INPUT_SHA" ]; then
echo "SHA=$INPUT_SHA" >> "$GITHUB_OUTPUT"
elif [ "${{ github.event_name }}" = "workflow_run" ]; then
echo "SHA=$RUN_SHA" >> "$GITHUB_OUTPUT"
else
echo "SHA=$GITHUB_SHA" >> "$GITHUB_OUTPUT"
fi
🧰 Tools
🪛 zizmor (1.26.1)

[error] 60-60: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)


[error] 61-61: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)


[error] 63-63: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)

📍 Affects 1 file
  • .github/workflows/backend-cd.yml#L58-L66 (this comment)
  • .github/workflows/backend-cd.yml#L84-L85
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/backend-cd.yml around lines 58 - 66, Prevent command
injection in the Resolve SHA step by mapping inputs.sha and
github.event.workflow_run.head_sha to environment variables, using those
variables in the shell, and using the built-in GITHUB_SHA variable for the
fallback. In .github/workflows/backend-cd.yml lines 84-85, also map
steps.resolve_sha.outputs.SHA to an environment variable before echoing it to
GITHUB_ENV.

Source: Linters/SAST tools

The previous deploy died at:

  >>> FATAL: firebase-adminsdk.json is missing or invalid
  >>> (no private_key field).

Root cause: Doppler --format env exports the JSON as a multi-line
secret. The materialize block used:

  FIREBASE_JSON="$(grep '^FIREBASE_CREDENTIALS_JSON='     /tmp/doppler_raw.env | cut -d= -f2-)"

`cut -d= -f2-` only keeps the part after the first '=' on the
matching line — i.e. just the leading "{". The file written to
$APP_DIR/firebase-adminsdk.json was therefore "{", which the
python sanity check correctly rejected as missing private_key.

Fix: use python to capture the multi-line value between the
FIREBASE_CREDENTIALS_JSON= header and the next KEY= line (or EOF).
Applied to both the main materialize block and the rollback block
so a future rollback still has a valid Firebase JSON.

This unblocks the deploy pipeline — once pushed to main, CD will
re-run with this commit, materialize the full JSON, start the
container, and (hopefully) pass the health check.

Co-authored-by: Cursor <cursoragent@cursor.com>
The previous python-regex fix (commit 62afaea) didn't work because
Doppler --format env emits multi-line values like this:

  FIREBASE_CREDENTIALS_JSON="{
    \"type\": \"service_account\",\n
    ...
  }"

— quoted, with \" escaping inner quotes and REAL newlines inside
the quoted block. A regex that grabs everything between KEY=" and the
next KEY= line captures the closing \" too, leaving the surrounding
double-quotes in place; json.loads then bails with 'Expecting value:
line 1 column 2 (char 1)'.

Approach: walk the file line-by-line starting at the header line,
collect subsequent lines, and stop when the cumulative unescaped quote
count is even (i.e. we've reached the closing quote of the value).
Strip the surrounding double-quotes and unescape \" → ".

Verified locally against the real firebase-adminsdk.json from the
repo (private_key contains a multi-line PEM block). Output JSON parses
cleanly and contains the real newlines that FirebaseAdmin needs in
the PEM.

Co-authored-by: Cursor <cursoragent@cursor.com>
…ilure

The deploy pipeline reached 'Pull latest image' for the first time
(commit 0054c33 made the Firebase JSON materialize step succeed), but
then died with:

  failed commit on ref 'attestation-sha256:...':
  rename /var/lib/containerd/io.containerd.content.v1.content/ingest/...
        /var/lib/containerd/io.containerd.content.v1.content/blobs/...
  no such file or directory
  Failed to pull image ***/menugreensystem:main

Root cause: Docker Buildx v6 pushes provenance + SBOM attestations by
default. On the deploy host, `docker pull` then commits those
attestation manifests into containerd's blob store, and the rename
fails because the ingest directory gets cleared by `docker image
prune` running earlier in the deploy (step 6).

Two changes:

1. .github/workflows/backend-ci.yml: disable provenance and sbom in the
   build-push-action. We don't use these attestations downstream
   (no cosign verify, no policy check), so turning them off keeps the
   next `docker pull` clean.

2. backend/scripts/deploy-server.sh: wrap the pull in a 3-attempt
   retry with a 5s delay between attempts, and if those fail restart
   containerd/docker once and retry a final time. This makes the
   pipeline self-heal if a stale attestation from an older CI build
   is still sitting in the registry.

Co-authored-by: Cursor <cursoragent@cursor.com>
The deploy reached 'Start API container' for the first time (commit
4420084 made Docker pull work), then died with:

  failed to read /home/***/apps/menugreen/.env:
  line 22: unexpected character '}' in variable name '}='='

Root cause: the .env loop used:

  [[ $key =~ ^(FIREBASE_CREDENTIALS_JSON=) ]] && continue

That only matches the very first line of the multi-line value
('FIREBASE_CREDENTIALS_JSON="{'). All continuation lines ('  "type": ...'
or the closing '}"') still fell through with their first 'word' becoming
$key. Once IFS='=' split the closing '}"' line, key was literally '}',
raw_value was empty, and the loop wrote '= ' into .env — which Docker
compose rejects with the error above.

Fix: instead of blacklisting a single key, whitelist what a real env
variable name looks like — anything that isn't an ALL-CAPS identifier
(continuation of a quoted multi-line value, blank line, comment, etc.)
is dropped. That catches both FIREBASE_CREDENTIALS_JSON AND any future
multi-line secret we might add to Doppler without us having to remember
to blacklist each one.

Verified locally with a mocked Doppler env containing a multi-line JSON
secret — generated .env contains exactly the expected 8 keys, no JSON
fragments leaked through, LIGHTSAIL_SSH_KEY still filtered.

Co-authored-by: Cursor <cursoragent@cursor.com>
…ebase JSON

The deploy reached 'Container menugreen_api Created/Started' for the
first time (commit ae7521f fixed .env parsing), but the container then
crash-looped with:

  System.ArgumentException: PKCS8 data must be contained within
  '-----BEGIN PRIVATE KEY-----' and '-----END PRIVATE KEY-----'.
    at Google.Apis.Auth.OAuth2.Pkcs8.DecodeRsaParameters
    at Program.<Main>$ ... in MenuGreen.API/Program.cs:line 35

(repeating ~6 times — one per health-check interval during the 30-attempt
wait window — until the deploy timed out.)

Root cause: the Doppler --format env extractor in deploy-server.sh
captures the FIREBASE_CREDENTIALS_JSON value but does NOT normalise the
embedded newline encoding of the PEM. Whether Doppler stores the
private_key field with real newlines (\n as one char) or literal \n
(two chars: backslash + n) depends on how the secret was ingested
originaly, and either form will pass a naive json.loads sanity check
(string 'starts with -----BEGIN'). But GoogleCredential.FromFile()
hands the private_key string to RSA crypto, which expects real newlines
between the BEGIN line and the END line. If the file has the literal
\n encoding, the PEM body smashes into one giant line and
Pkcs8.DecodeRsaParameters throws the ArgumentException above.

Fix:
  1. Strengthen the sanity check: json.load() the file, find the BEGIN
     and END markers in private_key, and require a real '\n' between
     them. If absent, abort the deploy with a clear error.
  2. Re-serialise the parsed JSON with json.dump(indent=2) so the
     on-disk file is canonical and the PEM is always human-readable
     (one line at a time).

This catches all 3 broken variants (literal \n, no newline at all,
missing field) at deploy time, and fixes the most common one
(literal \n -> real \n) automatically by writing the JSON back
from the parsed dict.

Verified locally with a 4-case test (real \n PASS, literal \n
REJECTED, no-newline REJECTED, missing-field REJECTED).

Co-authored-by: Cursor <cursoragent@cursor.com>
The previous fix (3546f1a) used:

  sudo python3 - <<PYEOF
    ...
  PYEOF

But that broke at runtime:

  /tmp/.../deploy-server.sh: line 248: pk: command not found
    File '<stdin>', line 1
      import json, sys
    IndentationError: unexpected indent

Two compounding bugs:

1. The Doppler env-extraction earlier in the same script set pk as a
   bare shell variable. Even though that block ended, the trailing
   content of the previous python3 -c heredoc was bleeding back into
   the script context, so bash was treating 'pk:' as a command.

2. sudo python3 - (stdin-based) is unreliable across sudo versions —
   some builds drop stdin entirely, some leave the heredoc un-delimited,
   and the leading 6-space indent on each line gets passed as-is to
   python3 -c which then chokes with IndentationError.

Fix: write the Python script to /tmp/firebase_pem_check.py via bash
heredoc (UNQUOTED, so $APP_DIR still expands), then run it via
'sudo python3 /tmp/firebase_pem_check.py'. That has no stdin ambiguity,
no shell-globals bleed-through, and the script can be debugged on disk
if anything goes wrong in the future.

Also corrected the heredoc tag: 'PYSCRIPT' (quoted) would skip variable
expansion and leave a literal '$APP_DIR' in the Python script — now
bare PYSCRIPT so $APP_DIR expands to /home/<user>/apps/menugreen
as expected.

Verified locally with 4-case smoke test (real \n PASS, literal \n
REJECTED, no-newline REJECTED, missing-field REJECTED).

Co-authored-by: Cursor <cursoragent@cursor.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
.github/workflows/backend-ci.yml (1)

5-5: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Keep CI and CD branch filters aligned. Removing Tuanx here stops Backend CI - Build & Push from completing on that branch, while .github/workflows/backend-cd.yml still listens for workflow_run events from Tuanx. Either keep Tuanx here or remove it from CD if the branch is retired.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/backend-ci.yml at line 5, Align the branch filters between
the backend CI workflow and backend CD workflow: either restore Tuanx in the CI
branches list so its build completes, or remove Tuanx from the CD workflow if
the branch is retired. Ensure both workflows use the same branch set.
🧹 Nitpick comments (2)
.github/workflows/backend-ci.yml (1)

116-127: 🔒 Security & Privacy | 🔵 Trivial | 🏗️ Heavy lift

Preserve supply-chain metadata or document the security trade-off.

provenance: false and sbom: false remove provenance and SBOM attestations from every pushed image. Confirm that no vulnerability scanner, registry policy, or release process depends on them; preferably fix the deploy-host pull failure or publish these attestations through a separate step.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/backend-ci.yml around lines 116 - 127, Revisit the Docker
Buildx configuration around provenance and sbom in the workflow: preserve these
attestations by fixing the deploy-host pull failure, or document and validate
that scanners, registry policies, and release processes do not depend on them
before disabling them. If they must remain disabled, add a separate attestation
publishing step and record the security trade-off in the workflow documentation.
backend/scripts/deploy-server.sh (1)

200-221: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated multi-line JSON extraction logic between deploy and rollback paths.

This exact python heredoc extractor (and the paired ALL-CAPS whitelist regex at Line 325) is copy-pasted verbatim into perform_rollback at Lines 683-705/726. This duplication is precisely the kind of drift risk that already produced the earlier bug where a fix landed on the main path but not the rollback path. Extracting this into a single shared helper (a small script file or bash function parameterized by the input env path) would prevent the two copies from diverging again.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/scripts/deploy-server.sh` around lines 200 - 221, The multi-line
FIREBASE_CREDENTIALS_JSON extraction and ALL-CAPS whitelist logic is duplicated
between deployment and perform_rollback, allowing the paths to diverge. Extract
both into one shared helper, script, or parameterized Bash function that accepts
the input environment path, then have both paths reuse it while preserving their
current extraction and filtering behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/scripts/deploy-server.sh`:
- Around line 513-517: Remove the host-wide containerd/docker restart from the
PULL_OK recovery branch in deploy-server.sh. Scope recovery to this deployment
only, such as clearing the stale image metadata or failing the pull so CI can
retry, while preserving the final failure handling for unsuccessful docker
pulls.
- Around line 257-296: Replace the predictable /tmp/firebase_pem_check.py path
in the PEM validation block with a securely created temporary file using mktemp,
and execute that generated path with sudo. Add trap-based cleanup immediately
after creation so the temporary script is removed on both successful and failed
validation, eliminating the existing rm-based cleanup dependency; preserve the
current validation behavior and failure messages.
- Around line 504-518: Ensure the pull retry condition around the docker pull
pipeline reflects the docker command’s exit status rather than tail’s status.
Enable pipefail before the loop or explicitly inspect the pull command status
via PIPESTATUS, so failed pulls trigger retries and the existing containerd
restart fallback.

---

Outside diff comments:
In @.github/workflows/backend-ci.yml:
- Line 5: Align the branch filters between the backend CI workflow and backend
CD workflow: either restore Tuanx in the CI branches list so its build
completes, or remove Tuanx from the CD workflow if the branch is retired. Ensure
both workflows use the same branch set.

---

Nitpick comments:
In @.github/workflows/backend-ci.yml:
- Around line 116-127: Revisit the Docker Buildx configuration around provenance
and sbom in the workflow: preserve these attestations by fixing the deploy-host
pull failure, or document and validate that scanners, registry policies, and
release processes do not depend on them before disabling them. If they must
remain disabled, add a separate attestation publishing step and record the
security trade-off in the workflow documentation.

In `@backend/scripts/deploy-server.sh`:
- Around line 200-221: The multi-line FIREBASE_CREDENTIALS_JSON extraction and
ALL-CAPS whitelist logic is duplicated between deployment and perform_rollback,
allowing the paths to diverge. Extract both into one shared helper, script, or
parameterized Bash function that accepts the input environment path, then have
both paths reuse it while preserving their current extraction and filtering
behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0464ea35-6ce8-43eb-a10c-cd929f003a81

📥 Commits

Reviewing files that changed from the base of the PR and between 4332c0a and 5e85cc2.

📒 Files selected for processing (2)
  • .github/workflows/backend-ci.yml
  • backend/scripts/deploy-server.sh

Comment thread backend/scripts/deploy-server.sh Outdated
Comment on lines +257 to +296
sudo tee /tmp/firebase_pem_check.py > /dev/null <<PYSCRIPT
import json, sys
path = "$APP_DIR/firebase-adminsdk.json"
try:
with open(path) as f:
data = json.load(f)
except Exception as e:
sys.stderr.write("failed to parse JSON: %s\n" % e)
sys.exit(2)
pk = data.get("private_key", "")
if "-----BEGIN" not in pk or "-----END" not in pk:
sys.stderr.write("FATAL: private_key is missing BEGIN/END markers\n")
sys.exit(3)
if "-----BEGIN PRIVATE KEY-----" not in pk:
sys.stderr.write("FATAL: private_key does not contain '-----BEGIN PRIVATE KEY-----'\n")
sys.exit(4)
# Verify real newlines sit between the BEGIN/END markers — otherwise
# GoogleCredential will crash with the exact same ArgumentException.
begin_idx = pk.index("-----BEGIN PRIVATE KEY-----")
end_idx = pk.index("-----END PRIVATE KEY-----")
if begin_idx >= end_idx:
sys.stderr.write("FATAL: BEGIN marker must appear before END marker in private_key\n")
sys.exit(5)
body = pk[begin_idx:end_idx]
if "\n" not in body:
sys.stderr.write(
"FATAL: private_key PEM has no real newlines between BEGIN and END - "
"GoogleCredential needs real \\n in the PEM body, not literal \\\\n.\n"
)
sys.exit(6)
# Re-serialize canonically so on-disk JSON is consistent and PEM is human-readable.
with open(path, "w") as f:
json.dump(data, f, indent=2)
PYSCRIPT
if ! sudo python3 /tmp/firebase_pem_check.py; then
echo ">>> FATAL: $APP_DIR/firebase-adminsdk.json failed sanity check (exit $?)"
echo ">>> Aborting deploy before pulling new image."
exit 1
fi
rm -f /tmp/firebase_pem_check.py

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Predictable /tmp path for the sudo-executed PEM check script.

/tmp/firebase_pem_check.py is a hardcoded, predictable path, written via sudo tee and then executed via sudo python3 — a classic TOCTOU/symlink hazard (flagged by static analysis, CWE-377). A local actor able to pre-create that path (e.g. as a symlink) before this deploy step runs could hijack or redirect the write, or have malicious content executed with root privileges. Also note rm -f at Line 296 is skipped if the check fails and the script exits at Line 294, leaving the file behind.

🛡️ Proposed fix using mktemp + trap cleanup
-sudo tee /tmp/firebase_pem_check.py > /dev/null <<PYSCRIPT
+PEM_CHECK_SCRIPT="$(mktemp /tmp/firebase_pem_check.XXXXXX.py)"
+trap 'rm -f "$PEM_CHECK_SCRIPT"' RETURN
+sudo tee "$PEM_CHECK_SCRIPT" > /dev/null <<PYSCRIPT
 import json, sys
 ...
 PYSCRIPT
-if ! sudo python3 /tmp/firebase_pem_check.py; then
+if ! sudo python3 "$PEM_CHECK_SCRIPT"; then
   echo ">>> FATAL: $APP_DIR/firebase-adminsdk.json failed sanity check (exit $?)"
   echo ">>> Aborting deploy before pulling new image."
   exit 1
 fi
-rm -f /tmp/firebase_pem_check.py
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
sudo tee /tmp/firebase_pem_check.py > /dev/null <<PYSCRIPT
import json, sys
path = "$APP_DIR/firebase-adminsdk.json"
try:
with open(path) as f:
data = json.load(f)
except Exception as e:
sys.stderr.write("failed to parse JSON: %s\n" % e)
sys.exit(2)
pk = data.get("private_key", "")
if "-----BEGIN" not in pk or "-----END" not in pk:
sys.stderr.write("FATAL: private_key is missing BEGIN/END markers\n")
sys.exit(3)
if "-----BEGIN PRIVATE KEY-----" not in pk:
sys.stderr.write("FATAL: private_key does not contain '-----BEGIN PRIVATE KEY-----'\n")
sys.exit(4)
# Verify real newlines sit between the BEGIN/END markers — otherwise
# GoogleCredential will crash with the exact same ArgumentException.
begin_idx = pk.index("-----BEGIN PRIVATE KEY-----")
end_idx = pk.index("-----END PRIVATE KEY-----")
if begin_idx >= end_idx:
sys.stderr.write("FATAL: BEGIN marker must appear before END marker in private_key\n")
sys.exit(5)
body = pk[begin_idx:end_idx]
if "\n" not in body:
sys.stderr.write(
"FATAL: private_key PEM has no real newlines between BEGIN and END - "
"GoogleCredential needs real \\n in the PEM body, not literal \\\\n.\n"
)
sys.exit(6)
# Re-serialize canonically so on-disk JSON is consistent and PEM is human-readable.
with open(path, "w") as f:
json.dump(data, f, indent=2)
PYSCRIPT
if ! sudo python3 /tmp/firebase_pem_check.py; then
echo ">>> FATAL: $APP_DIR/firebase-adminsdk.json failed sanity check (exit $?)"
echo ">>> Aborting deploy before pulling new image."
exit 1
fi
rm -f /tmp/firebase_pem_check.py
PEM_CHECK_SCRIPT="$(mktemp /tmp/firebase_pem_check.XXXXXX.py)"
trap 'rm -f "$PEM_CHECK_SCRIPT"' RETURN
sudo tee "$PEM_CHECK_SCRIPT" > /dev/null <<PYSCRIPT
import json, sys
path = "$APP_DIR/firebase-adminsdk.json"
try:
with open(path) as f:
data = json.load(f)
except Exception as e:
sys.stderr.write("failed to parse JSON: %s\n" % e)
sys.exit(2)
pk = data.get("private_key", "")
if "-----BEGIN" not in pk or "-----END" not in pk:
sys.stderr.write("FATAL: private_key is missing BEGIN/END markers\n")
sys.exit(3)
if "-----BEGIN PRIVATE KEY-----" not in pk:
sys.stderr.write("FATAL: private_key does not contain '-----BEGIN PRIVATE KEY-----'\n")
sys.exit(4)
# Verify real newlines sit between the BEGIN/END markers — otherwise
# GoogleCredential will crash with the exact same ArgumentException.
begin_idx = pk.index("-----BEGIN PRIVATE KEY-----")
end_idx = pk.index("-----END PRIVATE KEY-----")
if begin_idx >= end_idx:
sys.stderr.write("FATAL: BEGIN marker must appear before END marker in private_key\n")
sys.exit(5)
body = pk[begin_idx:end_idx]
if "\n" not in body:
sys.stderr.write(
"FATAL: private_key PEM has no real newlines between BEGIN and END - "
"GoogleCredential needs real \\n in the PEM body, not literal \\\\n.\n"
)
sys.exit(6)
# Re-serialize canonically so on-disk JSON is consistent and PEM is human-readable.
with open(path, "w") as f:
json.dump(data, f, indent=2)
PYSCRIPT
if ! sudo python3 "$PEM_CHECK_SCRIPT"; then
echo ">>> FATAL: $APP_DIR/firebase-adminsdk.json failed sanity check (exit $?)"
echo ">>> Aborting deploy before pulling new image."
exit 1
fi
🧰 Tools
🪛 ast-grep (0.44.1)

[warning] 290-290: Writing to or reading from a hardcoded, predictable path under /tmp is vulnerable to symlink and TOCTOU attacks: a local attacker can pre-create the file (or a symlink pointing elsewhere) and hijack or corrupt the contents. Generate a unique, unpredictable temporary file with mktemp instead, e.g. tmpfile="$(mktemp)" (or mktemp -d for directories) and reference "$tmpfile".
Context: /tmp/firebase_pem_check.py
Note: [CWE-377] Insecure Temporary File.

(predictable-tmp-file-bash)


[warning] 295-295: Writing to or reading from a hardcoded, predictable path under /tmp is vulnerable to symlink and TOCTOU attacks: a local attacker can pre-create the file (or a symlink pointing elsewhere) and hijack or corrupt the contents. Generate a unique, unpredictable temporary file with mktemp instead, e.g. tmpfile="$(mktemp)" (or mktemp -d for directories) and reference "$tmpfile".
Context: /tmp/firebase_pem_check.py
Note: [CWE-377] Insecure Temporary File.

(predictable-tmp-file-bash)

🪛 Betterleaks (1.6.1)

[high] 270-271: Identified a Private Key, which may compromise cryptographic security and sensitive data encryption.

(private-key)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/scripts/deploy-server.sh` around lines 257 - 296, Replace the
predictable /tmp/firebase_pem_check.py path in the PEM validation block with a
securely created temporary file using mktemp, and execute that generated path
with sudo. Add trap-based cleanup immediately after creation so the temporary
script is removed on both successful and failed validation, eliminating the
existing rm-based cleanup dependency; preserve the current validation behavior
and failure messages.

Source: Linters/SAST tools

Comment on lines +504 to +518
PULL_OK=0
for attempt in 1 2 3; do
if sudo docker pull "$IMAGE:main" 2>&1 | tail -5; then
PULL_OK=1
break
fi
echo " ! Pull attempt $attempt failed, retrying in 5s..."
sleep 5
done
if [ "$PULL_OK" = "0" ]; then
echo " ! Pull still failing after 3 attempts — restarting containerd and trying once more"
sudo systemctl restart containerd 2>/dev/null || sudo systemctl restart docker 2>/dev/null || true
sleep 5
sudo docker pull "$IMAGE:main" || { echo "Failed to pull image $IMAGE:main"; exit 1; }
fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
grep -n 'set -e\|pipefail' backend/scripts/deploy-server.sh | head -20

Repository: EXE201-MenuGreen/MenuGreenSystem

Length of output: 396


🏁 Script executed:

#!/bin/bash
set -e
printf '\n[Top of backend/scripts/deploy-server.sh]\n'
sed -n '1,30p' backend/scripts/deploy-server.sh | cat -n

printf '\n[Retry block around lines 504-518]\n'
sed -n '504,518p' backend/scripts/deploy-server.sh | cat -n

printf '\n[Search for pipefail]\n'
grep -n 'pipefail' backend/scripts/deploy-server.sh || true

Repository: EXE201-MenuGreen/MenuGreenSystem

Length of output: 2600


🏁 Script executed:

#!/bin/bash
bash -lc '
set -e
false | tail -5
echo "pipeline_status=$?"
if false | tail -5; then echo IF_OK; else echo IF_FAIL; fi
'

Repository: EXE201-MenuGreen/MenuGreenSystem

Length of output: 194


Retry loop needs pipefail or explicit pull status handling. With only set -e, if sudo docker pull ... | tail -5; then tests tail’s exit code, so a failed pull can still set PULL_OK=1 and skip the retry/fallback path. Add set -o pipefail or check ${PIPESTATUS[0]} directly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/scripts/deploy-server.sh` around lines 504 - 518, Ensure the pull
retry condition around the docker pull pipeline reflects the docker command’s
exit status rather than tail’s status. Enable pipefail before the loop or
explicitly inspect the pull command status via PIPESTATUS, so failed pulls
trigger retries and the existing containerd restart fallback.

Comment on lines +513 to +517
if [ "$PULL_OK" = "0" ]; then
echo " ! Pull still failing after 3 attempts — restarting containerd and trying once more"
sudo systemctl restart containerd 2>/dev/null || sudo systemctl restart docker 2>/dev/null || true
sleep 5
sudo docker pull "$IMAGE:main" || { echo "Failed to pull image $IMAGE:main"; exit 1; }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Restarting containerd/docker on pull failure can take down unrelated containers on the host.

sudo systemctl restart containerd 2>/dev/null || sudo systemctl restart docker 2>/dev/null || true restarts the shared container runtime daemon. Restarting dockerd without live-restore enabled typically stops running containers host-wide, and even a containerd restart can disrupt other workloads sharing this host — turning a single image-pull hiccup into a broader outage for services unrelated to this deploy.

Consider scoping recovery to this deploy only (e.g., clear the stale attestation blob under the containerd content store, or just fail fast and let CI retry) rather than restarting the daemon that every other container on the box depends on.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/scripts/deploy-server.sh` around lines 513 - 517, Remove the
host-wide containerd/docker restart from the PULL_OK recovery branch in
deploy-server.sh. Scope recovery to this deployment only, such as clearing the
stale image metadata or failing the pull so CI can retry, while preserving the
final failure handling for unsuccessful docker pulls.

Deploy reached the Firebase sanity-check (commit 20ebdce) which
correctly rejected the on-disk PEM with:

  FATAL: private_key PEM has no real newlines between BEGIN and END -
         GoogleCredential needs real \n in the PEM body, not literal \n.

Root cause: the multi-line FIREBASE_CREDENTIALS_JSON secret was
extracted from Doppler's bulk '--format env' download. Doppler wraps
multi-line values in double quotes, escapes embedded double quotes as
backslash-quote, and (depending on Doppler CLI version) either
preserves or escapes newlines. The Python extractor in deploy-server.sh
attempted to un-escape double-quotes but had no robust handling for
newlines - any line that lost its real newlines (or kept the literal
backslash-n 2-char sequence) ended up as a one-line PEM body. The
sanity check's 'no \n between BEGIN and END' assertion caught this and
aborted the deploy.

Per Doppler docs
(https://docs.doppler.com/docs/accessing-secrets#mounting), the
recommended way to fetch a multi-line secret for a file-on-disk use
case is:

  doppler secrets get <NAME> --plain > /etc/path/to/secret

This emits the raw secret value byte-for-byte, with NO quote-wrapping
and NO escape sequences. If the secret was ingested as a raw JSON
file (which Firebase Admin SDK expects), the output IS the JSON file
content - exactly what GoogleCredential.FromFile wants to read.

Fix: replace the in-script Python extractor with a single
'doppler secrets get FIREBASE_CREDENTIALS_JSON --plain' call. The
sanity check (json.load + PEM newline verification + canonical
json.dump(indent=2) re-serialize) stays in place as a safety net for
the unlikely case where Doppler CLI has a bug or the secret is
ingested in an unexpected encoding.

The previous 4 commits on this same issue were all attempts to teach
the --format env extractor to un-escape the value correctly. They are
superseded by this approach, which avoids the escape dance entirely.

Verified locally with a 4-case test:
- Real \n in PEM  -> PASS (the actual --plain output form)
- Literal \n    -> REJECTED (safety net catches it)
- No newlines     -> REJECTED (safety net catches it)
- Missing field   -> REJECTED (safety net catches it)

Co-authored-by: Cursor <cursoragent@cursor.com>
The file was created by 'sudo tee' (owned by root), but 'rm' was
running without sudo. The first deploy creates the file; every
subsequent deploy then fails to remove it with:

  rm: cannot remove '/tmp/firebase_pem_check.py': Operation not permitted

That non-zero exit propagates out of deploy-server.sh and aborts
the CD workflow even though the Firebase sanity check itself passed.

Add 'sudo' so the rm uses the same privilege as the original tee.

Confirmed by log run on 2026-07-15T20:13:24: the previous commit
('doppler secrets get --plain') succeeded materializing the JSON,
the sanity check exited 0, and only the cleanup 'rm' failed with
exit code 1, killing the whole deploy.

Co-authored-by: Cursor <cursoragent@cursor.com>
Document the root cause and the two-commit fix:
- 273403e: switch to 'doppler secrets get --plain'
- 8225e0d: 'sudo rm -f /tmp/firebase_pem_check.py'

Co-authored-by: Cursor <cursoragent@cursor.com>
@Johnny-Dang
Johnny-Dang merged commit 91718da into main Jul 15, 2026
6 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request Jul 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant