const version = process.argv[2]; if (!version) { console.error("Version not passed to create-gitea-release.js"); process.exit(1); } import fs from "fs-extra"; import path from "path"; import { spawnSync } from "child_process"; import { fileURLToPath } from "url"; import fetch from "node-fetch"; import dotenv from "dotenv"; dotenv.config({ path: "./.env" }); import { createRequire } from "node:module"; const require = createRequire(import.meta.url); const conventionalChangelog = require("conventional-changelog-conventionalcommits").default || require("conventional-changelog-conventionalcommits"); // Resolve the directory of the current script const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // Absolute path to BUILD_NUMBER const buildNumberPath = path.resolve(__dirname, "../BUILD_NUMBER"); // Load build number from BUILD_NUMBER file let buildNumber = "0"; try { const rawBuild = fs.readFileSync(buildNumberPath, "utf8"); console.log("Raw build", rawBuild); buildNumber = rawBuild.trim(); } catch (e) { console.log(e); console.warn("BUILD_NUMBER file not found, defaulting to 0"); } const fullVersion = `${version}.${buildNumber}`; const { GITEA_URL, GITEA_USERNAME, GITEA_REPO, GITEA_TOKEN } = process.env; if (!GITEA_URL || !GITEA_USERNAME || !GITEA_REPO || !GITEA_TOKEN) { console.error("Missing required environment variables"); process.exit(1); } // Step 1: Generate or update CHANGELOG.md console.log("Generating CHANGELOG.md..."); const result = spawnSync( "npx", [ "conventional-changelog", "-p", "conventionalcommits", "-i", "CHANGELOG.md", "-s", "-r", "1", // This ensures only the latest release is considered for generation ], { stdio: "inherit", shell: true } ); if (result.status !== 0) { console.error("Failed to generate changelog"); process.exit(1); } // Corrected function to get the latest changelog entry from CHANGELOG.md const getLatestChangelog = async () => { try { const changelogContent = await fs.readFile( path.resolve(__dirname, "../CHANGELOG.md"), "utf8" ); // Regex to capture the content of the latest release. // It looks for a line starting with "## [" (the start of a release heading) // and captures everything until the next "## [" or the end of the file. // The `m` flag makes `^` and `$` match start/end of lines, not just string. // The `s` flag allows `.` to match newlines. const latestEntryMatch = changelogContent.match( /^(## \[.*?\] - .*?\n[\s\S]*?)(?=^## \[|\Z)/m ); if (latestEntryMatch && latestEntryMatch[1]) { // Split the matched content into lines const lines = latestEntryMatch[1].split("\n"); // Find the index of the first non-heading line // The first line is usually the heading (e.g., "## [1.0.0] - 2024-01-01") // So we start checking from the second line (index 1). let firstContentLineIndex = -1; for (let i = 1; i < lines.length; i++) { if (lines[i].trim() !== "") { // Check for non-empty and non-whitespace lines firstContentLineIndex = i; break; } } if (firstContentLineIndex !== -1) { // Join the lines from the first content line onwards and trim any leading/trailing whitespace return lines.slice(firstContentLineIndex).join("\n").trim(); } return ""; // No content found after heading } else { console.warn( "Could not find the latest changelog entry in CHANGELOG.md. Ensure it's formatted correctly." ); return "No changelog notes available."; // Default message if not found } } catch (err) { console.error("Error reading or parsing CHANGELOG.md:", err); throw err; } }; const releaseNotes = await getLatestChangelog(); // Step 3: Create or update Gitea release const createOrUpdateRelease = async () => { const tagName = `v${version}`; const apiBase = `https://${GITEA_URL}/api/v1/repos/${GITEA_USERNAME}/${GITEA_REPO}`; const existing = await fetch(`${apiBase}/releases/tags/${tagName}`, { headers: { Authorization: `token ${GITEA_TOKEN}` }, }); let release; if (existing.ok) { const existingRelease = await existing.json(); console.log(`Release ${tagName} already exists. Updating it.`); const updateResponse = await fetch( `${apiBase}/releases/${existingRelease.id}`, { method: "PATCH", headers: { Authorization: `token ${GITEA_TOKEN}`, "Content-Type": "application/json", }, body: JSON.stringify({ name: tagName, body: releaseNotes, draft: false, prerelease: true, }), } ); if (!updateResponse.ok) { const errorText = await updateResponse.text(); throw new Error( `Failed to update release: ${updateResponse.status} - ${errorText}` ); } release = await updateResponse.json(); console.log("Release updated:", release.html_url || release.url); } else if (existing.status === 404) { const createResponse = await fetch(`${apiBase}/releases`, { method: "POST", headers: { Authorization: `token ${GITEA_TOKEN}`, "Content-Type": "application/json", }, body: JSON.stringify({ tag_name: tagName, name: `Release ${fullVersion}`, body: releaseNotes, draft: false, prerelease: true, }), }); if (!createResponse.ok) { const errorText = await createResponse.text(); throw new Error( `Failed to create release: ${createResponse.status} - ${errorText}` ); } release = await createResponse.json(); console.log("Release created:", release.html_url || release.url); } else { const errorText = await existing.text(); throw new Error( `Failed to check release: ${existing.status} - ${errorText}` ); } return release; }; const uploadAsset = async (release) => { const apiUrl = `https://${GITEA_URL}/api/v1/repos/${GITEA_USERNAME}/${GITEA_REPO}/releases/assets?tag=${release.tag_name}`; const filePath = `releases/release-${fullVersion}.zip`; if (!(await fs.pathExists(filePath))) { console.warn(`Zip file not found: ${filePath}. Skipping asset upload.`); return; } const FormData = (await import("form-data")).default; const form = new FormData(); form.append("name", `release-${fullVersion}.zip`); form.append("attachment", fs.createReadStream(filePath)); const response = await fetch(apiUrl, { method: "POST", headers: { Authorization: `token ${GITEA_TOKEN}`, ...form.getHeaders(), }, body: form, }); if (!response.ok) { const errorText = await response.text(); throw new Error( `Failed to upload asset: ${response.status} - ${errorText}` ); } const asset = await response.json(); console.log("Asset uploaded:", asset.browser_download_url || asset.url); }; // Run everything (async () => { try { const release = await createOrUpdateRelease(); // await uploadAsset(release); // fix this later and just update the readme. } catch (err) { console.error(err); process.exit(1); } })();