UNPKG

@gitlab/eslint-plugin

Version:

GitLab package for our custom eslint rules

251 lines (199 loc) 7.36 kB
#!/usr/bin/env node import { readdirSync, readFileSync, writeFileSync } from 'node:fs'; import { join, relative } from 'node:path'; import { styleText } from 'node:util'; import defaultChangelogFunctions from '@changesets/cli/changelog'; import { ROOT, printDiagnostics, run } from './shared.mjs'; const { env } = process; const CHANGESET_DIR = join(ROOT, '.changeset'); const CHANGESET_BIN = join(ROOT, 'node_modules', '.bin', 'changeset'); // Changesets fails if this is an absolute path. const CHANGESET_RELEASE_PLAN_FILE = relative( process.cwd(), join(CHANGESET_DIR, 'release_plan.json'), ); function isEnvironmentOkay() { const messages = []; if (!env.CI) { messages.push('This script should only be run in CI.'); } if (env.DRY_RUN) { if (!env.CI_MERGE_REQUEST_IID) { messages.push('This script should only run in merge request pipelines.'); } if (!env.GITLAB_TOKEN_MR) { messages.push('GITLAB_TOKEN_MR is not defined.'); } } else { if (!env.CI_COMMIT_BRANCH) { messages.push('This script should only run in branch pipelines.'); } if (env.CI_COMMIT_BRANCH !== env.CI_DEFAULT_BRANCH) { messages.push( `This script should only run on pipelines for the default branch: CI_COMMIT_BRANCH=${env.CI_COMMIT_BRANCH}, CI_DEFAULT_BRANCH=${env.CI_DEFAULT_BRANCH}`, ); } if (!env.GITLAB_TOKEN) { messages.push('GITLAB_TOKEN is not defined.'); } if (!env.NPM_ID_TOKEN) { messages.push( 'NPM_ID_TOKEN is not defined. Is trusted publishing set up correctly? See https://docs.npmjs.com/trusted-publishers', ); } } messages.forEach((message) => { console.warn(message); }); return messages.length === 0; } function gitUrl() { const token = env.GITLAB_TOKEN || env.GITLAB_TOKEN_MR; const projectUrl = env.CI_MERGE_REQUEST_SOURCE_PROJECT_PATH || env.CI_PROJECT_PATH; return `https://gitlab-bot:${token}@gitlab.com/${projectUrl}.git`; } /** * Changesets needs git to be checked out on the branch, not on a detached * commit sha. In other words, HEAD must point to the branch name, not its * commit sha. * * For dry runs, there also needs to exist a local branch for the default * branch so that it can compare the source branch against it. */ function ensureBranches() { run('git', ['remote', 'set-url', 'origin', gitUrl()]); if (env.DRY_RUN) { const branch = env.CI_MERGE_REQUEST_SOURCE_BRANCH_NAME; run('git', ['fetch', 'origin', env.CI_DEFAULT_BRANCH, branch]); run('git', ['branch', env.CI_DEFAULT_BRANCH, `origin/${env.CI_DEFAULT_BRANCH}`]); run('git', ['checkout', '-b', branch, `origin/${branch}`]); } else { run('git', ['fetch', 'origin', env.CI_DEFAULT_BRANCH]); run('git', ['checkout', '-b', env.CI_DEFAULT_BRANCH, `origin/${env.CI_DEFAULT_BRANCH}`]); } } function getReleasePlan() { const changesetFiles = readdirSync(CHANGESET_DIR).filter( (name) => name.endsWith('.md') && name !== 'README.md', ); if (changesetFiles.length > 0) { console.log(`Found changeset files:\n${changesetFiles.join('\n')}`); } else { // Create an empty release plan file anyway, to avoid a spurious warning in // job log about missing artifacts. writeFileSync(CHANGESET_RELEASE_PLAN_FILE, ''); console.log('No changesets found.'); return null; } // We know there are changeset files, so we expect this command to succeed. run(CHANGESET_BIN, ['status', `--output=${CHANGESET_RELEASE_PLAN_FILE}`]); const releasePlan = JSON.parse(readFileSync(CHANGESET_RELEASE_PLAN_FILE, 'utf8')); console.log('Changesets release plan:', releasePlan); return releasePlan; } /** * Pretty-print the release plan JSON. * @param {object} releasePlan The release plan. */ function printReleasePlan({ releases }) { const releaseLines = releases.map( ({ name, type, oldVersion, newVersion }) => `${styleText('bold', name)} ${styleText('yellow', oldVersion)}${styleText( 'green', newVersion, )} (${type})`, ); console.log(releaseLines.join('\n')); } function releaseSection(type, lines) { const typeCapitalized = `${type.charAt(0).toUpperCase()}${type.slice(1)}`; return `### ${typeCapitalized} changes\n\n${lines.join('\n')}`; } async function createRelease({ tag, description }) { const url = `${env.CI_API_V4_URL}/projects/${env.CI_PROJECT_ID}/releases`; console.log(`Creating release via ${url}...`); const response = await fetch(url, { method: 'POST', headers: { 'content-type': 'application/json', 'private-token': env.GITLAB_TOKEN }, body: JSON.stringify({ tag_name: tag, description }), }); if (!response.ok) { const responseBody = JSON.stringify(await response.json(), null, 2); throw new Error( `Failed to create release for tag ${tag}: ${response.status}: ${response.statusText}, body: ${responseBody}`, ); } console.log(`Successfully created release at ${url}/${encodeURIComponent(tag)}`); } async function createReleases({ changesets, releases }) { for (const release of releases) { const releaseDescriptionObject = { major: [], minor: [], patch: [], }; for (const changeset of changesets) { const { type } = changeset.releases.find((rel) => rel.name === release.name && rel.type !== 'none') ?? {}; if (type) { releaseDescriptionObject[type].push( // eslint-disable-next-line no-await-in-loop await defaultChangelogFunctions.getReleaseLine(changeset, type), ); } } const description = Object.entries(releaseDescriptionObject) .filter(([, lines]) => lines.length > 0) .map(([type, lines]) => releaseSection(type, lines)) .join('\n\n'); // eslint-disable-next-line no-await-in-loop await createRelease({ tag: `v${release.newVersion}`, description }); } } function gitCommit({ message = 'Update packages for release [skip ci]' } = {}) { run('git', ['config', '--global', 'user.email', 'gitlab-bot@gitlab.com']); run('git', ['config', '--global', 'user.name', 'GitLab Bot']); // Stage modified and deleted files only. run('git', ['add', '--update']); run('git', ['status']); run('git', ['commit', '-m', message]); } function gitPush() { const refspec = `HEAD:${env.CI_COMMIT_BRANCH}`; run('git', ['push', '--follow-tags', 'origin', refspec]); } function publish() { run(CHANGESET_BIN, ['publish']); } async function main() { if (!isEnvironmentOkay()) { process.exitCode = 1; return; } ensureBranches(); const releasePlan = getReleasePlan(); if (!releasePlan || releasePlan.releases.length === 0) { console.log('Nothing to publish.'); return; } // Process changeset files and update package changelogs run(CHANGESET_BIN, ['version']); if (env.DRY_RUN) { console.log('Diff of changes that would be made if this were on the default branch:'); run('git', ['diff', '--color=always']); console.log('The following packages would be released:'); printReleasePlan(releasePlan); return; } gitCommit(); publish(); gitPush(); await createReleases(releasePlan); } try { await main(); } catch (error) { process.exitCode = 1; printDiagnostics(); console.error('Unhandled error (see above for diagnostics):', error); }