web-ext-deploy
Version:
A tool for deploying WebExtensions to multiple stores.
228 lines (227 loc) • 8.53 kB
JavaScript
import Axios from "axios";
import { backOff } from "exponential-backoff";
import FormData from "form-data";
import { getErrorMessage, getExtJson, getVerboseMessage, logSuccessfullyPublished } from "../../utils.js";
import fs from "fs";
const STORE = "Opera";
let axios;
async function handleRequestWithBackOff({ sendRequest, errorActionOnFailure, zip }) {
while (true) {
try {
const { data } = await sendRequest();
return [undefined, data];
}
catch (e) {
const isServerError = e.response.status >= 500;
if (isServerError) {
await backOff(() => Promise.resolve(), { maxDelay: 30_000, delayFirstAttempt: true, jitter: "full" });
continue;
}
const errors = Object.values(e.response.data || []);
// A client error
return [
getErrorMessage({
store: STORE,
error: errors.length > 0 ? errors.join("\n") : e.response.statusText,
actionName: errorActionOnFailure,
zip
})
];
}
}
}
async function verifySourceCodeExistence({ zip, packageId }) {
const { version, default_locale = "en" } = getExtJson(zip);
const sendRequest = async () => axios(`developer/package-versions/${packageId}-${version}/`);
const params = new URLSearchParams({ language: default_locale });
const url = `https://addons.opera.com/developer/package/${packageId}/version/${version}?${params}`;
const errorMessage = `No source code provided. Provide a URL in ${url} and submit the changes`;
const [error, data] = await handleRequestWithBackOff({
zip,
sendRequest,
errorActionOnFailure: "verify source code existence of"
});
if (error) {
return [error];
}
const isSourceCodeProvided = Boolean(data.source_url || data.source_for_moderators_url);
if (isSourceCodeProvided) {
return [undefined, isSourceCodeProvided];
}
return [errorMessage];
}
async function cancelLatestVersionIfNotSubmitted({ packageId, versionsListed, isVerbose, zip }) {
if (versionsListed.length === 0 || versionsListed[0].submitted_for_moderation) {
return [undefined];
}
const { version } = versionsListed[0];
if (isVerbose) {
console.log(getVerboseMessage({ store: STORE, message: `Canceling unsubmitted version ${version}` }));
}
const sendRequest = async () => {
return axios.post(`developer/package-versions/${packageId}-${version}/cancel_changes/`);
};
return handleRequestWithBackOff({
zip,
sendRequest,
errorActionOnFailure: "cancel unsubmitted changes of"
});
}
async function submitChanges({ zip, packageId }) {
const { version } = getExtJson(zip);
const sendRequest = async () => axios.post(`developer/package-versions/${packageId}-${version}/submit_for_moderation/`);
return handleRequestWithBackOff({
zip,
sendRequest,
errorActionOnFailure: "submit changes to"
});
}
function getFileMetadata(zipPath) {
const sizeInBytes = fs.statSync(zipPath).size;
const zipName = zipPath.split(/[\\/]/).pop();
const zipNameWithoutForbiddenCharacters = zipName.replace(/[.]/g, "");
const fileId = `${sizeInBytes}-${zipNameWithoutForbiddenCharacters}`;
return { zipName, fileId };
}
async function uploadZip({ zip }) {
const { zipName, fileId } = getFileMetadata(zip);
const formData = new FormData();
formData.append("flowChunkNumber", 1);
formData.append("flowFilename", zipName);
formData.append("flowIdentifier", fileId);
formData.append("file", fs.createReadStream(zip));
const sendRequest = async () => axios.post("file-upload/", formData);
return handleRequestWithBackOff({ zip, sendRequest, errorActionOnFailure: "upload zip for" });
}
async function verifyUploadSuccessful({ zipPath, packageId, lastVersion }) {
const { zipName, fileId } = getFileMetadata(zipPath);
const sendRequest = async () => axios.post("developer/package-versions/", {
file_id: fileId,
file_name: zipName,
metadata_from: lastVersion
}, {
params: {
package_id: packageId
}
});
const [error, data] = await handleRequestWithBackOff({
zip: zipPath,
sendRequest,
errorActionOnFailure: "verify upload of"
});
if (error) {
return [error];
}
if ("package_file" in data) {
return [data.package_file];
}
return [undefined, data];
}
async function updateChangelog({ zip, packageId, changelog }) {
const { version, default_locale = "en" } = getExtJson(zip);
const sendRequest = async () => axios.patch(`developer/package-versions/${packageId}-${version}/`, {
translations: {
[default_locale]: {
changelog
}
}
});
return handleRequestWithBackOff({ zip, sendRequest, errorActionOnFailure: "update changelog of" });
}
function verifyVersionNotSubmittedForModeration({ zip, versionsListed }) {
const { version } = getExtJson(zip);
const isVersionAlreadySubmitted = versionsListed.some(entry => entry.version === version && entry.submitted_for_moderation);
if (isVersionAlreadySubmitted) {
return [
getErrorMessage({
store: STORE,
error: `Version ${version} Has already been deployed`,
actionName: "update",
zip
})
];
}
return [undefined];
}
async function getVersions({ zip, packageId }) {
const sendRequest = async () => axios(`developer/packages/${packageId}/`);
return handleRequestWithBackOff({
zip,
sendRequest,
errorActionOnFailure: "get all package versions of"
});
}
export default async function deployToOpera({ sessionid, csrftoken, packageId, zip, changelog = "", verbose: isVerbose }) {
axios = Axios.create({
baseURL: `https://addons.opera.com/api/`,
headers: {
Accept: "application/json; version=1.0",
Cookie: `csrftoken=${csrftoken}; sessionid=${sessionid}`,
"X-Csrftoken": csrftoken,
Referer: "https://addons.opera.com"
}
});
const { name, version } = getExtJson(zip);
if (isVerbose) {
console.log(getVerboseMessage({
store: STORE,
message: `Retrieving listed versions of ${name} with package ID ${packageId}`
}));
}
// eslint-disable-next-line prefer-const
let [error, data] = await getVersions({ zip, packageId });
if (error) {
throw error;
}
if (isVerbose) {
console.log(getVerboseMessage({ store: STORE, message: `Verifying version ${version}` }));
}
[error] = verifyVersionNotSubmittedForModeration({ zip, versionsListed: data.versions });
if (error) {
throw error;
}
[error] = await cancelLatestVersionIfNotSubmitted({ zip, packageId, versionsListed: data.versions, isVerbose });
if (error) {
throw error;
}
if (isVerbose) {
console.log(getVerboseMessage({ store: STORE, message: "Uploading zip" }));
}
[error] = await uploadZip({ zip });
if (error) {
throw error;
}
if (isVerbose) {
console.log(getVerboseMessage({ store: STORE, message: "Verifying upload" }));
}
const lastVersion = data.versions.find(version => version.submitted_for_moderation)?.version || "";
[error] = await verifyUploadSuccessful({ zipPath: zip, packageId, lastVersion });
if (error) {
throw error;
}
if (isVerbose) {
console.log(getVerboseMessage({ store: STORE, message: "Verifying source code existence" }));
}
[error] = await verifySourceCodeExistence({ zip, packageId });
if (error) {
throw error;
}
if (changelog) {
if (isVerbose) {
console.log(getVerboseMessage({ store: STORE, message: "Updating changelog" }));
}
[error] = await updateChangelog({ zip, packageId, changelog });
if (error) {
throw error;
}
}
if (isVerbose) {
console.log(getVerboseMessage({ store: STORE, message: "Submitting changes" }));
}
[error] = await submitChanges({ zip, packageId });
if (error) {
throw error;
}
logSuccessfullyPublished({ extId: packageId, store: STORE, zip });
return true;
}