Skip to content

Commit

Permalink
[affected][newfeature]: comment support inside DSL and tests for affe…
Browse files Browse the repository at this point in the history
…cted_changes. (#9)

[affected][newfeature]: comment support inside DSL and tests for affected_changes. (#9)
  • Loading branch information
leblancmeneses authored Dec 11, 2024
1 parent 151e515 commit 90f4b30
Show file tree
Hide file tree
Showing 7 changed files with 653 additions and 90 deletions.
14 changes: 6 additions & 8 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,18 +68,16 @@ jobs:
force = false
deploy = "${{ github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/prod' }}"
- name: example affected
id: affected
uses: ./dist/apps/affected
with:
verbose: true
rules: |
ci: './github/workflows/ci.yml';
[affected](./apps/affected): './apps/affected/**' './dist/apps/affected/**' ci;
[version-autopilot](./apps/version-autopilot): './apps/version-autopilot/**' './dist/apps/version-autopilot/**' ci;
[pragma](./apps/pragma): './apps/pragma/**' './dist/apps/pragma/**' ci;
e2e: './e2e/**' affected version-autopilot !'**/*.md';
ci: '.github/workflows/ci.yml';
[affected](./apps/affected): './apps/affected/**' './dist/apps/affected/**';
[version-autopilot](./apps/version-autopilot): './apps/version-autopilot/**' './dist/apps/version-autopilot/**';
[pragma](./apps/pragma): './apps/pragma/**' './dist/apps/pragma/**';
e2e: './e2e/**' ci pragma affected version-autopilot !'**/*.md';
- name: e2e tests
if: ${{ !failure() && !cancelled() && fromJson(steps.affected.outputs.affected).changes.e2e }}
Expand All @@ -101,7 +99,7 @@ jobs:
run: git diff --exit-code -- ./dist

- name: debug
if: ${{ always() && !cancelled() }}
if: ${{ failure() && !cancelled() }}
shell: bash
run: |
echo ''
Expand Down
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
- [Actions](#actions)
- [Affected Action](#affected-action)
- [Rule DSL](#rule-dsl)
- [Rule Key Examples](#rule-key-examples)
- [Composing Rules](#composing-rules)
- [Exclusion Expression](#exclusion-expression)
- [Wrapping up example](#wrapping-up-example)
Expand Down Expand Up @@ -54,9 +53,15 @@ jobs:
recommended-imagetags-prefix: '' # optional; used in recommended_imagetags.
rules: |
<project-ui>: 'project-ui/**';
# project-ui is the image name, directory to calculate the sha, and changes key.
<project-api>: 'project-api/**';
# project-api is the image name, directory to calculate the sha, and changes key.
[project-dbmigrations](./databases/project): './databases/project/**';
# project-dbmigrations is the image name.
# './databases/project' is the directory to calculate the sha.
# changes.project-dbmigrations is boolean of the evaluated expression.
project-e2e: project-ui project-api project-dbmigrations !'**/*.md';
# changes.project-e2e is boolean of the evaluated expression.
```
### Rule DSL
Expand All @@ -67,21 +72,16 @@ These rules map a *project name*, its *directory*, and the *expression* to check
* **Rule keys with brackets** `[]` or `<>` will appear in the JSON object under `recommended_imagetags` or `shas`, and `changes`.
* **Rule keys without brackets** will only appear in `changes` but **not** in `recommended_imagetags` or `shas`.

#### Rule Key Examples

1. **Short Form**: `<project-ui>` The image name is `project-ui`, and the project directory is `project-ui`.
2. **Long Form**: `[project-dbmigrations](./databases/project)` The image name is `project-dbmigrations`, and the project directory is `./databases/project`.

#### Composing Rules

The `project-e2e` rule includes `project-ui`, `project-api`, and `project-dbmigrations`. This allows referencing prior expressions and combining them using `OR` operator.
The `project-e2e` rule includes `project-ui`, `project-api`, and `project-dbmigrations`. This allows referencing prior expressions and combining them.
For example, **e2e** runs if files change in any of these projects but not for markdown-only changes.

#### Exclusion Expression

The `!` operator excludes files or directories.

* For example, `**/*.md` excludes all markdown files.
* For example, `!'**/*.md'` excludes all markdown files.
* Glob expressions use [picomatch](https://github.com/micromatch/picomatch) for matching.

This structure provides flexibility and reusability for defining change-based rules across projects.
Expand Down
61 changes: 42 additions & 19 deletions apps/affected/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,56 +8,77 @@ import picomatch from 'picomatch';

function evaluateStatements(statements, originalChangedFiles) {
const result = {};
const seen = new Map();

function evaluateStatement(statementKey, changedFiles, seen = new Set<string>()) {
function evaluateStatement(statementKey, changedFiles) {
// Check if the statementKey has already been evaluated
if (seen.has(statementKey)) {
throw new Error(`Recursive or circular reference detected for statement: ${statementKey}`);
return seen.get(statementKey);
}
seen.add(statementKey);

const statement = statements.find((s) => s.key.name === statementKey);
if (!statement) {
throw new Error(`Referenced statement key '${statementKey}' does not exist.`);
}

let remainingFiles = [];
let cumulativeFiles = [];
let excludedFiles = [];

for (const value of statement.value) {
let currentFiles = [...changedFiles]; // Start with original files for each value

if (value.type === 'QUOTE_LITERAL') {
// Filter the files that match the QUOTE_LITERAL pattern
const isMatch = picomatch(value.value);
const isMatch = picomatch(value.value, { dot: true });
currentFiles = currentFiles.filter((file) => isMatch(file));
} else if (value.type === 'STATEMENT_REF') {
// Recursively evaluate the referenced statement and append new matches
const refMatches = evaluateStatement(value.value, originalChangedFiles, seen);
currentFiles = [...new Set([...currentFiles, ...(refMatches || [])])];
} else if (value.type === 'INVERSE' && value.exp.type === 'QUOTE_LITERAL') {
// Filter out files that match the INVERSE pattern
const isMatch = picomatch(value.exp.value);
currentFiles = currentFiles.filter((file) => !isMatch(file));
const refMatches = evaluateStatement(value.value, originalChangedFiles);
currentFiles = [...new Set([...currentFiles, ...(refMatches.cumulativeFiles || [])])];
excludedFiles = [...new Set([...excludedFiles, ...(refMatches.excludedFiles || [])])];
} else if (value.type === 'INVERSE') {
if (value.exp.type === 'QUOTE_LITERAL') {
// Filter out files that match the INVERSE pattern
const isMatch = picomatch(value.exp.value, { dot: true });
const inverseMatches = currentFiles.filter((file) => isMatch(file));
excludedFiles = [...new Set([...excludedFiles, ...inverseMatches])];
currentFiles = currentFiles.filter((file) => !isMatch(file));
} else if (value.exp.type === 'STATEMENT_REF') {
// Evaluate the referenced statement in INVERSE
const refMatches = evaluateStatement(value.exp.value, originalChangedFiles);
const inverseMatches = currentFiles.filter((file) =>
refMatches.cumulativeFiles.includes(file)
);
excludedFiles = [...new Set([...excludedFiles, ...inverseMatches])];
currentFiles = currentFiles.filter((file) => !inverseMatches.includes(file));
}
} else {
throw new Error(`Unsupported value type: ${value.type}`);
}

// Append currentFiles to remainingFiles while avoiding duplicates
remainingFiles = [...new Set([...remainingFiles, ...currentFiles])];
cumulativeFiles = [...new Set([...cumulativeFiles, ...currentFiles])];
}

seen.delete(statementKey);
return remainingFiles;
}
// Cache the result for the current statementKey
const evaluatedResult = { cumulativeFiles, excludedFiles };
seen.set(statementKey, evaluatedResult);

return evaluatedResult;
}

for (const statement of statements) {
if (statement.type === 'STATEMENT') {
result[statement.key.name] = evaluateStatement(statement.key.name, originalChangedFiles).length > 0;
const { cumulativeFiles, excludedFiles } = evaluateStatement(statement.key.name, originalChangedFiles);
const netFiles = cumulativeFiles.filter((file) => !excludedFiles.includes(file));
result[statement.key.name] = netFiles.length > 0;
}
}

return result;
}




export const getChangedFiles = async () => {
const eventName = github.context.eventName;
const baseSha = process.env.BASE_SHA || github.context.payload?.pull_request?.base?.sha || github.context.sha;
Expand Down Expand Up @@ -116,7 +137,6 @@ export const getCommitHash = (path: string, hasChanges: boolean) => {
export const getDevOrProdPrefixImageName = (hasChanges: boolean, sha: string, appTarget: string, path?: string, productionBranch?: string, imageTagPrefix?: string) => {
const folderOfInterest = path ? path.startsWith("./") ? path : `./${path}` : `./${appTarget}`;

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const baseRef = process.env.BASE_REF || github.context.payload?.pull_request?.base?.ref || process.env.GITHUB_REF_NAME;
const baseSha = process.env.BASE_SHA || github.context.payload?.pull_request?.base?.sha || github.context.sha;
const headSha = process.env.HEAD_SHA || github.context.payload?.pull_request?.head?.sha || github.context.sha;
Expand Down Expand Up @@ -208,6 +228,9 @@ export async function run() {
recommended_imagetags: affectedImageTags,
};
core.setOutput('affected', affectedOutput);
core.setOutput('affected_shas', affectedShas);
core.setOutput('affected_changes', affectedChanges);
core.setOutput('affected_recommended_imagetags', affectedImageTags);
core.info(`affected: ${JSON.stringify(affectedOutput, null, 2)}!`);
} catch (error) {
core.setFailed(error.message);
Expand Down
15 changes: 13 additions & 2 deletions apps/affected/src/parser.peggy
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,17 @@ validKeyName
return first + rest.join('');
}

_ "whitespace or comments"
= (Whitespace / LineComment / LineCommentAlt / MultilineComment)*

_ "whitespace"
= [ \t\n\r]*
Whitespace
= [ \t\n\r]+

LineComment
= "//" [^\n\r]* ("\n" / "\r\n" / "\r" / !.)

LineCommentAlt
= "#" [^\n\r]* ("\n" / "\r\n" / "\r" / !.)

MultilineComment
= "/*" (!"*/" .)* "*/"
Loading

0 comments on commit 90f4b30

Please sign in to comment.