Skip to main content

Command Palette

Search for a command to run...

How I Moved My Google AI Studio App to GitHub CI

Three wrong Fixes, one ghost container, and an afternoon I did not plan for.

Updated
6 min readView as Markdown
How I Moved My Google AI Studio App to GitHub CI
N

I am Nandan, And you probably know me as a "Software Engineer who "hacked" an Airline to retrieve his luggage".

I am a full-time Software Engineer, Tech Speaker, and mentor. I enjoy talking about Web Development, Machine Learning, Natural language Processing, Machine learning Accelerated Mobile Pages, Progressive Web Apps, Cybersecurity, Chatbots, etc.

My claim to fame was when I posted a series of tweets on Twitter about data privacy issues on an airline’s website and the tweet got viral for all the good reasons. The story was covered by all major media portals all around the world including BBC, Saudi Gazette, Times of India, Boing Boing, Lallantop etc. and I have been interviewed by some major radio channels and podcasts.

In my free time, I like to indulge myself in activities like Photography, Gardening, Snooker, or Boxing. I am a proud owner of many plants, I sometimes talk to them (mostly pep talks).

Kitaaben started life in Google AI Studio. No shame in that — it's genuinely the fastest way I know to get a full-stack app with Firebase wired up and live. But "fast to start" and "where your code should live forever" are two different things.

I wanted GitHub to be the source of truth. Real commit history. Real pull requests. A deploy pipeline I could reason about without a proprietary UI sitting in the middle. The service was already live on Cloud Run, so the plan was simple: point it at the GitHub repo and be done by lunch.

It took the whole afternoon. Just not the afternoon I expected.

The plan that should have worked

Cloud Run has a built-in feature for exactly this — Continuously deploy from a repository. Connect a GitHub repo, pick a branch, and it auto-generates a Cloud Build trigger that builds and deploys on every push. No YAML. Click, connect, push.

Snag 1. The very first build failed, but for a boring reason. My repo had two lockfiles — package-lock.json and a stray pnpm-lock.yaml that got committed by accident at some point. Google's Buildpacks saw the pnpm lockfile and built with pnpm. And recent pnpm versions block dependency install scripts by default as a supply-chain protection. So esbuild's postinstall script (the one that downloads its platform binary) never ran, and the build died before it even reached my code.

Fix: delete the stray lockfile. The project was never pnpm-based anyway. Quick, forgettable, not the story.

The error that refused to explain itself

Next push got further — all the way to the deploy step — and then this:

ERROR: (gcloud.run.services.update)
spec.template.metadata.annotations[run.googleapis.com/sources]:
Source annotation has sources that are not referenced by a container.

Read that sentence a few times. Nothing in it tells you what to do. No stack trace, no file, no line number. Just a Cloud Run validator rejecting a deploy against its own service definition.

Wrong theory #1: strip the annotation

The obvious read — some stale annotation is sitting on the live service, left over from however AI Studio originally provisioned it. So just remove it:

$ gcloud run services update kitaaben --region us-west1 \
    --remove-annotations=run.googleapis.com/sources
 
ERROR: unrecognized arguments: --remove-annotations=run.googleapis.com/sources
(did you mean '--remove-env-vars'?)

Turns out gcloud run services update has no generic "edit any annotation" flag. Most of Cloud Run's raw Knative-style metadata isn't meant to be hand-edited through convenience flags — precisely to stop people from doing what I was about to do.

Wrong theory #2: ditch Buildpacks entirely

New theory. The run.googleapis.com/sources annotation is part of Cloud Run's source-deploy tracking — it maps source paths to containers so the console can show "this revision was built from commit X." A plain prebuilt Docker image needs none of that. So: write a Dockerfile, sidestep Buildpacks and its source-tracking machinery completely.

I wrote a multi-stage Dockerfile. Built it locally, ran it, confirmed it booted and served traffic. Pushed.

And this is where it got genuinely confusing. The build went green. The live site didn't change.

The build log explained why. Adding a Dockerfile made Cloud Build quietly switch strategies. Instead of the four-step Buildpacks template it had been using (build → pull → push → deploy), it fell back to Google's generic "there's a Dockerfile here, build and push an image" default, which has no deploy step at all. Green build. No deploy. Nobody tells you.

Fix for this part: a handwritten cloudbuild.yaml with three explicit steps. docker build, docker push, gcloud run deploy --image. Three commands, in order, in the repo. No more guessing what the pipeline does.

Same error, completely different deploy method

With a real deploy step finally in place, I pushed again — and got the exact same annotation error. This time from a plain gcloud run deploy --image call that had nothing to do with source builds or Buildpacks at all.

That was the moment the real shape of the problem clicked. It was never about how I was deploying. Buildpacks, Dockerfile, source-based, image-based — didn't matter. Something was wrong with the live service's own persisted state, and every deploy method was stepping on the same landmine.

Exporting the body

Time to look at what the service actually thought it was running:

$ gcloud run services describe kitaaben --region us-west1 \
    --format export > service.yaml

Buried in that YAML: the container's image field wasn't a real image reference. It was the literal string scratch — Cloud Run's internal placeholder for services that resolve their actual container indirectly, through that same sources annotation I'd been fighting all afternoon.

The service was born this way. AI Studio provisioned it through its own source-deploy pipeline on day one, and it had been carrying that placeholder ever since. A ghost container, quietly broken since birth, and it only mattered the day I tried to deploy any other way.

So every attempt was failing one of two ways: either the deploy tried to update the sources annotation and Cloud Run rejected the mismatch, or the leftover scratch placeholder had nothing left to resolve against.

The actual fix

Fix both in one edit, apply atomically:

# in service.yaml:
#   1. delete the run.googleapis.com/sources annotation line
#   2. change image: scratch to the real image path
 
$ gcloud run services replace service.yaml
Applying new configuration to Cloud Run service [kitaaben]...
Deploying... done.

That was it. Not a flag. Not a build strategy. Not a YAML rewrite of the pipeline. A genuinely stuck field in the service's own state that no amount of retrying — from any deploy method — was ever going to clear.

If you're doing this migration

  • If AI Studio (or any source-deploy tool) originally provisioned your Cloud Run service, don't assume a clean deploy method inherits a clean service. describe --format export and read the spec before you spend a day guessing.

  • run.googleapis.com/sources and image: scratch travel together. See one, check for the other.

  • Cloud Build silently changes strategy based on what's in your repo. Dockerfile present vs absent produces different pipelines. If a build goes green but nothing deploys, check whether the deploy step still exists.

  • Not every Cloud Run field has a convenience flag. When --remove-X doesn't exist, the fallback is always: export the spec, hand-edit, services replace. Haven't touched the pipeline since. It's a Cloud Build trigger watching main, building a Docker image, deploying it. No mystery annotations.

GitHub is the source of truth now. AI Studio is where Kitaaben was born, not where it lives.

That’s all, folks! I hope you found this helpful. If you enjoyed this, check out more articles on my Blog, https://blog.nandan.dev/

Feel free to comment, email me at connect@nandan.dev, or connect with me on Twitter, Instagram, or GitHub. Don’t forget to subscribe to my newsletter for regular updates on JavaScript topics!

Twitter | Instagram | Github | Website