electron-publish
Version:
Part of [electron-builder](https://github.com/electron-userland/electron-builder).
290 lines • 13.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.GitlabPublisher = void 0;
const builder_util_1 = require("builder-util");
const fs_1 = require("fs");
const promises_1 = require("fs/promises");
const promises_2 = require("fs/promises");
const builder_util_runtime_1 = require("builder-util-runtime");
const lazy_val_1 = require("lazy-val");
const mime = require("mime");
const FormData = require("form-data");
const url_1 = require("url");
const httpPublisher_1 = require("./httpPublisher");
class GitlabPublisher extends httpPublisher_1.HttpPublisher {
constructor(context, info, version) {
super(context, true);
this.info = info;
this.version = version;
this._release = new lazy_val_1.Lazy(() => (this.token === "__test__" ? Promise.resolve(null) : this.getOrCreateRelease()));
this.providerName = "gitlab";
this.releaseLogFields = null;
let token = info.token || null;
if ((0, builder_util_1.isEmptyOrSpaces)(token)) {
token = process.env.GITLAB_TOKEN || null;
if ((0, builder_util_1.isEmptyOrSpaces)(token)) {
throw new builder_util_1.InvalidConfigurationError(`GitLab Personal Access Token is not set, neither programmatically, nor using env "GITLAB_TOKEN"`);
}
token = token.trim();
if (!(0, builder_util_1.isTokenCharValid)(token)) {
throw new builder_util_1.InvalidConfigurationError(`GitLab Personal Access Token (${JSON.stringify(token)}) contains invalid characters, please check env "GITLAB_TOKEN"`);
}
}
this.token = token;
this.host = info.host || "gitlab.com";
this.projectId = this.resolveProjectId();
this.baseApiPath = `https://${this.host}/api/v4`;
if (version.startsWith("v")) {
throw new builder_util_1.InvalidConfigurationError(`Version must not start with "v": ${version}`);
}
// By default, we prefix the version with "v"
this.tag = info.vPrefixedTagName === false ? version : `v${version}`;
}
async getOrCreateRelease() {
const logFields = {
tag: this.tag,
version: this.version,
};
try {
const existingRelease = await this.getExistingRelease();
if (existingRelease) {
return existingRelease;
}
// Create new release if it doesn't exist
return this.createRelease();
}
catch (error) {
const errorInfo = this.categorizeGitlabError(error);
builder_util_1.log.error({
...logFields,
error: error.message,
errorType: errorInfo.type,
statusCode: errorInfo.statusCode,
}, "Failed to get or create GitLab release");
throw error;
}
}
async getExistingRelease() {
const url = this.buildProjectUrl("/releases");
const releases = await this.gitlabRequest(url);
for (const release of releases) {
if (release.tag_name === this.tag) {
return release;
}
}
return null;
}
async getDefaultBranch() {
try {
const url = this.buildProjectUrl();
const project = await this.gitlabRequest(url);
return project.default_branch || "main";
}
catch (error) {
builder_util_1.log.warn({ error: error.message }, "Failed to get default branch, using 'main' as fallback");
return "main";
}
}
async createRelease() {
const releaseName = this.info.vPrefixedTagName === false ? this.version : `v${this.version}`;
const branchName = await this.getDefaultBranch();
const releaseData = {
tag_name: this.tag,
name: releaseName,
description: `Release ${releaseName}`,
ref: branchName,
};
builder_util_1.log.debug({
tag: this.tag,
name: releaseName,
ref: branchName,
projectId: this.projectId,
}, "creating GitLab release");
const url = this.buildProjectUrl("/releases");
return this.gitlabRequest(url, releaseData, "POST");
}
async doUpload(fileName, arch, dataLength, requestProcessor, filePath) {
const release = await this._release.value;
if (release == null) {
builder_util_1.log.warn({ file: fileName, ...this.releaseLogFields }, "skipped publishing");
return;
}
const logFields = {
file: fileName,
arch: builder_util_1.Arch[arch],
size: dataLength,
uploadTarget: this.info.uploadTarget || "project_upload",
};
try {
builder_util_1.log.debug(logFields, "starting GitLab upload");
const assetPath = await this.uploadFileAndReturnAssetPath(fileName, dataLength, requestProcessor, filePath);
// Add the uploaded file as a release asset link
if (assetPath) {
await this.addReleaseAssetLink(fileName, assetPath);
builder_util_1.log.info({ ...logFields, assetPath }, "GitLab upload completed successfully");
}
else {
builder_util_1.log.warn({ ...logFields }, "No asset URL found for file");
}
return assetPath;
}
catch (e) {
const errorInfo = this.categorizeGitlabError(e);
builder_util_1.log.error({
...logFields,
error: e.message,
errorType: errorInfo.type,
statusCode: errorInfo.statusCode,
}, "GitLab upload failed");
throw e;
}
}
async uploadFileAndReturnAssetPath(fileName, dataLength, requestProcessor, filePath) {
// Default to project_upload method
const uploadTarget = this.info.uploadTarget || "project_upload";
let assetPath;
if (uploadTarget === "generic_package") {
await this.uploadToGenericPackages(fileName, dataLength, requestProcessor);
// For generic packages, construct the download URL
const projectId = encodeURIComponent(this.projectId);
assetPath = `${this.baseApiPath}/projects/${projectId}/packages/generic/releases/${this.version}/${fileName}`;
}
else {
// Default to project_upload
const uploadResult = await this.uploadToProjectUpload(fileName, filePath);
// For project uploads, construct full URL from relative path
assetPath = `https://${this.host}${uploadResult.full_path}`;
}
return assetPath;
}
async addReleaseAssetLink(fileName, assetUrl) {
try {
const linkData = {
name: fileName,
url: assetUrl,
link_type: "other",
};
const url = this.buildProjectUrl(`/releases/${this.tag}/assets/links`);
await this.gitlabRequest(url, linkData, "POST");
builder_util_1.log.debug({ fileName, assetUrl }, "Successfully linked asset to GitLab release");
}
catch (e) {
builder_util_1.log.warn({ fileName, assetUrl, error: e.message }, "Failed to link asset to GitLab release");
// Don't throw - the file was uploaded successfully, linking is optional
}
}
async uploadToProjectUpload(fileName, filePath) {
const uploadUrl = `${this.baseApiPath}/projects/${encodeURIComponent(this.projectId)}/uploads`;
const parsedUrl = new url_1.URL(uploadUrl);
// Check file size to determine upload method
const stats = await (0, promises_1.stat)(filePath);
const fileSize = stats.size;
const STREAMING_THRESHOLD = 50 * 1024 * 1024; // 50MB
const form = new FormData();
if (fileSize > STREAMING_THRESHOLD) {
// Use streaming for large files
builder_util_1.log.debug({ fileName, fileSize }, "using streaming upload for large file");
const fileStream = (0, fs_1.createReadStream)(filePath);
form.append("file", fileStream, fileName);
}
else {
// Use buffer for small files
builder_util_1.log.debug({ fileName, fileSize }, "using buffer upload for small file");
const fileContent = await (0, promises_2.readFile)(filePath);
form.append("file", fileContent, fileName);
}
const response = await builder_util_1.httpExecutor.doApiRequest((0, builder_util_runtime_1.configureRequestOptions)({
protocol: parsedUrl.protocol,
hostname: parsedUrl.hostname,
port: parsedUrl.port,
path: parsedUrl.pathname,
headers: { ...form.getHeaders(), ...this.setAuthHeaderForToken(this.token) },
timeout: this.info.timeout || undefined,
}, null, "POST"), this.context.cancellationToken, (it) => form.pipe(it));
// Parse the JSON response string
return JSON.parse(response);
}
async uploadToGenericPackages(fileName, dataLength, requestProcessor) {
const uploadUrl = `${this.baseApiPath}/projects/${encodeURIComponent(this.projectId)}/packages/generic/releases/${this.version}/${fileName}`;
const parsedUrl = new url_1.URL(uploadUrl);
return builder_util_1.httpExecutor.doApiRequest((0, builder_util_runtime_1.configureRequestOptions)({
protocol: parsedUrl.protocol,
hostname: parsedUrl.hostname,
port: parsedUrl.port,
path: parsedUrl.pathname,
headers: { "Content-Length": dataLength, "Content-Type": mime.getType(fileName) || "application/octet-stream", ...this.setAuthHeaderForToken(this.token) },
timeout: this.info.timeout || undefined,
}, null, "PUT"), this.context.cancellationToken, requestProcessor);
}
buildProjectUrl(path = "") {
return new url_1.URL(`${this.baseApiPath}/projects/${encodeURIComponent(this.projectId)}${path}`);
}
resolveProjectId() {
if (this.info.projectId) {
return String(this.info.projectId);
}
throw new builder_util_1.InvalidConfigurationError("GitLab project ID is not specified, please set it in configuration.");
}
gitlabRequest(url, data = null, method = "GET") {
return (0, builder_util_runtime_1.parseJson)(builder_util_1.httpExecutor.request((0, builder_util_runtime_1.configureRequestOptions)({
port: url.port,
path: url.pathname,
protocol: url.protocol,
hostname: url.hostname,
headers: { "Content-Type": "application/json", ...this.setAuthHeaderForToken(this.token) },
timeout: this.info.timeout || undefined,
}, null, method), this.context.cancellationToken, data));
}
setAuthHeaderForToken(token) {
const headers = {};
if (token != null) {
// If the token starts with "Bearer", it is an OAuth application secret
// Note that the original gitlab token would not start with "Bearer"
// it might start with "gloas-", if so user needs to add "Bearer " prefix to the token
if (token.startsWith("Bearer")) {
headers.authorization = token;
}
else {
headers["PRIVATE-TOKEN"] = token;
}
}
return headers;
}
categorizeGitlabError(error) {
if (error instanceof builder_util_runtime_1.HttpError) {
const statusCode = error.statusCode;
switch (statusCode) {
case 401:
return { type: "authentication", statusCode };
case 403:
return { type: "authorization", statusCode };
case 404:
return { type: "not_found", statusCode };
case 409:
return { type: "conflict", statusCode };
case 413:
return { type: "file_too_large", statusCode };
case 422:
return { type: "validation_error", statusCode };
case 429:
return { type: "rate_limit", statusCode };
case 500:
case 502:
case 503:
case 504:
return { type: "server_error", statusCode };
default:
return { type: "http_error", statusCode };
}
}
if (error.code === "ECONNRESET" || error.code === "ENOTFOUND" || error.code === "ETIMEDOUT") {
return { type: "network_error" };
}
return { type: "unknown_error" };
}
toString() {
return `GitLab (project: ${this.projectId}, version: ${this.version})`;
}
}
exports.GitlabPublisher = GitlabPublisher;
//# sourceMappingURL=gitlabPublisher.js.map