Skip to content

Commit 4ba6828

Browse files
committed
adding release tool
1 parent d09770d commit 4ba6828

File tree

4 files changed

+206
-2
lines changed

4 files changed

+206
-2
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ npm-debug.log
66
.DS_Store
77
.idea
88
.vscode
9+
tools/release/config.json

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "fredy",
3-
"version": "19.3.6",
3+
"version": "19.3.7",
44
"description": "[F]ind [R]eal [E]states [d]amn eas[y].",
55
"scripts": {
66
"prepare": "husky",
@@ -17,7 +17,8 @@
1717
"lint:fix": "yarn lint --fix",
1818
"migratedb": "node lib/services/storage/migrations/migrate.js",
1919
"migratedb:overwrite": "x-var MIGRATION_ALLOW_CHECKSUM_UPDATE=true node lib/services/storage/migrations/migrate.js",
20-
"copyright": "node ./copyright.js"
20+
"copyright": "node ./copyright.js",
21+
"release": "node ./tools/release/release.js"
2122
},
2223
"type": "module",
2324
"lint-staged": {
@@ -98,6 +99,7 @@
9899
"zustand": "^5.0.11"
99100
},
100101
"devDependencies": {
102+
"chalk": "^5.6.2",
101103
"@babel/core": "7.29.0",
102104
"@babel/eslint-parser": "7.28.6",
103105
"@babel/preset-env": "7.29.0",

tools/release/release.js

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
/*
2+
* Copyright (c) 2026 by Christian Kellner.
3+
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
4+
*/
5+
6+
import fs from 'fs';
7+
import path from 'path';
8+
import { execSync, spawn } from 'child_process';
9+
import fetch from 'node-fetch';
10+
import chalk from 'chalk';
11+
import { fileURLToPath } from 'url';
12+
13+
/**
14+
* Release Tool for Fredy
15+
*
16+
* This tool automates the process of creating a GitHub release.
17+
* It fetches the latest release, compares it with the current master branch,
18+
* allows manual editing of commit messages, and creates a new release on GitHub.
19+
*/
20+
21+
// Define __dirname for ESM
22+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
23+
24+
// Configuration and Paths
25+
const CONFIG_PATH = path.join(__dirname, 'config.json');
26+
const PACKAGE_JSON_PATH = path.join(__dirname, '../../package.json');
27+
const REPO = 'orangecoding/fredy';
28+
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
29+
const GITHUB_TOKEN = config.github_token;
30+
31+
/**
32+
* Main function to execute the release process
33+
*/
34+
async function createRelease() {
35+
/* eslint-disable no-console */
36+
try {
37+
console.log(chalk.cyan('🚀 Starting release process...'));
38+
39+
// 1. Load Configuration
40+
if (!fs.existsSync(CONFIG_PATH)) {
41+
console.error(chalk.red('❌ Error: config.json not found in tools/release/'));
42+
process.exit(1);
43+
}
44+
45+
if (!GITHUB_TOKEN) {
46+
console.error(chalk.red('❌ Error: GitHub token not configured.'));
47+
process.exit(1);
48+
}
49+
50+
// 2. Get current version from package.json
51+
const packageJson = JSON.parse(fs.readFileSync(PACKAGE_JSON_PATH, 'utf8'));
52+
const version = packageJson.version;
53+
const tag = version; // Using version as tag
54+
55+
console.log(chalk.blue(`📦 Target version: ${version}`));
56+
57+
// 3. Check if release already exists
58+
console.log(chalk.yellow('🔍 Checking if release already exists...'));
59+
const existingReleaseResponse = await fetch(`https://api.github.com/repos/${REPO}/releases/tags/${tag}`, {
60+
headers: {
61+
Authorization: `token ${GITHUB_TOKEN}`,
62+
Accept: 'application/vnd.github.v3+json',
63+
},
64+
});
65+
66+
if (existingReleaseResponse.status === 200) {
67+
console.error(chalk.red(`❌ Error: A release with tag ${tag} already exists.`));
68+
process.exit(1);
69+
}
70+
71+
// 4. Fetch latest release to find the starting point for the diff
72+
console.log(chalk.yellow('📡 Fetching latest release from GitHub...'));
73+
const latestReleaseResponse = await fetch(`https://api.github.com/repos/${REPO}/releases/latest`, {
74+
headers: {
75+
Authorization: `token ${GITHUB_TOKEN}`,
76+
Accept: 'application/vnd.github.v3+json',
77+
},
78+
});
79+
80+
if (!latestReleaseResponse.ok) {
81+
console.error(chalk.red('❌ Error fetching latest release.'));
82+
const errorData = await latestReleaseResponse.json();
83+
console.error(chalk.red(JSON.stringify(errorData)));
84+
process.exit(1);
85+
}
86+
87+
const latestRelease = await latestReleaseResponse.json();
88+
const latestTag = latestRelease.tag_name;
89+
console.log(chalk.green(`✅ Latest release found: ${latestTag}`));
90+
91+
// 5. Ensure the latest tag is available locally
92+
console.log(chalk.yellow(`📡 Fetching tag ${latestTag} from remote...`));
93+
try {
94+
execSync(`git fetch origin tag ${latestTag} --no-tags`);
95+
} catch (error) {
96+
console.error(chalk.red(`❌ Error fetching tag ${latestTag} from origin.`));
97+
console.error(error.message);
98+
// We don't exit here, maybe it's already there but fetch failed for some reason
99+
}
100+
101+
// 6. Get commit messages between latest tag and current HEAD
102+
console.log(chalk.yellow(`Git diff: ${latestTag} .. HEAD`));
103+
let commitMessages;
104+
try {
105+
commitMessages = execSync(`git log ${latestTag}..HEAD --pretty=format:"- %s"`).toString().trim();
106+
} catch (error) {
107+
console.error(chalk.red('❌ Error running git log. Make sure the latest tag is available locally.'), error);
108+
process.exit(1);
109+
}
110+
111+
if (!commitMessages) {
112+
console.log(chalk.magenta('⚠️ No new commits found since last release.'));
113+
commitMessages = '- No changes recorded';
114+
}
115+
116+
// 7. Open commit messages in editor for manual adjustment
117+
const tempFilePath = path.join(__dirname, 'CHANGELOG_EDIT.tmp');
118+
const initialContent = `# Release Notes for ${version}\n# Edit the messages below. Lines starting with # will be ignored.\n\n${commitMessages}`;
119+
fs.writeFileSync(tempFilePath, initialContent);
120+
121+
console.log(chalk.blue('📝 Opening editor for release notes (using nano or $EDITOR)...'));
122+
await openInEditor(tempFilePath);
123+
124+
// 8. Read edited content
125+
let editedContent = fs
126+
.readFileSync(tempFilePath, 'utf8')
127+
.split('\n')
128+
.filter((line) => !line.startsWith('#'))
129+
.join('\n')
130+
.trim();
131+
132+
fs.unlinkSync(tempFilePath); // Clean up temp file
133+
134+
if (!editedContent) {
135+
console.error(chalk.red('❌ Release notes are empty. Aborting release.'));
136+
process.exit(1);
137+
}
138+
139+
// 9. Create the new release
140+
console.log(chalk.cyan(`🚀 Creating release ${version} on GitHub...`));
141+
const createResponse = await fetch(`https://api.github.com/repos/${REPO}/releases`, {
142+
method: 'POST',
143+
headers: {
144+
Authorization: `token ${GITHUB_TOKEN}`,
145+
Accept: 'application/vnd.github.v3+json',
146+
'Content-Type': 'application/json',
147+
},
148+
body: JSON.stringify({
149+
tag_name: tag,
150+
name: version,
151+
body: editedContent,
152+
draft: false,
153+
prerelease: false,
154+
}),
155+
});
156+
157+
if (createResponse.status === 201) {
158+
const data = await createResponse.json();
159+
console.log(chalk.green('🎉 Release successfully created!'));
160+
console.log(chalk.green(`🔗 URL: ${data.html_url}`));
161+
} else {
162+
const errorData = await createResponse.json();
163+
console.error(chalk.red('❌ Failed to create release.'));
164+
console.error(chalk.red(JSON.stringify(errorData, null, 2)));
165+
process.exit(1);
166+
}
167+
} catch (error) {
168+
console.error(chalk.red('💥 An unexpected error occurred:'));
169+
console.error(error);
170+
process.exit(1);
171+
}
172+
}
173+
174+
/**
175+
* Helper to open a file in a terminal editor
176+
* @param {string} filePath
177+
*/
178+
function openInEditor(filePath) {
179+
return new Promise((resolve, reject) => {
180+
const editor = process.env.EDITOR || 'nano';
181+
const child = spawn(editor, [filePath], {
182+
stdio: 'inherit',
183+
});
184+
185+
child.on('exit', (code) => {
186+
if (code === 0) {
187+
resolve();
188+
} else {
189+
reject(new Error(`Editor exited with code ${code}`));
190+
}
191+
});
192+
});
193+
}
194+
195+
await createRelease();
196+
/* eslint-enable no-console */

yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2401,6 +2401,11 @@ chalk@^4.0.0, chalk@^4.1.0:
24012401
ansi-styles "^4.1.0"
24022402
supports-color "^7.1.0"
24032403

2404+
chalk@^5.6.2:
2405+
version "5.6.2"
2406+
resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.6.2.tgz#b1238b6e23ea337af71c7f8a295db5af0c158aea"
2407+
integrity sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==
2408+
24042409
character-entities-html4@^2.0.0:
24052410
version "2.1.0"
24062411
resolved "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz"

0 commit comments

Comments
 (0)