Skip to content

Commit c34bbef

Browse files
authored
Set local build configs throughout the deploy steps (#9184)
Sets the deploy context config for localBuild backends In prepare, call the local build and set the build outputs in the config In deploy, zip and upload the build directory
1 parent 6ab08e8 commit c34bbef

File tree

7 files changed

+201
-21
lines changed

7 files changed

+201
-21
lines changed

src/deploy/apphosting/args.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { BuildConfig } from "../../gcp/apphosting";
44
export interface LocalBuild {
55
buildConfig: BuildConfig;
66
buildDir: string;
7+
annotations: Record<string, string>;
78
}
89

910
export interface Context {

src/deploy/apphosting/deploy.spec.ts

Lines changed: 91 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,28 @@ function initializeContext(): Context {
3535
ignore: [],
3636
},
3737
],
38+
[
39+
"foo-local-build",
40+
{
41+
backendId: "foo-local-build",
42+
rootDir: "/",
43+
ignore: [],
44+
localBuild: true,
45+
},
46+
],
47+
]),
48+
backendLocations: new Map<string, string>([
49+
["foo", "us-central1"],
50+
["foo-local-build", "us-central1"],
3851
]),
39-
backendLocations: new Map<string, string>([["foo", "us-central1"]]),
4052
backendStorageUris: new Map<string, string>(),
41-
backendLocalBuilds: {},
53+
backendLocalBuilds: {
54+
"foo-local-build": {
55+
buildDir: "./nextjs/standalone",
56+
buildConfig: {},
57+
annotations: {},
58+
},
59+
},
4260
};
4361
}
4462

@@ -67,17 +85,25 @@ describe("apphosting", () => {
6785
sinon.verifyAndRestore();
6886
});
6987

