-
Notifications
You must be signed in to change notification settings - Fork 233
Fix: treat "version is the current active version" as a successful deploy (#461) #462
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -40,6 +40,23 @@ export type ProductionSuccessResult = { | |
| }; | ||
| }; | ||
|
|
||
| type ChannelListResult = { | ||
| status: "success"; | ||
| result: { | ||
| channels: Array<{ name: string; url: string; expireTime: string }>; | ||
| }; | ||
| }; | ||
|
|
||
| // Firebase returns this (FAILED_PRECONDITION) when the version being released is | ||
| // already the live/active version for the target. The upload + finalize already | ||
| // succeeded and the site is serving the correct content, so it's a successful | ||
| // no-op rather than a failure. | ||
| const ALREADY_ACTIVE_VERSION = /is the current active version/i; | ||
|
|
||
| export function isAlreadyActiveVersionError(message: unknown): boolean { | ||
| return typeof message === "string" && ALREADY_ACTIVE_VERSION.test(message); | ||
| } | ||
|
|
||
| type DeployConfig = { | ||
| projectId: string; | ||
| target?: string; | ||
|
|
@@ -110,18 +127,28 @@ async function execWithCredentials( | |
| } | ||
| ); | ||
| } catch (e) { | ||
| console.log(Buffer.concat(deployOutputBuf).toString("utf-8")); | ||
| const output = Buffer.concat(deployOutputBuf).toString("utf-8"); | ||
| console.log(output); | ||
| console.log(e.message); | ||
|
|
||
| if (!debug) { | ||
| console.log( | ||
| "Retrying deploy with the --debug flag for better error output" | ||
| ); | ||
| await execWithCredentials(args, projectId, gacFilename, { | ||
| debug: true, | ||
| firebaseToolsVersion, | ||
| force, | ||
| }); | ||
| if ( | ||
| isAlreadyActiveVersionError(output) || | ||
| isAlreadyActiveVersionError(e.message) | ||
| ) { | ||
| console.log( | ||
| "The deployed version is already the current active version; treating as a successful no-op deploy." | ||
| ); | ||
| } else { | ||
| console.log( | ||
| "Retrying deploy with the --debug flag for better error output" | ||
| ); | ||
| await execWithCredentials(args, projectId, gacFilename, { | ||
| debug: true, | ||
| firebaseToolsVersion, | ||
| force, | ||
| }); | ||
| } | ||
| } else { | ||
| throw e; | ||
| } | ||
|
|
@@ -155,9 +182,60 @@ export async function deployPreview( | |
| | ChannelSuccessResult | ||
| | ErrorResult; | ||
|
|
||
| if ( | ||
| deploymentResult.status === "error" && | ||
| isAlreadyActiveVersionError(deploymentResult.error) | ||
| ) { | ||
| return await getExistingChannel(gacFilename, deployConfig); | ||
| } | ||
|
|
||
| return deploymentResult; | ||
| } | ||
|
|
||
| async function getExistingChannel( | ||
| gacFilename: string, | ||
| deployConfig: ChannelDeployConfig | ||
| ): Promise<ChannelSuccessResult> { | ||
| const { projectId, channelId, target, firebaseToolsVersion } = deployConfig; | ||
|
|
||
| const listText = await execWithCredentials( | ||
| ["hosting:channel:list", ...(target ? ["--site", target] : [])], | ||
| projectId, | ||
| gacFilename, | ||
| { firebaseToolsVersion } | ||
| ); | ||
|
|
||
| const list = JSON.parse(listText.trim()) as ChannelListResult | ErrorResult; | ||
|
|
||
| const channel = | ||
| list.status === "success" | ||
| ? list.result.channels.find((c) => | ||
| c.name.endsWith(`/channels/${channelId}`) | ||
| ) | ||
| : undefined; | ||
|
|
||
| if (!channel) { | ||
| console.log( | ||
| `Could not find channel "${channelId}" when reading back the already-active deploy; reporting success without URL details.` | ||
| ); | ||
| } | ||
|
|
||
| const site = | ||
| channel?.name.match(/\/sites\/([^/]+)\//)?.[1] ?? target ?? channelId; | ||
|
|
||
| return { | ||
| status: "success", | ||
| result: { | ||
| [site]: { | ||
| site, | ||
| ...(target ? { target } : {}), | ||
| url: channel?.url ?? "", | ||
| expireTime: channel?.expireTime ?? "", | ||
| }, | ||
| }, | ||
| }; | ||
| } | ||
|
Comment on lines
+195
to
+237
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If To make this more robust, we should wrap the channel list retrieval in a async function getExistingChannel(
gacFilename: string,
deployConfig: ChannelDeployConfig
): Promise<ChannelSuccessResult> {
const { projectId, channelId, target, firebaseToolsVersion } = deployConfig;
const site = target ?? channelId;
try {
const listText = await execWithCredentials(
["hosting:channel:list", ...(target ? ["--site", target] : [])],
projectId,
gacFilename,
{ firebaseToolsVersion }
);
const list = JSON.parse(listText.trim()) as ChannelListResult | ErrorResult;
const channel =
list.status === "success"
? list.result.channels.find((c) =>
c.name.endsWith(`/channels/${channelId}`)
)
: undefined;
if (!channel) {
console.log(
`Could not find channel "${channelId}" when reading back the already-active deploy; reporting success without URL details.`
);
if (list.status === "error") {
console.log(`Channel list error: ${list.error}`);
}
}
const resolvedSite =
channel?.name.match(/\/sites\/([^/]+)\/\//)?.[1] ?? site;
return {
status: "success",
result: {
[resolvedSite]: {
site: resolvedSite,
...(target ? { target } : {}),
url: channel?.url ?? "",
expireTime: channel?.expireTime ?? "",
},
},
};
} catch (e) {
console.log(
`Failed to retrieve channel list: ${e.message}. Reporting success without URL details.`
);
return {
status: "success",
result: {
[site]: {
site,
...(target ? { target } : {}),
url: "",
expireTime: "",
},
},
};
}
} |
||
|
|
||
| export async function deployProductionSite( | ||
| gacFilename, | ||
| productionDeployConfig: ProductionDeployConfig | ||
|
|
@@ -176,5 +254,15 @@ export async function deployProductionSite( | |
| | ProductionSuccessResult | ||
| | ErrorResult; | ||
|
|
||
| if ( | ||
| deploymentResult.status === "error" && | ||
| isAlreadyActiveVersionError(deploymentResult.error) | ||
| ) { | ||
| return { | ||
| status: "success", | ||
| result: { hosting: target || projectId }, | ||
| }; | ||
| } | ||
|
|
||
| return deploymentResult; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If
debugistrue(for example, if the action is run with debug options or during a retry), thecatchblock will immediately execute theelsebranch and throw the error, completely bypassing theisAlreadyActiveVersionErrorcheck. This means an "already active version" error will still cause the deployment to fail when debugging is active.We should check
isAlreadyActiveVersionErrorfirst, before checkingif (!debug).