UNPKG

electron-publish

Version:

Part of [electron-builder](https://github.com/electron-userland/electron-builder).

290 lines 13.1 kB
"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