Appearance
App Engine Deployment
When to use
Use this script when deploying a Node.js application to Google App Engine and you want a repeatable, safe deployment process that integrates with git. The script enforces a clean working tree, derives a version name from the current branch, deploys without promoting, and tags the deployment in git.
The pattern
Build a CLI deploy script that:
- Guards against dirty state — reject deployment when there are uncommitted changes
- Derives a version name — use the current git branch as the default App Engine version identifier
- Deploys without promoting — use
--no-promoteso the new version receives no traffic until explicitly promoted - Tags the deployment — create a git tag so the exact commit is traceable to each deployed version
Deploying
Deploy the current branch interactively:
sh
npm run deploy
# Deploys version "feature-my-feature" (from branch name)
# gcloud prompts for confirmationDeploy a named version silently for CI:
sh
npm run deploy:quiet -- staging
# Deploys version "staging" with no promptsAfter deploying, promote a version to receive traffic:
sh
gcloud app versions migrate staging \
--project=my-project-idList all deployed versions:
sh
gcloud app versions list --project=my-project-idDefining the deploy script
javascript
#!/usr/bin/env node
import { execSync, execFileSync } from "node:child_process";
import { program } from "commander";
const PROJECT_ID = process.env.GCLOUD_PROJECT
?? "my-project-id";
// Guard: no uncommitted changes
const diff = execSync("git diff", { encoding: "utf-8" }).trim();
if (diff) {
console.error(
"Uncommitted changes detected. Commit before deploying."
);
process.exit(1);
}
const defaultVersion = execSync(
"git branch --show-current",
{ encoding: "utf-8" },
).trim().replaceAll("/", "-");
program.name("deploy");
program.description("Deploy to App Engine");
program.argument("[version]", "version name", defaultVersion);
program.option("--quiet", "suppress interactive prompts", false);
program.action(async (version, options) => {
const args = [
"app", "deploy", "app.yaml",
"--no-promote",
`--project=${PROJECT_ID}`,
`--version=${version}`,
];
if (options.quiet) args.push("--quiet");
try {
execFileSync("gcloud", args, { stdio: "inherit" });
tagDeployment(version);
} catch {
console.error("Deployment failed.");
process.exit(1);
}
});
program.parse();
function tagDeployment(version) {
const tag = `deployments/${version}`;
execSync(`git tag ${tag} --force`);
console.log();
console.log(`Tag created: ${tag}`);
}The app.yaml file
Every App Engine app needs app.yaml in the project root:
yaml
runtime: nodejs22
instance_class: F1
handlers:
- url: /.*
script: autoPackage script
Add the deploy command to package.json so the team uses a consistent entry point:
json
{
"scripts": {
"deploy": "node scripts/deploy.js",
"deploy:quiet": "node scripts/deploy.js --quiet"
}
}Trade-offs
--no-promoteis safer but adds a manual step to promote. In CI, follow the deploy with an explicitgcloud app versions migratecommand if you want automatic traffic shifting.- Branch name as version is convenient but can produce awkward names from deep branch paths. The
replaceAllcall converts slashes to hyphens, but very long branch names may hit the App Engine version name length limit. - Git tag with
--forcemoves the tag if it already exists, which loses the previous tag position. This is intentional — the tag always points to the latest deploy of that version name. If you need history, use annotated tags or a deploy log instead. - Uncommitted change guard catches dirty state but does not check for unpushed commits. Add a
git diff origincheck if you need to enforce that code is pushed before deployment. execSync/execFileSyncblocks the process, which is appropriate for a CLI deploy script. For server-side or concurrent use, switch to the asyncspawnalternatives.