Skip to content

Commit

Permalink
[affected][newfeature]: cli for affected task (#17)
Browse files Browse the repository at this point in the history
[affected][newfeature]: cli for affected task (#17)
  • Loading branch information
leblancmeneses authored Dec 27, 2024
1 parent fb21e88 commit d112a2b
Show file tree
Hide file tree
Showing 17 changed files with 422 additions and 221 deletions.
2 changes: 1 addition & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"error",
{
"enforceBuildableLibDependency": true,
"allow": [],
"allow": ["./package.json"],
"depConstraints": [
{
"sourceTag": "*",
Expand Down
2 changes: 1 addition & 1 deletion .github/affected.rules
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
peggy-parser: 'apps/affected/src/parser.peggy';
peggy-parser-checkIf-incomplete: peggy-parser AND (!'apps/affected/src/parser.ts' OR !'apps/affected/src/parser.spec.ts');

<affected>: './apps/affected/**' './dist/apps/affected/**';
<affected>: './apps/affected/**' './dist/apps/affected/**' './package.json';
<version-autopilot>: './apps/version-autopilot/**' './dist/apps/version-autopilot/**';
<pragma>: './apps/pragma/**' './dist/apps/pragma/**';
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,12 @@ jobs:
sed -i '1i /* eslint-disable @typescript-eslint/ban-ts-comment */\n// @ts-nocheck' apps/affected/src/parser.ts
git diff --exit-code -- ./
- name: ensure version bump
if: ${{ !failure() && !cancelled() && fromJson(steps.affected.outputs.affected).changes.affected && github.event_name == 'pull_request' }}
run: |
# package.json bump required by affected.
git diff ${{ github.event.pull_request.base.sha }} ${{ github.sha }} package.json | grep version
- name: build affected
if: ${{ !failure() && !cancelled() && fromJson(steps.affected.outputs.affected).changes.affected }}
run: |
Expand Down
1 change: 0 additions & 1 deletion .husky-ext-act/.artifacts/.gitignore

This file was deleted.

13 changes: 0 additions & 13 deletions .husky-ext-act/act.payload.json

This file was deleted.

32 changes: 0 additions & 32 deletions .husky-ext-act/act.yml

This file was deleted.

43 changes: 12 additions & 31 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -9,51 +9,32 @@ set -e
# This script demonstrates how to manually calculate affected projects without relying on Nx, Bazel, or Make.
# Our rule-based approach standardizes the process, making it adaptable to diverse tech stacks and monorepo structures.

node dist/apps/affected/cli/main.cli.js calculate --rules-file ./.github/affected.rules > affected.json

required_tools=("gh" "act" "jq" "unzip" "npx")

check_command() {
if ! command -v "$1" &>/dev/null; then
echo "Notice: $1 is not installed. Skipping further processing."
exit 0
fi
}

# Check each tool
for tool in "${required_tools[@]}"; do
check_command "$tool"
done

echo "All required tools are installed. Proceeding..."


rm -rf .husky-ext-act/.artifacts/1/ || true

act pull_request -s GITHUB_TOKEN="$(gh auth token)" -W .husky-ext-act/act.yml -j calculate -e .husky-ext-act/act.payload.json --action-offline-mode --artifact-server-path .husky-ext-act/.artifacts
unzip .husky-ext-act/.artifacts/1/affected/affected.zip -d .husky-ext-act/.artifacts/1/affected/

if [[ ! -f .husky-ext-act/.artifacts/1/affected/affected-changes.json ]]; then
echo "File does not exist: .husky-ext-act/.artifacts/1/affected/affected-changes.json"
if [[ ! -f affected.json ]]; then
echo "File does not exist: affected.json"
exit 1
fi

# Check properties
affected=$(jq '.affected' .husky-ext-act/.artifacts/1/affected/affected-changes.json)
pragma=$(jq '.pragma' .husky-ext-act/.artifacts/1/affected/affected-changes.json)
version_autopilot=$(jq '.["version-autopilot"]' .husky-ext-act/.artifacts/1/affected/affected-changes.json)
affected=$(jq '.changes.affected' affected.json)
pragma=$(jq '.changes.pragma' affected.json)
version_autopilot=$(jq '.changes.["version-autopilot"]' affected.json)

if [[ $affected == "true" ]]; then
npx nx run affected:lint
npx nx run affected:test
npx nx run affected:build:production
npx nx run affected:build:production --no-cache
fi
if [[ $pragma == "true" ]]; then
npx nx run pragma:lint
npx nx run pragma:test
npx nx run pragma:build:production
npx nx run pragma:build:production --no-cache
fi
if [[ $version_autopilot == "true" ]]; then
npx nx run version-autopilot:lint
npx nx run version-autopilot:test
npx nx run version-autopilot:build:production
fi
npx nx run version-autopilot:build:production --no-cache
fi

rm affected.json
2 changes: 1 addition & 1 deletion apps/affected/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,4 @@ outputs:
description: 'A JSON object where the propery is the target and value is a boolean signifying if the target had changes'
runs:
using: 'node20'
main: '../../dist/apps/affected/main.js'
main: '../../dist/apps/affected/main/main.js'
49 changes: 47 additions & 2 deletions apps/affected/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@
"projectType": "application",
"tags": [],
"targets": {
"build": {
"build-main": {
"executor": "@nx/esbuild:esbuild",
"outputs": ["{options.outputPath}"],
"defaultConfiguration": "production",
"options": {
"platform": "node",
"outputPath": "dist/apps/affected",
"outputPath": "dist/apps/affected/main",
"format": ["cjs"],
"bundle": true,
"minify": true,
Expand Down Expand Up @@ -40,6 +40,51 @@
}
}
},
"build-cli": {
"executor": "@nx/esbuild:esbuild",
"outputs": ["{options.outputPath}"],
"defaultConfiguration": "production",
"options": {
"platform": "node",
"outputPath": "dist/apps/affected/cli",
"format": ["cjs"],
"bundle": true,
"minify": true,
"thirdParty": true,
"main": "apps/affected/src/main.cli.ts",
"tsConfig": "apps/affected/tsconfig.app.json",
"assets": [],
"generatePackageJson": false,
"esbuildOptions": {
"sourcemap": true,
"outExtension": {
".js": ".js"
}
}
},
"configurations": {
"development": {},
"production": {
"esbuildOptions": {
"legalComments": "none",
"sourcemap": false,
"outExtension": {
".js": ".js"
}
}
}
}
},
"build": {
"executor": "nx:run-commands",
"options": {
"commands": [
{ "command": "nx run affected:build-main:production" },
{ "command": "nx run affected:build-cli:production" }
],
"parallel": false
}
},
"serve": {
"executor": "@nx/js:node",
"defaultConfiguration": "development",
Expand Down
114 changes: 114 additions & 0 deletions apps/affected/src/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import fs from 'fs';
import path from 'path';
import { getChangedFiles, writeChangedFiles } from './changedFiles';
import { evaluateStatementsForChanges } from './evaluateStatementsForChanges';
import { allGitFiles, evaluateStatementsForHashes } from './evaluateStatementsForHashes';
import { parse } from './parser';
import { AST } from './parser.types';

export interface ImageContext {
event: string;
pull_request_number: number;
}

export const getRules = (rulesInput: string, rulesFile: string) => {
if (rulesInput && rulesFile) {
throw new Error("Only one of 'rules' or 'rules-file' can be specified. Please use either one.");
}

if (!rulesInput && !rulesFile) {
throw new Error("You must specify either 'rules' or 'rules-file'.");
}

let rules = '';
if (rulesInput) {
rules = rulesInput;
} else {
const rulesFilePath = path.resolve(rulesFile);
if (!fs.existsSync(rulesFilePath)) {
throw new Error(`The specified rules-file does not exist: ${rulesFilePath}`);
}

rules = fs.readFileSync(rulesFilePath, 'utf8');
}
return rules;
};



export const getImageName = (appTarget: string, sha: string, truncateSha1Size = 0, imageTagRegistry = '', imageTagPrefix = '', imageTagSuffix = '', imageContext?: ImageContext) => {
let sha1 = sha;
if (isNaN(truncateSha1Size) || truncateSha1Size === 0) {
sha1 = sha;
} else if (truncateSha1Size > 0) {
sha1 = sha.slice(0, truncateSha1Size);
} else {
sha1 = sha.slice(truncateSha1Size);
}

const imageName1 = `${appTarget}:${imageTagPrefix}${sha1}${imageTagSuffix}`;

let imageName2 = `${appTarget}:latest`;
if (imageContext && imageContext.event === 'pull_request') {
imageName2 = `${appTarget}:pr-${imageContext.pull_request_number}`;
}

return [imageName1, imageName2].map((imageName) => `${imageTagRegistry || ''}${imageName}`);
}

export const processRules = async (
log: (message: string) => void,
rulesInput: string,
truncateSha1Size: number,
imageTagRegistry: string,
imageTagPrefix: string,
imageTagSuffix: string,
changedFilesOutputFile?: string,
imageContext?: ImageContext) => {
const affectedImageTags: Record<string, string[]> = {};
const affectedShas: Record<string, string> = {};
const affectedChanges: Record<string, boolean> = {};

if (rulesInput) {
const statements = parse(rulesInput, undefined) as AST;

if (!Array.isArray(statements)) {
throw new Error('Rules must be an array of statements');
}

const changedFiles = await getChangedFiles();
log(`Changed Files: ${changedFiles.join('\n')}`);
if (changedFilesOutputFile) {
await writeChangedFiles(changedFilesOutputFile, changedFiles);
}

const { changes } = evaluateStatementsForChanges(statements, changedFiles);
for (const [key, value] of Object.entries(changes)) {
affectedChanges[key] = value;
}

const allFiles = await allGitFiles();
log(`All Git Files: ${allFiles.join('\n')}`);
const commitSha = await evaluateStatementsForHashes(statements, allFiles);

for (const statement of statements) {
if (statement.type !== 'STATEMENT') continue;

const { key } = statement;
if (key.path) {
affectedShas[key.name] = commitSha[key.name];

const imageName = getImageName(key.name, commitSha[key.name], truncateSha1Size, imageTagRegistry, imageTagPrefix, imageTagSuffix, imageContext);
affectedImageTags[key.name] = imageName;

log(`Key: ${key.name}, Path: ${key.path}, Commit SHA: ${commitSha}, Image: ${imageName}`);
}
}
}

return {
shas: affectedShas,
changes: affectedChanges,
recommended_imagetags: affectedImageTags,
};
};
53 changes: 53 additions & 0 deletions apps/affected/src/main.cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// node dist/apps/affected/cli/main.cli.js calculate --rules-file ./.github/affected.rules --verbose
// node dist/apps/affected/cli/main.cli.js calculate --rules-file ./.github/affected.rules
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
import { processRules, getRules } from './common';

import * as packageJson from '../../../package.json';

export const log = (verbose: boolean, message: string) => {
if (verbose) {
console.log(message);
}
};

yargs(hideBin(process.argv))
.scriptName("main.cli.js") // Optional: Set a custom script name for the help output
.usage('$0 <command> [options]')
.version(packageJson.version)
.command(
'calculate',
'Calculate affected targets',
(yargs) => {
yargs
.option('rules', { type: 'string', describe: 'Rules as a string', demandOption: false })
.option('rules-file', { type: 'string', describe: 'Path to rules file', demandOption: false })
.option('verbose', { type: 'boolean', default: false, describe: 'Verbose logging' })
.option('truncate-sha1-size', { type: 'number', default: 0, describe: 'SHA1 truncation size' })
.option('image-tag-prefix', { type: 'string', default: '', describe: 'Image tag prefix' })
.option('image-tag-suffix', { type: 'string', default: '', describe: 'Image tag suffix' })
.option('image-tag-registry', { type: 'string', default: '', describe: 'Image tag registry' })
.option('changed-files-output-file', { type: 'string', describe: 'Path to write changed files', demandOption: false });
},
async (argv) => {
try {
const rules = getRules(argv.rules as string, argv['rules-file'] as string);
const affectedOutput = await processRules(
log.bind(null, argv.verbose as boolean),
rules,
argv['truncate-sha1-size'] as number,
argv['image-tag-registry'] as string,
argv['image-tag-prefix'] as string,
argv['image-tag-suffix'] as string,
argv['changed-files-output-file'] as string | undefined
);
console.info(`${JSON.stringify(affectedOutput, null, 2)}`);
} catch (error) {
console.error(error.message);
process.exit(1);
}
}
)
.help()
.argv;
Loading

0 comments on commit d112a2b

Please sign in to comment.