Skip to content

Commit

Permalink
[affected][newfeature]: generalized image tag (#12)
Browse files Browse the repository at this point in the history
[affected][newfeature]: generalized image tag (#12)
  • Loading branch information
leblancmeneses authored Dec 23, 2024
1 parent 3b68540 commit 11ef5d9
Show file tree
Hide file tree
Showing 7 changed files with 254 additions and 70 deletions.
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,10 @@ jobs:
uses: leblancmeneses/actions/dist/apps/affected@main
with:
verbose: false # optional
gitflow-production-branch: '' # optional; used in recommended_imagetags.
recommended-imagetags-prefix: '' # optional; used in recommended_imagetags.
recommended-imagetags-tag-prefix: '' # optional; The prefix to add to the image tag. target:<prefix>sha1
recommended-imagetags-tag-suffix: '' # optional; The suffix to add to the image tag. target:sha1<suffix>'
recommended-imagetags-registry: '' # optional; used in recommended_imagetags.
recommended-imagetags-tag-truncate-size: 0 # optional; The number of characters to keep from the sha1 value.
rules: |
peggy-parser: 'apps/affected/src/parser.peggy';
peggy-parser-checkIf-incomplete: peggy-parser AND (!'apps/affected/src/parser.ts' OR !'apps/e2e/src/affected/parser.spec.ts');
Expand Down Expand Up @@ -195,15 +197,15 @@ The `affected` action will generate the following JSON objects:
},
"recommended_imagetags": {
"project-ui": [
"project-ui:dev-38aabc2d6ae9866f3c1d601cba956bb935c02cf5",
"project-ui:38aabc2d6ae9866f3c1d601cba956bb935c02cf5",
"project-ui:pr-6"
],
"project-api": [
"project-api:dev-dd65064e5d3e4b0a21b867fa02561e37b2cf7f01",
"project-api:dd65064e5d3e4b0a21b867fa02561e37b2cf7f01",
"project-api:pr-6"
],
"project-dbmigrations": [
"project-dbmigrations:dev-7b367954a3ca29a02e2b570112d85718e56429c9",
"project-dbmigrations:7b367954a3ca29a02e2b570112d85718e56429c9",
"project-dbmigrations:pr-6"
],
}
Expand Down
22 changes: 18 additions & 4 deletions apps/affected/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,20 @@ inputs:
rules:
description: 'Defines rules in DSL format specified in README.md.'
required: true
gitflow-production-branch:
description: 'The name of the production branch in your GitFlow workflow (e.g., "main" or "prod").'
recommended-imagetags-tag-prefix:
description: 'The prefix to add to the image tag. target:<prefix>sha'
required: false
default: ''
recommended-imagetags-prefix:
description: 'The prefix to add to all image tags'
recommended-imagetags-tag-suffix:
description: 'The suffix to add to the image tag. target:sha<suffix>'
required: false
default: ''
recommended-imagetags-tag-truncate-size:
description: 'The number of characters to keep from the sha1 value. Positive values retain characters from the start (left), negative values retain characters from the end (right), and 0 keeps the entire value.'
required: false
default: 0
recommended-imagetags-registry:
description: 'The registry to add to all image tags.'
required: false
default: ''
verbose:
Expand All @@ -19,6 +27,12 @@ inputs:
outputs:
affected:
description: 'A JSON array representing the projects that have changes, recommended_imagetags, and commit SHA.'
affected_shas:
description: 'A JSON object where the propery is the target and value is the sha1'
affected_changes:
description: 'A JSON object where the propery is the target and value is a boolean signifying if the target had changes'
affected_recommended_imagetags:
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: 'main.js'
31 changes: 15 additions & 16 deletions apps/affected/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,24 @@ import {evaluateStatementsForChanges} from './evaluateStatementsForChanges';
import {allGitFiles, evaluateStatementsForHashes} from './evaluateStatementsForHashes';
import { AST } from './parser.types';


export const getImageName = (appTarget: string, hasChanges: boolean, sha: string, productionBranch?: string, imageTagPrefix?: string) => {
let baseRef = process.env.BASE_REF || github.context.payload?.pull_request?.base?.ref || process.env.GITHUB_REF_NAME;

if (baseRef && baseRef.includes('/')) {
baseRef = baseRef.split('/').pop();
export const getImageName = (appTarget: string, sha: string, truncateSha1Size = 0, imageTagRegistry = '', imageTagPrefix = '', imageTagSuffix = '') => {
let sha1 = sha;
if (isNaN(truncateSha1Size) || truncateSha1Size === 0) {
sha1=sha;
} else if (truncateSha1Size > 0) {
sha1=sha.slice(0, truncateSha1Size);
} else {
sha1=sha.slice(truncateSha1Size);
}

let imageName1 = `${appTarget}:${baseRef}-${sha}`;
if (!hasChanges) {
if (productionBranch) {
imageName1 = `${appTarget}:${productionBranch}-${sha}`;
}
}
const imageName1 = `${appTarget}:${imageTagPrefix}${sha1}${imageTagSuffix}`;

let imageName2 = `${appTarget}:latest`;
if (github.context.eventName === 'pull_request') {
imageName2 = `${appTarget}:pr-${github.context.payload.pull_request.number}`;
}

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

export const log = (message: string, verbose: boolean) => {
Expand All @@ -43,8 +40,10 @@ export async function run() {

const rulesInput = core.getInput('rules', { required: true });
const verbose = core.getInput('verbose', { required: false }) === 'true';
const productionBranch = core.getInput('gitflow-production-branch', { required: false }) || '';
const imageTagPrefix = core.getInput('recommended-imagetags-prefix', { required: false }) || '';
const truncateSha1Size = parseInt(core.getInput('recommended-imagetags-tag-truncate-size', { required: false }) || '0');
const imageTagPrefix = core.getInput('recommended-imagetags-tag-prefix', { required: false }) || '';
const imageTagSuffix = core.getInput('recommended-imagetags-tag-suffix', { required: false }) || '';
const imageTagRegistry = core.getInput('recommended-imagetags-registry', { required: false }) || '';

log(`github.context: ${JSON.stringify(github.context, undefined, 2)}`, verbose);

Expand Down Expand Up @@ -74,7 +73,7 @@ export async function run() {
if (key.path) {
affectedShas[key.name] = commitSha[key.name];

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

log(`Key: ${key.name}, Path: ${key.path}, Commit SHA: ${commitSha}, Image: ${imageName}`, verbose);
Expand Down
167 changes: 162 additions & 5 deletions apps/e2e/src/affected/affected.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ describe("affected.spec", () => {
jest.clearAllMocks();
jest.resetModules();
github.context.eventName = 'push';
delete process.env.BASE_REF;
process.env.BASE_REF = 'dev';
});

test("should parse valid YAML and set outputs", async () => {
Expand Down Expand Up @@ -130,18 +128,177 @@ describe("affected.spec", () => {
});
expect(core.setOutput).toHaveBeenCalledWith("affected_recommended_imagetags", {
"project-ui": [
"project-ui:dev-" + getHash('project-ui/'),
"project-ui:" + getHash('project-ui/'),
"project-ui:latest",
],
"project-api": [
"project-api:dev-" + getHash('project-api/'),
"project-api:" + getHash('project-api/'),
"project-api:latest",
],
"project-dbmigrations": [
"project-dbmigrations:dev-" + getHash('databases/project/'),
"project-dbmigrations:" + getHash('databases/project/'),
"project-dbmigrations:latest",
],
});
expect(core.info).toHaveBeenCalled();
});


describe('recommended_imagetags', () => {
const files = [
"project-api/file1.ts",
"project-ui/file1.ts",
];

function getHash(folder: string) {
const matchedFiles = [...files.filter(f => f.startsWith(folder))].sort();
return crypto.createHash('sha1')
.update(matchedFiles.join('\n') + '\n')
.digest('hex');
}

beforeEach(() => {
jest.spyOn(core, "setOutput").mockImplementation(jest.fn());

const execSyncResponses = {
'git diff --name-status HEAD~1 HEAD': () => files.map(f => `M\t${f}`).join('\n'),
'git ls-files': () => files.join('\n'),
};

jest.spyOn(cp, 'execSync')
.mockImplementation((command: string) => {
if (command.startsWith('git hash-object')) {
const match = command.match(/git hash-object\s+"([^"]+)"/);
if (!match) {
throw new Error(`Unexpected command: ${command}`);
}

return match[1];
}

if (command.startsWith('git diff --name-status')) {
return files.map(f => `M\t${f}`).join('\n');
}

if (execSyncResponses[command]) {
return execSyncResponses[command]();
}
throw new Error(`Unexpected input: ${command}`);
});
});

test("should generate tags with prefix", async () => {
// Arrange
jest.spyOn(core, "getInput").mockImplementation((inputName: string) => {
if (inputName === "rules") return `
<project-api>: 'project-api/**/*.ts';
`;
if (inputName === "recommended-imagetags-tag-prefix") return `prefix-`;
return "";
});

// Act
await run();

// Assert
expect(core.setOutput).toHaveBeenCalledWith("affected_recommended_imagetags", {
"project-api": [
"project-api:prefix-" + getHash('project-api/'),
"project-api:latest",
],
});
});

test("should generate tags with suffix", async () => {
// Arrange
jest.spyOn(core, "getInput").mockImplementation((inputName: string) => {
if (inputName === "rules") return `
<project-api>: 'project-api/**/*.ts';
`;
if (inputName === "recommended-imagetags-tag-suffix") return `-suffix`;
return "";
});

// Act
await run();

// Assert
expect(core.setOutput).toHaveBeenCalledWith("affected_recommended_imagetags", {
"project-api": [
"project-api:" + getHash('project-api/') + '-suffix',
"project-api:latest",
],
});
});

test("should generate tags with keep first seven chars of sha1", async () => {
// Arrange
jest.spyOn(core, "getInput").mockImplementation((inputName: string) => {
if (inputName === "rules") return `
<project-api>: 'project-api/**/*.ts';
`;
if (inputName === "recommended-imagetags-tag-truncate-size") return `7`;
if (inputName === "recommended-imagetags-registry") return `registry.cool/`;
return "";
});

// Act
await run();

// Assert
expect(core.setOutput).toHaveBeenCalledWith("affected_recommended_imagetags", {
"project-api": [
"registry.cool/project-api:" + getHash('project-api/').slice(0, 7),
"registry.cool/project-api:latest",
],
});
});

test("should generate tags with keep last seven chars of sha1", async () => {
// Arrange
jest.spyOn(core, "getInput").mockImplementation((inputName: string) => {
if (inputName === "rules") return `
<project-api>: 'project-api/**/*.ts';
`;
if (inputName === "recommended-imagetags-tag-truncate-size") return `-7`;
return "";
});

// Act
await run();

// Assert
expect(core.setOutput).toHaveBeenCalledWith("affected_recommended_imagetags", {
"project-api": [
"project-api:" + getHash('project-api/').slice(-7),
"project-api:latest",
],
});
});

test("should generate tags with keep first seven chars of sha1", async () => {
// Arrange
jest.spyOn(core, "getInput").mockImplementation((inputName: string) => {
if (inputName === "rules") return `
<project-api>: 'project-api/**/*.ts';
`;
if (inputName === "recommended-imagetags-tag-prefix") return `prefix-`;
if (inputName === "recommended-imagetags-tag-suffix") return `-suffix`;
if (inputName === "recommended-imagetags-tag-truncate-size") return `7`;
if (inputName === "recommended-imagetags-registry") return `registry.cool/`;
return "";
});

// Act
await run();

// Assert
expect(core.setOutput).toHaveBeenCalledWith("affected_recommended_imagetags", {
"project-api": [
"registry.cool/project-api:prefix-" + getHash('project-api/').slice(0, 7) + '-suffix',
"registry.cool/project-api:latest",
],
});
});
});
});
2 changes: 0 additions & 2 deletions apps/e2e/src/affected/changes.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,12 +108,10 @@ M\tapps/affected/src/main.ts

delete github.context.eventName;
delete github.context.payload;
delete process.env.BASE_REF;
delete process.env.BASE_SHA;
delete process.env.HEAD_SHA;

github.context.eventName = 'pull_request';
process.env.BASE_REF='develop';
process.env.BASE_SHA='base1';
process.env.HEAD_SHA='head1';
github.context.payload = {
Expand Down
22 changes: 18 additions & 4 deletions dist/apps/affected/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,20 @@ inputs:
rules:
description: 'Defines rules in DSL format specified in README.md.'
required: true
gitflow-production-branch:
description: 'The name of the production branch in your GitFlow workflow (e.g., "main" or "prod").'
recommended-imagetags-tag-prefix:
description: 'The prefix to add to the image tag. target:<prefix>sha'
required: false
default: ''
recommended-imagetags-prefix:
description: 'The prefix to add to all image tags'
recommended-imagetags-tag-suffix:
description: 'The suffix to add to the image tag. target:sha<suffix>'
required: false
default: ''
recommended-imagetags-tag-truncate-size:
description: 'The number of characters to keep from the sha1 value. Positive values retain characters from the start (left), negative values retain characters from the end (right), and 0 keeps the entire value.'
required: false
default: 0
recommended-imagetags-registry:
description: 'The registry to add to all image tags.'
required: false
default: ''
verbose:
Expand All @@ -19,6 +27,12 @@ inputs:
outputs:
affected:
description: 'A JSON array representing the projects that have changes, recommended_imagetags, and commit SHA.'
affected_shas:
description: 'A JSON object where the propery is the target and value is the sha1'
affected_changes:
description: 'A JSON object where the propery is the target and value is a boolean signifying if the target had changes'
affected_recommended_imagetags:
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: 'main.js'
Loading

0 comments on commit 11ef5d9

Please sign in to comment.