Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save ArtDepartmentMJ/04da069c9df36a6db8e81a203f8460c5 to your computer and use it in GitHub Desktop.

Select an option

Save ArtDepartmentMJ/04da069c9df36a6db8e81a203f8460c5 to your computer and use it in GitHub Desktop.
release script
const VERSION = '1.0.5';
import { execSync } from 'child_process';
import { readFileSync } from 'fs';
import { resolve } from 'path';
import * as readline from 'readline/promises';
import { stdin as input, stdout as output } from 'process';
// Load .env from repo root
try {
const envPath = resolve(execSync('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim(), '.env');
const lines = readFileSync(envPath, 'utf8').split('\n');
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
const eq = trimmed.indexOf('=');
if (eq === -1) continue;
const key = trimmed.slice(0, eq).trim();
const val = trimmed.slice(eq + 1).trim().replace(/^(['"])(.*)\1$/, '$2');
if (!(key in process.env)) process.env[key] = val;
}
} catch {
// No .env file
}
// Parse CLI args: --message "..." --tag v1.0.5 --no-tag --push --no-push --deploy --no-deploy
const args = process.argv.slice(2);
const cli = {};
for (let i = 0; i < args.length; i++) {
if (args[i] === '--message' || args[i] === '-m') cli.message = args[++i];
else if (args[i] === '--tag' || args[i] === '-t') cli.tag = args[++i];
else if (args[i] === '--no-tag') cli.noTag = true;
else if (args[i] === '--push') cli.push = true;
else if (args[i] === '--no-push') cli.push = false;
else if (args[i] === '--deploy') cli.deploy = true;
else if (args[i] === '--no-deploy') cli.deploy = false;
}
const nonInteractive = Object.keys(cli).length > 0;
const rl = readline.createInterface({ input, output });
async function ask(question) {
const answer = await rl.question(question);
return answer.trim();
}
function run(cmd, opts = {}) {
return execSync(cmd, { stdio: opts.silent ? 'pipe' : 'inherit', encoding: 'utf8' });
}
function runCapture(cmd) {
return execSync(cmd, { stdio: 'pipe', encoding: 'utf8' }).trim();
}
// Get current branch
const branch = runCapture('git branch --show-current');
// Get latest tag and suggest next patch version
let latestTag = '';
let suggestedTag = '';
try {
latestTag = runCapture('git tag --sort=-v:refname | head -1');
const match = latestTag.match(/^(v?)(\d+)\.(\d+)\.(\d+)$/);
if (match) {
const [, prefix, major, minor, patch] = match;
suggestedTag = `${prefix}${major}.${minor}.${parseInt(patch) + 1}`;
} else {
suggestedTag = latestTag;
}
} catch {
// No tags yet
}
console.log(`\nBranch: ${branch}`);
console.log(`Latest tag: ${latestTag || '(none)'}`);
console.log('\n→ Building...');
run('npm run build');
console.log('\n→ Staging all files...');
run('git add -A');
const staged = runCapture('git diff --cached --name-only');
const hasChanges = !!staged;
if (!hasChanges) {
console.log('\nNothing to commit.');
}
// Commit message (only if there's something to commit)
let message = cli.message ?? '';
if (hasChanges && !message) {
message = await ask('\nCommit message: ');
}
if (hasChanges && !message.trim()) {
console.error('Commit message is required.');
rl.close();
process.exit(1);
}
// Tag
let finalTag = null;
if (!cli.noTag && cli.tag === undefined) {
const tagYesNo = await ask('\nCreate a tag? [Y/n]: ');
if (tagYesNo.toLowerCase() !== 'n') {
const tagInput = await ask(suggestedTag ? `Tag [${suggestedTag}]: ` : 'Tag: ');
finalTag = tagInput || suggestedTag || null;
}
} else if (!cli.noTag && cli.tag) {
finalTag = cli.tag;
}
// Push
let shouldPush;
if (cli.push !== undefined) {
shouldPush = cli.push;
} else {
const pushYesNo = await ask('\nPush to remote? [Y/n]: ');
shouldPush = pushYesNo.toLowerCase() !== 'n';
}
// Deploy
const envoyer = process.env.ENVOYER_HOOK;
const forgeToken = process.env.FORGE_API_TOKEN;
const forgeServer = process.env.FORGE_SERVER_ID;
const forgeSite = process.env.FORGE_SITE_ID;
const canDeploy = shouldPush && (envoyer || (forgeToken && forgeServer && forgeSite));
let shouldDeploy = false;
if (canDeploy) {
if (cli.deploy !== undefined) {
shouldDeploy = cli.deploy;
} else {
const deployYesNo = await ask('\nTrigger deployment? [Y/n]: ');
shouldDeploy = deployYesNo.toLowerCase() !== 'n';
}
}
rl.close();
if (hasChanges) {
console.log(`\n→ Committing: "${message}"`);
run(`git commit -m ${JSON.stringify(message)}`);
}
if (finalTag) {
console.log(`\n→ Tagging: ${finalTag}`);
run(`git tag ${finalTag}`);
}
if (shouldPush) {
console.log(`\n→ Pushing branch "${branch}"...`);
try {
run(`git push origin ${branch}`);
} catch {
run(`git push --set-upstream origin ${branch}`);
}
if (finalTag) {
console.log('\n→ Pushing tags...');
run('git push --tags');
}
}
if (shouldDeploy) {
if (envoyer) {
console.log('\n→ Triggering deployment via Envoyer...');
await fetch(`${envoyer}?branch=${encodeURIComponent(branch)}`, { method: 'POST' });
console.log(' Done.');
} else {
console.log('\n→ Triggering deployment via Forge...');
const res = await fetch(`https://forge.laravel.com/api/v1/servers/${forgeServer}/sites/${forgeSite}/deployment/deploy`, {
method: 'POST',
headers: { Authorization: `Bearer ${forgeToken}`, Accept: 'application/json' },
});
if (!res.ok) {
const body = await res.text();
console.error(` Forge API error ${res.status}: ${body}`);
} else {
console.log(' Done.');
}
}
}
console.log(`\nDone.${finalTag ? ` Released ${finalTag}` : ''}`);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment