Skip to content

Commit

Permalink
feat(nx-set-shas): rewrited using clipanion package
Browse files Browse the repository at this point in the history
- GitLab: can be used with private token or job token

BREAKING CHANGE: Undice was removed in favor of native fetch, Node 18.12+ required
  • Loading branch information
gperdomor committed Apr 22, 2024
1 parent 211fe86 commit ea92e42
Show file tree
Hide file tree
Showing 14 changed files with 383 additions and 253 deletions.
5 changes: 2 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"@swc/helpers": "0.5.3",
"chalk": "4.1.2",
"ci-info": "4.0.0",
"commander": "11.1.0",
"clipanion": "4.0.0-rc.3",
"csv-parse": "5.5.5",
"handlebars": "4.7.8",
"moment": "2.30.1",
Expand All @@ -31,8 +31,7 @@
"properties-file": "3.5.4",
"semver": "7.6.0",
"tmp": "0.2.3",
"tslib": "2.6.2",
"undici": "5.28.4"
"tslib": "2.6.2"
},
"devDependencies": {
"@commitlint/cli": "19.2.2",
Expand Down
2 changes: 1 addition & 1 deletion packages/nx-set-shas/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"extends": ["../../.eslintrc.json"],
"extends": ["../../.eslintrc.base.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
Expand Down
12 changes: 6 additions & 6 deletions packages/nx-set-shas/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,18 @@
"name": "@nx-tools/nx-set-shas",
"version": "6.0.0-alpha.2",
"type": "commonjs",
"main": "./src/index.js",
"typings": "./src/index.d.ts",
"bin": {
"nx-set-shas": "./src/index.js"
},
"main": "./src/index.js",
"typings": "./src/index.d.ts",
"dependencies": {
"@nx-tools/core": "6.0.0-alpha.2",
"chalk": "4.1.2",
"commander": "11.1.0",
"undici": "5.28.4"
"clipanion": "^4.0.0-rc.3",
"chalk": "^4.1.2",
"semver": "^7.6.0"
},
"peerDependencies": {
"tslib": "^2.5.3"
"tslib": "^2.5.0"
}
}
26 changes: 12 additions & 14 deletions packages/nx-set-shas/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "packages/nx-set-shas/src",
"projectType": "library",
"release": {
"version": {
"generatorOptions": {
"packageRoot": "dist/{projectRoot}",
"currentVersionResolver": "git-tag"
}
}
},
"tags": ["type:cli", "scope:nx-set-shas"],
"targets": {
"build": {
"executor": "@nx/js:tsc",
Expand All @@ -14,21 +23,10 @@
"assets": ["packages/nx-set-shas/*.md"]
}
},
"publish": {
"command": "node tools/scripts/publish.mjs nx-set-shas {args.ver} {args.tag}",
"dependsOn": ["build"]
},
"lint": {
"executor": "@nx/eslint:lint",
"outputs": ["{options.outputFile}"]
},
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"nx-release-publish": {
"options": {
"jestConfig": "packages/nx-set-shas/jest.config.ts"
"packageRoot": "dist/{projectRoot}"
}
}
},
"tags": ["type:lib"]
}
}
28 changes: 19 additions & 9 deletions packages/nx-set-shas/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
#!/usr/bin/env node
import { Command } from 'commander';
import { logger } from '@nx-tools/core';
import { Builtins, Cli } from 'clipanion';
import { version } from '../package.json';
import { gitlabCommand } from './lib/gitlab/command';
import { GitLabCommand } from './lib/gitlab/command';
import { Context } from './lib/types';

const program = new Command();
const cli = new Cli<Context>({
binaryName: `nx-set-shas`,
binaryLabel: `Nx set SHAs`,
binaryVersion: version,
});

program
.name('nx-set-shas')
.description('Sets the base and head SHAs required for `nx affected` commands in CI')
.addCommand(gitlabCommand())
.version(version, '-v, --version', 'Output the current version');
cli.register(Builtins.DefinitionsCommand);
cli.register(Builtins.HelpCommand);
cli.register(Builtins.TokensCommand);
cli.register(Builtins.VersionCommand);

program.parse(process.argv);
cli.register(GitLabCommand);

cli.runExit(process.argv.slice(2), {
cwd: process.cwd(),
logger: logger,
});
7 changes: 7 additions & 0 deletions packages/nx-set-shas/src/lib/gitlab/command.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { GitLabCommand } from './command';