70-
describe("deploy", () => {
88+
describe("deploy local source", () => {
7189
const opts = {
7290
...BASE_OPTS,
7391
projectId: "my-project",
7492
only: "apphosting",
7593
config: new Config({
76-
apphosting: {
77-
backendId: "foo",
78-
rootDir: "/",
79-
ignore: [],
80-
},
94+
apphosting: [
95+
{
96+
backendId: "foo",
97+
rootDir: "/",
98+
ignore: [],
99+
},
100+
{
101+
backendId: "foo-local-build",
102+
rootDir: "/",
103+
ignore: [],
104+
localBuild: true,
105+
},
106+
],
81107
}),
82108
};
83109

@@ -89,36 +115,87 @@ describe("apphosting", () => {
89115
original: new FirebaseError("original error", { status: 404 }),
90116
}),
91117
);
118+
getBucketStub.onSecondCall().rejects(
119+
new FirebaseError("error", {
120+
original: new FirebaseError("original error", { status: 404 }),
121+
}),
122+
);
92123
createBucketStub.resolves();
93-
createArchiveStub.resolves("path/to/foo-1234.zip");
94-
uploadObjectStub.resolves({
124+
createArchiveStub.onFirstCall().resolves("path/to/foo-1234.zip");
125+
createArchiveStub.onSecondCall().resolves("path/to/foo-local-build-1234.zip");
126+
uploadObjectStub.onFirstCall().resolves({
95127
bucket: "firebaseapphosting-sources-12345678-us-central1",
96128
object: "foo-1234",
97129
});
130+
uploadObjectStub.onSecondCall().resolves({
131+
bucket: "firebaseapphosting-build-12345678-us-central1",
132+
object: "foo-local-build-1234",
133+
});
134+
98135
createReadStreamStub.resolves();
99136

100137
await deploy(context, opts);
101138

102-
expect(createBucketStub).to.be.calledOnce;
139+
// assert backend foo calls
140+
expect(createBucketStub).to.be.calledWithMatch("my-project", {
141+
name: "firebaseapphosting-sources-000000000000-us-central1",
142+
location: "us-central1",
143+
lifecycle: sinon.match.any,
144+
});
145+
expect(createArchiveStub).to.be.calledWithExactly(
146+
context.backendConfigs.get("foo"),
147+
process.cwd(),
148+
undefined,
149+
);
150+
expect(uploadObjectStub).to.be.calledWithMatch(
151+
sinon.match.any,
152+
"firebaseapphosting-sources-000000000000-us-central1",
153+
);
154+
155+
// assert backend foo-local-build calls
156+
expect(createBucketStub).to.be.calledWithMatch("my-project", {
157+
name: "firebaseapphosting-build-000000000000-us-central1",
158+
location: "us-central1",
159+
lifecycle: sinon.match.any,
160+
});
161+
expect(createArchiveStub).to.be.calledWithExactly(
162+
context.backendConfigs.get("foo-local-build"),
163+
process.cwd(),
164+
"./nextjs/standalone",
165+
);
166+
expect(uploadObjectStub).to.be.calledWithMatch(
167+
sinon.match.any,
168+
"firebaseapphosting-build-000000000000-us-central1",
169+
);
103170
});
104171

105172
it("correctly creates and sets storage URIs", async () => {
106173
const context = initializeContext();
107174
getProjectNumberStub.resolves("000000000000");
108175
getBucketStub.resolves();
109176
createBucketStub.resolves();
110-
createArchiveStub.resolves("path/to/foo-1234.zip");
111-
uploadObjectStub.resolves({
112-
bucket: "firebaseapphosting-sources-12345678-us-central1",
177+
createArchiveStub.onFirstCall().resolves("path/to/foo-1234.zip");
178+
createArchiveStub.onSecondCall().resolves("path/to/foo-local-build-1234.zip");
179+
180+
uploadObjectStub.onFirstCall().resolves({
181+
bucket: "firebaseapphosting-sources-000000000000-us-central1",
113182
object: "foo-1234",
114183
});
184+
185+
uploadObjectStub.onSecondCall().resolves({
186+
bucket: "firebaseapphosting-build-000000000000-us-central1",
187+
object: "foo-local-build-1234",
188+
});
115189
createReadStreamStub.resolves();
116190

117191
await deploy(context, opts);
118192

119193
expect(context.backendStorageUris.get("foo")).to.equal(
120194
"gs://firebaseapphosting-sources-000000000000-us-central1/foo-1234.zip",
121195
);
196+
expect(context.backendStorageUris.get("foo-local-build")).to.equal(
197+
"gs://firebaseapphosting-build-000000000000-us-central1/foo-local-build-1234.zip",
198+
);
122199
});
123200
});
124201
});

src/deploy/apphosting/deploy.ts

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,9 @@ export default async function (context: Context, options: Options): Promise<void
2424
}
2525

