web-ext-deploy
Version:
A tool for deploying WebExtensions to multiple stores.
173 lines (172 loc) • 6.58 kB
JavaScript
import Axios from "axios";
import chalk from "chalk";
import dedent from "dedent";
import { backOff } from "exponential-backoff";
import status from "http-status";
import { getErrorMessage, getExtJson, getVerboseMessage, logSuccessfullyPublished } from "../../utils.js";
import fs from "fs";
const STORE = "Edge";
let axios;
async function handleRequestWithBackOff({ sendRequest, errorActionOnFailure, zip, productId, isGetLocation }) {
while (true) {
try {
const { data, headers } = await sendRequest();
return [undefined, isGetLocation ? headers.location : data];
}
catch (e) {
const isServerError = e.response.status >= 500;
if (isServerError) {
await backOff(Promise.resolve, { maxDelay: 30_000, delayFirstAttempt: true, jitter: "full" });
continue;
}
if (e.response.status === status.TOO_MANY_REQUESTS) {
const secondsToWait = Number(e.response.data.message.match(/\d+/)[0]);
if (secondsToWait >= 60) {
const newTime = new Date(Date.now() + secondsToWait * 1000).toLocaleTimeString();
console.log(chalk.yellow(getVerboseMessage({
store: STORE,
message: dedent(`
Too many requests. A retry will automatically be at ${newTime}
Or, you can deploy manually: https://partner.microsoft.com/en-us/dashboard/microsoftedge/${productId}/packages/dashboard
`),
prefix: "Warning"
})));
}
await new Promise(resolve => setTimeout(resolve, secondsToWait * 1000));
continue;
}
// Some sort of client error
return [
getErrorMessage({
store: STORE,
error: e.response.statusText,
actionName: errorActionOnFailure,
zip
})
];
}
}
}
async function checkStatusOfPackageUpload({ productId, operationId, zip }) {
// https://learn.microsoft.com/en-us/microsoft-edge/extensions-chromium/publish/api/using-addons-api#checking-the-status-of-a-package-upload
const sendRequest = () => axios(`/products/${productId}/submissions/draft/package/operations/${operationId}`);
let data;
let error;
do {
[error, data] = await handleRequestWithBackOff({
sendRequest,
errorActionOnFailure: "verify upload of",
zip,
productId
});
if (error) {
return [error];
}
} while (data.status === "InProgress");
if (data.status === "Failed") {
const errors = data.errors.map(({ message }) => message).join("\n");
return [errors];
}
return [undefined, data];
}
async function uploadZip({ zip, productId }) {
// https://learn.microsoft.com/en-us/microsoft-edge/extensions-chromium/publish/api/using-addons-api#uploading-a-package-to-update-an-existing-submission
const sendRequest = () => axios.post(`/products/${productId}/submissions/draft/package`, fs.createReadStream(zip), {
headers: {
"Content-Type": "application/zip"
}
});
const [error, location] = await handleRequestWithBackOff({
sendRequest,
errorActionOnFailure: "upload",
zip,
productId,
isGetLocation: true
});
if (error) {
return [error];
}
return [undefined, location];
}
async function publishSubmission({ zip, productId, devChangelog }) {
// https://learn.microsoft.com/en-us/microsoft-edge/extensions-chromium/publish/api/using-addons-api#publishing-the-submission
const sendRequest = () => axios.post(`/products/${productId}/submissions`, { notes: devChangelog });
const [error, location] = await handleRequestWithBackOff({
sendRequest,
errorActionOnFailure: "publish",
productId,
zip,
isGetLocation: true
});
if (error) {
return [error];
}
return [undefined, location];
}
async function checkPublishStatus({ zip, productId, operationId }) {
// https://learn.microsoft.com/en-us/microsoft-edge/extensions-chromium/publish/api/using-addons-api#checking-the-status-of-a-package-upload
const sendRequest = () => axios(`/products/${productId}/submissions/operations/${operationId}`);
const [error, data] = await handleRequestWithBackOff({
sendRequest,
errorActionOnFailure: "check the submission status of",
zip,
productId
});
if (error) {
return [error];
}
if (!("status" in data)) {
return [data.message];
}
if (data.status === "Failed") {
const errors = [];
for (const error of data.errors || []) {
errors.push(error.message);
}
if (errors.length === 0) {
errors.push(data.message);
}
return [errors.join("\n")];
}
return [undefined, data];
}
export async function deployToEdgePublishApi({ productId, clientId, apiKey, zip, verbose: isVerbose, devChangelog }) {
axios = Axios.create({
baseURL: "https://api.addons.microsoftedge.microsoft.com/v1",
headers: {
Authorization: `ApiKey ${apiKey}`,
"X-ClientID": clientId
}
});
const { name } = getExtJson(zip);
if (isVerbose) {
console.log(getVerboseMessage({ store: STORE, message: `Uploading zip of ${name} with product ID ${productId}` }));
}
let [error, operationId] = await uploadZip({ zip, productId });
if (error) {
throw error;
}
if (isVerbose) {
console.log(getVerboseMessage({ store: STORE, message: `Verifying upload` }));
}
[error] = await checkStatusOfPackageUpload({ zip, productId, operationId });
if (error) {
throw error;
}
if (isVerbose) {
console.log(getVerboseMessage({ store: STORE, message: `Publishing submission` }));
}
[error, operationId] = await publishSubmission({ zip, productId, devChangelog });
if (error) {
throw error;
}
if (isVerbose) {
console.log(getVerboseMessage({ store: STORE, message: `Checking the submission status` }));
}
[error] = await checkPublishStatus({ zip, productId, operationId });
if (error) {
throw error;
}
logSuccessfullyPublished({ store: STORE, extId: productId, zip });
return true;
}