Skip to content

Commit

Permalink
[affected][newfeature]: changed-files-output-path added (#13)
Browse files Browse the repository at this point in the history
[affected][newfeature]: changed-files-output-path added (#13)
  • Loading branch information
leblancmeneses authored Dec 25, 2024
1 parent 11ef5d9 commit cea1ce6
Show file tree
Hide file tree
Showing 16 changed files with 233 additions and 155 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ jobs:

- name: calculate version-autopilot
id: version-autopilot
uses: ./dist/apps/version-autopilot
uses: ./apps/version-autopilot
with:
major: 0
minor: 0
Expand All @@ -61,7 +61,7 @@ jobs:
- name: calculate pragma
id: pragma
uses: ./dist/apps/pragma
uses: ./apps/pragma
with:
variables: |
lint-appname-ui = 'skip'
Expand All @@ -70,7 +70,7 @@ jobs:
- name: calculate affected
id: affected
uses: ./dist/apps/affected
uses: ./apps/affected
with:
rules: |
peggy-parser: 'apps/affected/src/parser.peggy';
Expand Down
47 changes: 38 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,14 @@ jobs:

- name: calculate affected
id: affected
uses: leblancmeneses/actions/dist/apps/affected@main
uses: leblancmeneses/actions/apps/affected@main
with:
verbose: false # optional
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.
changed-files-output-path: '' # optional; The path to write the file containing the list of changed files.
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 @@ -297,7 +298,7 @@ These variables will take precedence over the defaults specified in the variable
```yaml
- name: calculate pragma
id: pragma
uses: leblancmeneses/actions/dist/apps/pragma@main
uses: leblancmeneses/actions/apps/pragma@main
with:
variables: | # INI format to initialize default variables
lint-appname-ui = ''
Expand Down Expand Up @@ -355,7 +356,7 @@ This will automatically increment the version on every **run** of your github ac
```yaml
- name: calculate version autopilot
id: version-autopilot
uses: leblancmeneses/actions/dist/apps/version-autopilot@main
uses: leblancmeneses/actions/apps/version-autopilot@main
with:
major: 0
minor: 0
Expand Down Expand Up @@ -444,6 +445,8 @@ If you are looking for semantic versioning research `git tags` and [release pipe

# Recommendations for multi-job pipeline

A [single job pipeline](https://github.com/leblancmeneses/actions/blob/main/.github/workflows/ci.yml) is a great starting point for CI/CD workflows. However, as your project evolves, you may need to divide your pipeline into multiple jobs to enhance performance, maintainability, and accommodate different operating systems for various tools.

Create an init job to calculate variables needed across multiple jobs. This will avoid redundant checkouts and calculations across each job.

Generate an init.yml file with the following content:
Expand Down Expand Up @@ -476,21 +479,31 @@ jobs:
- name: calculate pragma outputs
id: pragma
uses: leblancmeneses/actions/dist/apps/pragma@main
uses: leblancmeneses/actions/apps/pragma@main
with:
variables: |
...
- name: calculate affected outputs
id: affected
uses: leblancmeneses/actions/dist/apps/affected@main
uses: leblancmeneses/actions/apps/affected@main
with:
changed-files-output-path: .artifacts/affected.json
rules: |
...
- name: upload affected output
uses: actions/upload-artifact@v4
with:
name: affected
if-no-files-found: ignore
retention-days: 1
path: .artifacts/**
include-hidden-files: true
- name: calculate version-autopilot outputs
id: version-autopilot
uses: leblancmeneses/actions/dist/apps/version-autopilot@main
uses: leblancmeneses/actions/apps/version-autopilot@main
with:
major: 0
minor: 0
Expand Down Expand Up @@ -520,6 +533,17 @@ jobs:
needs: [vars]
runs-on: ubuntu-latest
steps:
- name: checkout code
uses: actions/checkout@v4
with:
persist-credentials: false
- name: download affected
uses: actions/download-artifact@v4
with:
name: affected
path: .artifacts/
- name: example output
run: |
echo "affected: "
Expand All @@ -528,15 +552,20 @@ jobs:
echo '${{ needs.vars.outputs.pragma }}' | jq .
echo "version-autopilot: "
echo '${{ needs.vars.outputs.version-autopilot }}' | jq .
cat ./.artifacts/affected.json
for file in $(jq -r '.[] | .file' ./.artifacts/affected.json); do
echo "processing: $file"
done
```

We recommend locking the `uses:` clause to a specific tag or sha to avoid pipeline
breakage due to future changes in the action.

```yaml
uses: leblancmeneses/actions/dist/apps/<taskname>@main # latest
uses: leblancmeneses/actions/dist/apps/<taskname>@v1.1.1 # specific tag
uses: leblancmeneses/actions/dist/apps/<taskname>@commit-sha # specific sha
uses: leblancmeneses/actions/apps/<taskname>@main # latest
uses: leblancmeneses/actions/apps/<taskname>@v1.1.1 # specific tag
uses: leblancmeneses/actions/apps/<taskname>@commit-sha # specific sha
```

# Run locally
Expand Down
12 changes: 10 additions & 2 deletions apps/affected/action.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
name: 'Affected Processor'
name: 'Affected Targets'
description: 'Determines affected components and generates corresponding SHA, recommended image tags, and change status based on the provided rules.'
author: Leblanc Meneses
branding:
icon: 'target'
color: 'red'
inputs:
rules:
description: 'Defines rules in DSL format specified in README.md.'
Expand All @@ -20,6 +24,10 @@ inputs:
description: 'The registry to add to all image tags.'
required: false
default: ''
changed-files-output-path:
description: 'Writes a JSON array of ChangedFile objects, where each object has a file name and its change status.'
required: false
default: ''
verbose:
description: 'Enable verbose logging. Use "true" or "false".'
required: false
Expand All @@ -35,4 +43,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: 'main.js'
main: '../../dist/apps/affected/main.js'
2 changes: 1 addition & 1 deletion apps/affected/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"thirdParty": true,
"main": "apps/affected/src/main.ts",
"tsConfig": "apps/affected/tsconfig.app.json",
"assets": ["apps/affected/action.yml"],
"assets": [],
"generatePackageJson": false,
"esbuildOptions": {
"sourcemap": true,
Expand Down
20 changes: 19 additions & 1 deletion apps/affected/src/changedFiles.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { execSync } from 'child_process';
import * as github from '@actions/github';
import { EXEC_SYNC_MAX_BUFFER } from './constants';
import { promises as fs } from 'fs';
import path from 'path';

export enum ChangeStatus {
Added = 'added',
Expand Down Expand Up @@ -81,4 +83,20 @@ export const getChangedFiles = async (): Promise<ChangedFile[]> => {
}

return changedFiles;
};
};


export const writeChangedFiles = async (changed_files_output_path: string, changedFiles: ChangedFile[]): Promise<void> => {
const directory = path.dirname(changed_files_output_path);
try {
await fs.mkdir(directory, { recursive: true });
} catch (err) {
throw new Error(`Failed to create directory at ${directory}: ${err.message}`, { cause: err });
}

try {
await fs.writeFile(changed_files_output_path, JSON.stringify(changedFiles, null, 2), 'utf8');
} catch (err) {
throw new Error(`Failed to write changed files to ${changed_files_output_path}: ${err.message}`, { cause: err });
}
}
20 changes: 13 additions & 7 deletions apps/affected/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import * as core from '@actions/core';
import * as github from '@actions/github';
import { parse } from './parser';
import {getChangedFiles} from './changedFiles';
import {evaluateStatementsForChanges} from './evaluateStatementsForChanges';
import {allGitFiles, evaluateStatementsForHashes} from './evaluateStatementsForHashes';
import { getChangedFiles, writeChangedFiles } from './changedFiles';
import { evaluateStatementsForChanges } from './evaluateStatementsForChanges';
import { allGitFiles, evaluateStatementsForHashes } from './evaluateStatementsForHashes';
import { AST } from './parser.types';



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

const imageName1 = `${appTarget}:${imageTagPrefix}${sha1}${imageTagSuffix}`;
Expand Down Expand Up @@ -44,6 +46,7 @@ export async function run() {
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 }) || '';
const changedFilesOutputPath = core.getInput('changed-files-output-path', { required: false }) || '';

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

Expand All @@ -56,8 +59,11 @@ export async function run() {

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

const {changes} = evaluateStatementsForChanges(statements, changedFiles);
const { changes } = evaluateStatementsForChanges(statements, changedFiles);
for (const [key, value] of Object.entries(changes)) {
affectedChanges[key] = value;
}
Expand Down
87 changes: 86 additions & 1 deletion apps/e2e/src/affected/affected.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,23 @@ jest.mock('child_process', () => {
};
});
import { run } from "../../../affected/src/main";
import * as changedFilesModule from '../../../affected/src/changedFiles';
import * as core from "@actions/core";
import * as cp from 'child_process';
import crypto from 'crypto';
import * as github from '@actions/github';



describe("affected.spec", () => {
beforeEach(() => {
jest.clearAllMocks();
jest.resetModules();
jest.restoreAllMocks();
github.context.eventName = 'push';
});
afterEach(() => {
jest.restoreAllMocks();
});

test("should parse valid YAML and set outputs", async () => {
// Arrange
Expand Down Expand Up @@ -301,4 +305,85 @@ describe("affected.spec", () => {
});
});
});


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

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 not generate an output file", async () => {
// Arrange
jest.spyOn(core, "getInput").mockImplementation((inputName: string) => {
if (inputName === "rules") return `
<project-api>: 'project-api/**/*.ts';
`;
return "";
});
let fileWritten = false;
jest.spyOn(changedFilesModule, 'writeChangedFiles').mockImplementation(async (changedFiles) => {
fileWritten = true;
});

// Act
await run();

// Assert
expect(fileWritten).toBe(false);
expect(changedFilesModule.writeChangedFiles).not.toHaveBeenCalled();
});

test("should generate an output file", async () => {
// Arrange
jest.spyOn(core, "getInput").mockImplementation((inputName: string) => {
if (inputName === "rules") return `
<project-api>: 'project-api/**/*.ts';
`;
if (inputName === "changed-files-output-path") return 'abc.txt';
return "";
});
let fileWritten = false;
jest.spyOn(changedFilesModule, 'writeChangedFiles').mockImplementation(async (changedFiles) => {
fileWritten = true;
});

// Act
await run();

// Assert
expect(fileWritten).toBe(true);
expect(changedFilesModule.writeChangedFiles).toHaveBeenCalledWith('abc.txt', files.map(f => ({ file: f, status: changedFilesModule.ChangeStatus.Modified })));
});
});
});
8 changes: 6 additions & 2 deletions apps/pragma/action.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
name: 'Pragma Action'
description: 'Used to change the behavior of the build run using dynamic variables controlled by pull request description.'
description: 'Used to change the behavior of the build run using dynamic variables controlled by the pull request description.'
author: Leblanc Meneses
branding:
icon: 'settings'
color: 'blue'
inputs:
variables:
description: 'INI-formatted list of key-value pairs (e.g., var1=value1\nvar2=value2)'
Expand All @@ -13,4 +17,4 @@ outputs:
description: 'A JSON object of the input variables and their values.'
runs:
using: 'node20'
main: 'main.js'
main: '../../dist/apps/pragma/main.js'
Loading

0 comments on commit cea1ce6

Please sign in to comment.