2626
// Ensure that a bucket exists in each region that a backend is or will be deployed to
27-
for (const loc of context.backendLocations.values()) {
28-
const bucketName = `firebaseapphosting-sources-${options.projectNumber}-${loc.toLowerCase()}`;
27+
for (const [backendId, loc] of context.backendLocations) {
28+
const cfg = context.backendConfigs.get(backendId);
29+
const bucketName = `firebaseapphosting-${cfg?.localBuild ? "build" : "sources"}-${options.projectNumber}-${loc.toLowerCase()}`;
2930
try {
3031
await gcs.getBucket(bucketName);
3132
} catch (err) {
@@ -73,8 +74,20 @@ export default async function (context: Context, options: Options): Promise<void
7374
}
7475

7576
for (const cfg of context.backendConfigs.values()) {
76-
const projectSourcePath = options.projectRoot ? options.projectRoot : process.cwd();
77-
const zippedSourcePath = await createArchive(cfg, projectSourcePath);
77+
const rootDir = options.projectRoot ?? process.cwd();
78+
let builtAppDir;
79+
if (cfg.localBuild) {
80+
builtAppDir = context.backendLocalBuilds[cfg.backendId].buildDir;
81+
if (!builtAppDir) {
82+
throw new FirebaseError(`No local build dir found for ${cfg.backendId}`);
83+
}
84+
}
85+
const zippedSourcePath = await createArchive(cfg, rootDir, builtAppDir);
86+
logLabeledBullet(
87+
"apphosting",
88+
`Zipped ${cfg.localBuild ? "built app" : "source"} for backend ${cfg.backendId}`,
89+
);
90+
7891
const backendLocation = context.backendLocations.get(cfg.backendId);
7992
if (!backendLocation) {
8093
throw new FirebaseError(
@@ -83,19 +96,20 @@ export default async function (context: Context, options: Options): Promise<void
8396
}
8497
logLabeledBullet(
8598
"apphosting",
86-
`Uploading source code at ${projectSourcePath} for backend ${cfg.backendId}...`,
99+
`Uploading ${cfg.localBuild ? "built app" : "source"} for backend ${cfg.backendId}...`,
87100
);
101+
const gcsBucketName = `firebaseapphosting-${cfg.localBuild ? "build" : "sources"}-${options.projectNumber}-${backendLocation.toLowerCase()}`;
88102
const { bucket, object } = await gcs.uploadObject(
89103
{
90104
file: zippedSourcePath,
91105
stream: fs.createReadStream(zippedSourcePath),
92106
},
93-
`firebaseapphosting-sources-${options.projectNumber}-${backendLocation.toLowerCase()}`,
107+
gcsBucketName,
94108
);
95109
logLabeledBullet("apphosting", `Source code uploaded at gs://${bucket}/${object}`);
96110
context.backendStorageUris.set(
97111
cfg.backendId,
98-
`gs://firebaseapphosting-sources-${options.projectNumber}-${backendLocation.toLowerCase()}/${path.basename(zippedSourcePath)}`,
112+
`gs://${gcsBucketName}/${path.basename(zippedSourcePath)}`,
99113
);
100114
}
101115
}

src/deploy/apphosting/prepare.spec.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import * as prompt from "../../prompt";
1010
import { RC } from "../../rc";
1111
import { Context } from "./args";
1212
import prepare, { getBackendConfigs } from "./prepare";
13+
import * as localbuilds from "../../apphosting/localbuilds";
1314

1415
const BASE_OPTS = {
1516
cwd: "/",
@@ -75,6 +76,58 @@ describe("apphosting", () => {
7576
});
7677

7778
describe("prepare", () => {
79+
it("correctly creates configs for localBuild backends", async () => {
80+
const optsWithLocalBuild = {
81+
...opts,
82+
config: new Config({
83+
apphosting: {
84+
backendId: "foo",
85+
rootDir: "/",
86+
ignore: [],
87+
localBuild: true,
88+
},
89+
}),
90+
};
91+
const context = initializeContext();
92+
93+
const annotations = {
94+
adapterPackageName: "@apphosting/angular-adapter",
95+
adapterVersion: "14.1",
96+
framework: "nextjs",
97+
};
98+
const buildConfig = {
99+
runCommand: "npm run build:prod",
100+
env: [],
101+
};
102+
sinon.stub(localbuilds, "localBuild").resolves({
103+
outputFiles: ["./next/standalone"],
104+
buildConfig,
105+
annotations,
106+
});
107+
listBackendsStub.onFirstCall().resolves({
108+
backends: [
109+
{
110+
name: "projects/my-project/locations/us-central1/backends/foo",
111+
},
112+
],
113+
});
114+
115+
await prepare(context, optsWithLocalBuild);
116+
117+
expect(context.backendLocations.get("foo")).to.equal("us-central1");
118+
expect(context.backendConfigs.get("foo")).to.deep.equal({
119+
backendId: "foo",
120+
rootDir: "/",
121+
ignore: [],
122+
localBuild: true,
123+
});
124+
expect(context.backendLocalBuilds["foo"]).to.deep.equal({
125+
buildDir: "./next/standalone",
126+
buildConfig,
127+
annotations,
128+
});
129+
});
130+
78131
it("links to existing backend if it already exists", async () => {
79132
const context = initializeContext();
80133
listBackendsStub.onFirstCall().resolves({
@@ -93,6 +146,7 @@ describe("apphosting", () => {
93146
rootDir: "/",
94147
ignore: [],
95148
});
149+
expect(context.backendLocalBuilds).to.deep.equal({});
96150
});
97151

98152
it("creates a backend if it doesn't exist yet", async () => {
@@ -113,6 +167,7 @@ describe("apphosting", () => {
113167
rootDir: "/",
114168
ignore: [],
115169
});
170+
expect(context.backendLocalBuilds).to.deep.equal({});
116171
});
117172

118173
it("skips backend deployment if alwaysDeployFromSource is false", async () => {
@@ -143,6 +198,7 @@ describe("apphosting", () => {
143198

144199
expect(context.backendLocations.get("foo")).to.equal(undefined);
145200
expect(context.backendConfigs.get("foo")).to.deep.equal(undefined);
201+
expect(context.backendLocalBuilds).to.deep.equal({});
146202
});
147203

148204
it("prompts user if codebase is already connected and alwaysDeployFromSource is undefined", async () => {
@@ -172,6 +228,7 @@ describe("apphosting", () => {
172228
ignore: [],
173229
alwaysDeployFromSource: true,
174230
});
231+
expect(context.backendLocalBuilds).to.deep.equal({});
175232
});
176233
});
177234

src/deploy/apphosting/prepare.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ import { Options } from "../../options";
1111
import { needProjectId } from "../../projectUtils";
1212
import { checkbox, confirm } from "../../prompt";
1313
import { logLabeledBullet, logLabeledWarning } from "../../utils";
14+
import { localBuild } from "../../apphosting/localbuilds";
1415
import { Context } from "./args";
16+
import { FirebaseError } from "../../error";
1517

1618
/**
1719
* Prepare backend targets to deploy from source. Checks that all required APIs are enabled,
@@ -26,6 +28,7 @@ export default async function (context: Context, options: Options): Promise<void
2628
context.backendConfigs = new Map<string, AppHostingSingle>();
2729
context.backendLocations = new Map<string, string>();
2830
context.backendStorageUris = new Map<string, string>();
31+
context.backendLocalBuilds = {};
2932

3033
const configs = getBackendConfigs(options);
3134
const { backends } = await listBackends(projectId, "-");
@@ -144,6 +147,32 @@ export default async function (context: Context, options: Options): Promise<void
144147
`Skipping deployment of backend(s) ${skippedBackends.map((cfg) => cfg.backendId).join(", ")}.`,
145148
);
146149
}
150+
151+
for (const config of context.backendConfigs.values()) {
152+
if (!config.localBuild) {
153+
continue;
154+
}
155+
logLabeledBullet("apphosting", `Starting local build for backend ${config.backendId}`);
156+
try {
157+
const { outputFiles, annotations, buildConfig } = await localBuild(
158+
options.projectRoot || "./",
159+
"nextjs",
160+
);
161+
if (outputFiles.length !== 1) {
162+
throw new FirebaseError(
163+
`Local build for backend ${config.backendId} failed: No output files found.`,
164+
);
165+
}
166+
context.backendLocalBuilds[config.backendId] = {
167+
// TODO(9114): This only works for nextjs.
168+
buildDir: outputFiles[0],
169+
buildConfig,
170+
annotations,
171+
};
172+
} catch (e) {
173+
throw new FirebaseError(`Local Build for backend ${config.backendId} failed: ${e}`);
174+
}
175+
}
147176
return;
148177
}
149178

src/deploy/apphosting/release.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ describe("apphosting", () => {
6363
env: [{ variable: "CHICKEN", value: "bok-bok" }],
6464
},
6565
buildDir: "./",
66+
annotations: {},
6667
},
6768
},
6869
};

src/deploy/apphosting/release.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export default async function (context: Context, options: Options): Promise<void
3434
continue;
3535
}
3636
backendIds.push(backendId);
37+
// TODO(9114): Add run_command
3738
let buildConfig;
3839
if (config.localBuild) {
3940
buildConfig = context.backendLocalBuilds[backendId].buildConfig;

0 commit comments

Comments
 (0)