describe('nxSetShas', () => {
it('should work', () => {
expect(GitLabCommand.paths).toEqual([['gitlab']]);
});
});
214 changes: 173 additions & 41 deletions packages/nx-set-shas/src/lib/gitlab/command.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,178 @@
import { logger } from '@nx-tools/core';
import { Command } from 'commander';
import { existsSync } from 'node:fs';
import { printVerboseHook } from '../utils/debug-utils';
import { findLatestCommit } from './find-successful-pipeline';

export const defaultWorkingDirectory = '.';

export const gitlabCommand = () => {
const command = new Command('gitlab');

command
.description('Find latest successful pipeline of gitlab project')
.option('-t, --token <token>', 'Authentication token')
.option('-o, --output <output>')
.option('-d, --working-dir <directory>', 'The directory where your repository is located', defaultWorkingDirectory)
.option('--verbose', 'output debug logs', false)
.option(
'--error-on-no-successful-pipeline <error>',
'By default, if no successful pipeline is found on the main branch to determine the SHA, we will log a warning and use HEAD~1. Enable this option to error and exit instead.',
false
)
.requiredOption(
'-b, --branch <branch>',
'The name of the main branch in your repo, used as the target of PRs. E.g. main, master etc',
process.env['CI_DEFAULT_BRANCH']
)
.requiredOption('-p, --project <project>', 'The ID of the project.', process.env['CI_PROJECT_ID'])
.hook('preAction', printVerboseHook)
.action(async (options) => {
const { branch, project, token, errorOnNoSuccessfulPipeline, output, workingDir } = options;

if (workingDir !== defaultWorkingDirectory) {
if (existsSync(workingDir)) {
process.chdir(workingDir);
import * as chalk from 'chalk';
import { Command, Option } from 'clipanion';
import { spawnSync } from 'node:child_process';
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
import { stripNewLineEndings, validateNodejsVersion } from '../helpers';
import { Context } from '../types';
import { findSuccessfulCommit } from './gitlab.helpers';

const defaultWorkingDirectory = '.';

export class GitLabCommand extends Command<Context> {
static override paths = [[`gitlab`]];

static override usage = Command.Usage({
category: `GitLab commands`,
description: `Find the base and head SHAs required for the nx affected commands in GitLab CI.`,
// details: `
// A longer description of the command with some \`markdown code\`.

// Multiple paragraphs are allowed. Clipanion will take care of both reindenting the content and wrapping the paragraphs as needed.
// `,
examples: [
[`A basic example`, `$0 gitlab --project 40806764 --branch main`],
[`With an private token`, `$0 gitlab --project 40806764 --branch main --token glpat-xxxxxxxx`],
[`With custom env var output`, `$0 gitlab --project 40806764 --branch main --output .env`],
],
});

mainBranchName = Option.String(`--branch,-b`, 'main', {
description: 'The name of the main branch in your repo, used as the target of PRs. E.g. main, master etc.',
env: 'CI_DEFAULT_BRANCH',
});

errorOnNoSuccessfulPipeline = Option.Boolean('--error-on-no-successful-pipeline', false, {
description:
'By default, if no successful workflow is found on the main branch to determine the SHA, we will log a warning and use HEAD~1. Enable this option to error and exit instead.',
});

lastSuccessfulEvent = Option.String(`--last-successful-event`, 'push', {
description:
'The type of event to check for the last successful commit corresponding to that workflow-id, e.g. push, pull_request, release etc.',
});

workingDirectory = Option.String('--working-dir,-d', defaultWorkingDirectory, {
description: 'The directory where your repository is located.',
});

project = Option.String('--project,-p', {
description: 'The ID of the GitLab project.',
env: 'CI_PROJECT_ID',
required: true,
});

token = Option.String('--token,-t', {
description: 'GitLab API authentication token. If is not provided, the CI Job token will be used.',
});

output = Option.String('--output,-o', {
description: 'Output file where the env variables will be setted.',
});

reportFailure = () => {
this.context.logger.error(`
Unable to find a successful workflow run on 'origin/${this.mainBranchName}'
NOTE: You have set 'error-on-no-successful-workflow' on the action so this is a hard error.
Is it possible that you have no runs currently on 'origin/${this.mainBranchName}'?
- If yes, then you should run the workflow without this flag first.
- If no, then you might have changed your git history and those commits no longer exist.`);
};

async execute() {
if (!validateNodejsVersion(this.context)) {
return 1;
}

const {
workingDirectory,
errorOnNoSuccessfulPipeline,
lastSuccessfulEvent,
mainBranchName,
project,
token,
output,
} = this;

if (this.workingDirectory !== defaultWorkingDirectory) {
if (existsSync(workingDirectory)) {
process.chdir(workingDirectory);
} else {
this.context.logger.warn('\n');
this.context.logger.warn(`WARNING: Working directory '${workingDirectory}' doesn't exist.\n`);
}
}

let BASE_SHA: string | undefined;

const eventName = process.env['CI_MERGE_REQUEST_ID'] ? 'pull_request' : '';

const headResult = spawnSync('git', ['rev-parse', 'HEAD'], {
encoding: 'utf-8',
});

let HEAD_SHA = headResult.stdout.trim();

if (eventName === 'pull_request') {
try {
const baseResult = spawnSync('git', ['merge-base', `origin/${mainBranchName}`, 'HEAD'], { encoding: 'utf-8' });
BASE_SHA = baseResult.stdout;
} catch (e: any) {
this.context.logger.error(`${e.message}\n`);
return 1;
}
} else {
try {
BASE_SHA = await findSuccessfulCommit({ lastSuccessfulEvent, mainBranchName, project, token });
} catch (e: any) {
this.context.logger.error(`${e.message}\n`);
return 1;
}

if (!BASE_SHA) {
if (errorOnNoSuccessfulPipeline) {
this.reportFailure();
return 1;
} else {
logger.warn('\n');
logger.warn(`WARNING: Working directory '${workingDir}' doesn't exist.\n`);
this.context.logger.warn('\n');
this.context.logger.warn(
`WARNING: Unable to find a successful workflow run on 'origin/${mainBranchName}', or the latest successful workflow was connected to a commit which no longer exists on that branch (e.g. if that branch was rebased)\n`
);
this.context.logger.warn(`We are therefore defaulting to use HEAD~1 on 'origin/${mainBranchName}'\n`);
this.context.logger.warn('\n');
this.context.logger.warn(
`NOTE: You can instead make this a hard error by setting 'error-on-no-successful-workflow' on the action in your workflow.\n`
);
this.context.logger.warn('\n');

const commitCountOutput = spawnSync('git', ['rev-list', '--count', `origin/${mainBranchName}`], {
encoding: 'utf-8',
}).stdout.trim();
const commitCount = parseInt(stripNewLineEndings(commitCountOutput), 10);

const LAST_COMMIT_CMD = `origin/${mainBranchName}${commitCount > 1 ? '~1' : ''}`;
const baseRes = spawnSync('git', ['rev-parse', LAST_COMMIT_CMD], {
encoding: 'utf-8',
});
BASE_SHA = baseRes.stdout.trim();
}
} else {
this.context.logger.info('\n');
this.context.logger.info(`Found the last successful workflow run on 'origin/${mainBranchName}'\n`);
this.context.logger.info(`Commit: ${BASE_SHA}\n\n`);
}
}

await findLatestCommit(project, branch, output, token, errorOnNoSuccessfulPipeline);
});
BASE_SHA = stripNewLineEndings(BASE_SHA);
HEAD_SHA = stripNewLineEndings(HEAD_SHA);
this.context.logger.info(`NX_BASE: ${BASE_SHA}\n`);
this.context.logger.info(`NX_HEAD: ${HEAD_SHA}\n`);

let lines: string[] = [];

if (output) {
if (existsSync(output)) {
const variables = readFileSync(output).toString('utf-8').split('\n');
lines = variables.filter(
(variable) => !(variable.startsWith('NX_BASE') || variable.startsWith('NX_HEAD') || variable === '')
);
}
lines.push(`NX_BASE=${BASE_SHA}`, `NX_HEAD=${HEAD_SHA}`);
writeFileSync(output, lines.join('\n'), { encoding: 'utf-8' });
this.context.logger.info(
chalk.blue(`NX_BASE and NX_HEAD environment variables have been written to '${output}'\n`)
);
}

return command;
};
return 0;
}
}
Loading

0 comments on commit ea92e42

Please sign in to comment.