Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions migrations/checks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,20 @@ steps:
drift-resolution-scripts-name: flyway-drift-resolution-scripts-${{ matrix.target }}
```

### Code Scanning

When using Flyway 12.2+, code review results are automatically uploaded to [GitHub Code Scanning](https://docs.github.com/en/code-security/code-scanning) in SARIF format. Results appear in the Security tab and as PR annotations.

This requires:
- GitHub Advanced Security to be enabled on the repository (available by default on public repos, requires GitHub Enterprise for private repos)
- The workflow to have `security-events: write` permission

If either requirement is not met, the upload is silently skipped.

| Input | Description | Required | Default |
|------------------------------|--------------------------------------------------------|----------|---------|
| `skip-code-scanning-upload` | Skip uploading SARIF results to GitHub Code Scanning | No | `false` |

### Other

| Input | Description | Required | Default |
Expand Down
11 changes: 11 additions & 0 deletions migrations/checks/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@ inputs:
description: 'Number of days to retain the drift resolution scripts artifact'
required: false
default: '7'
skip-code-scanning-upload:
description: 'Skip uploading SARIF results to GitHub Code Scanning'
required: false
default: 'false'

outputs:
exit-code:
Expand Down Expand Up @@ -159,3 +163,10 @@ runs:
path: ${{ steps.checks.outputs.drift-resolution-folder }}
retention-days: ${{ inputs.drift-resolution-scripts-retention-days }}
if-no-files-found: ignore
- name: Upload SARIF to GitHub Code Scanning
if: always() && inputs.skip-code-scanning-upload == 'false' && steps.checks.outputs.sarif-path != ''
uses: github/codeql-action/upload-sarif@v3
continue-on-error: true
with:
sarif_file: ${{ steps.checks.outputs.sarif-path }}
category: flyway-code-review
17 changes: 11 additions & 6 deletions migrations/checks/dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10584,6 +10584,7 @@ var un = () => {
exitCode: n.exitCode,
result: {
reportPath: e?.error?.htmlReport,
sarifReportPath: e?.error?.sarifReport,
...t
}
};
Expand All @@ -10593,6 +10594,7 @@ var un = () => {
exitCode: n.exitCode,
result: {
reportPath: r?.htmlReport,
sarifReportPath: r?.sarifReport,
...i
}
};
Expand Down Expand Up @@ -10623,6 +10625,7 @@ var un = () => {
return rn("code-violation-count", r.violationCount.toString()), rn("code-violation-codes", r.violationCodes.join(",")), {
exitCode: n,
reportPath: r.reportPath,
sarifReportPath: r.sarifReportPath,
violationCount: r.violationCount
};
}, zn = async (e, t) => {
Expand Down Expand Up @@ -10731,21 +10734,23 @@ var un = () => {
a
], s = o.find((e) => e?.reportPath)?.reportPath;
rn("report-path", vn(s ?? "report.html", e.workingDirectory));
let c = o.find((e) => e !== void 0 && e.exitCode !== 0);
rn("exit-code", (c?.exitCode ?? 0).toString());
let l;
if (i && (l = i.driftDetected ? "Drift detected" : i.comparisonSupported ? i.driftCheckSkipped ? "Drift check not run - skipped because no snapshot in database (expected for initial deployment)" : "No drift" : "Drift check not run - drift analysis is not supported for this database type"), await wn({
let c = r?.sarifReportPath;
rn("sarif-path", c ? vn(c, e.workingDirectory) : "");
let l = o.find((e) => e !== void 0 && e.exitCode !== 0);
rn("exit-code", (l?.exitCode ?? 0).toString());
let u;
if (i && (u = i.driftDetected ? "Drift detected" : i.comparisonSupported ? i.driftCheckSkipped ? "Drift check not run - skipped because no snapshot in database (expected for initial deployment)" : "No drift" : "Drift check not run - drift analysis is not supported for this database type"), await wn({
dryrun: n ? { exitCode: n.exitCode } : void 0,
code: r ? {
exitCode: r.exitCode,
violationCount: r.violationCount
} : void 0,
driftStatus: l,
driftStatus: u,
changes: a ? {
exitCode: a.exitCode,
changedObjectCount: a.changedObjectCount ?? 0
} : void 0
}), c) throw Error("Flyway checks failed");
}), l) throw Error("Flyway checks failed");
}, Gn = () => {
let e = tn("target-environment") || void 0, t = tn("target-url") || void 0, n = tn("target-user") || void 0, r = tn("target-password") || void 0, i = tn("target-schemas") || void 0, a = tn("target-migration-version") || void 0, o = tn("cherry-pick") || void 0, s = tn("build-environment") || void 0, l = tn("build-url") || void 0, u = tn("build-user") || void 0, d = tn("build-password") || void 0, f = tn("build-schemas") || void 0, p = nn("build-ok-to-erase"), m = nn("skip-code-review"), h = nn("skip-drift-check"), g = nn("skip-deployment-changes-report"), _ = nn("skip-deployment-script-review"), v = nn("fail-on-code-review"), y = nn("fail-on-drift"), b = tn("working-directory");
return {
Expand Down
7 changes: 6 additions & 1 deletion migrations/checks/src/flyway/check-code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,12 @@ const runCheckCode = async (inputs: FlywayMigrationsChecksInputs) => {
const { exitCode, result } = await checkForCodeReviewViolations(args, inputs.workingDirectory);
core.setOutput("code-violation-count", result.violationCount.toString());
core.setOutput("code-violation-codes", result.violationCodes.join(","));
return { exitCode, reportPath: result.reportPath, violationCount: result.violationCount };
return {
exitCode,
reportPath: result.reportPath,
sarifReportPath: result.sarifReportPath,
violationCount: result.violationCount,
};
};

export { runCheckCode };
3 changes: 3 additions & 0 deletions migrations/checks/src/flyway/run-checks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ const runChecks = async (inputs: FlywayMigrationsChecksInputs, edition: FlywayEd
const reportFile = results.find((r) => r?.reportPath)?.reportPath;
core.setOutput("report-path", resolvePath(reportFile ?? "report.html", inputs.workingDirectory));

const sarifFile = codeResult?.sarifReportPath;
core.setOutput("sarif-path", sarifFile ? resolvePath(sarifFile, inputs.workingDirectory) : "");

const failed = results.find((r) => r !== undefined && r.exitCode !== 0);
core.setOutput("exit-code", (failed?.exitCode ?? 0).toString());

Expand Down
27 changes: 24 additions & 3 deletions migrations/checks/tests/flyway/check-code.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,15 +98,36 @@ describe("runCheckCode", () => {
expect(args).not.toContain(expect.stringContaining("default_build"));
});

it("should return exitCode and reportPath from result", async () => {
it("should return exitCode, reportPath, and sarifReportPath from result", async () => {
checkForCodeReviewViolations.mockResolvedValue({
exitCode: 1,
result: { reportPath: "/tmp/report.html", violationCount: 1, violationCodes: ["RG06"] },
result: {
reportPath: "/tmp/report.html",
sarifReportPath: "/tmp/report.sarif",
violationCount: 1,
violationCodes: ["RG06"],
},
});

const result = await runCheckCode({});

expect(result).toEqual({ exitCode: 1, reportPath: "/tmp/report.html", violationCount: 1 });
expect(result).toEqual({
exitCode: 1,
reportPath: "/tmp/report.html",
sarifReportPath: "/tmp/report.sarif",
violationCount: 1,
});
});

it("should return undefined sarifReportPath when not present in result", async () => {
checkForCodeReviewViolations.mockResolvedValue({
exitCode: 0,
result: { reportPath: "/tmp/report.html", violationCount: 0, violationCodes: [] },
});

const result = await runCheckCode({});

expect(result).toEqual(expect.objectContaining({ sarifReportPath: undefined }));
});

it("should set code-violation-count and code-violation-codes outputs", async () => {
Expand Down
24 changes: 24 additions & 0 deletions migrations/checks/tests/flyway/run-checks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,4 +195,28 @@ describe("runChecks", () => {

expect(setOutput).toHaveBeenCalledWith("report-path", "/tmp/reports/custom-report.html");
});

it("should set sarif-path from sarifReport in code review output", async () => {
exec.mockImplementation(mockExec({ stdout: { sarifReport: "report.sarif" } }));

await runChecks(baseInputs, "enterprise");

expect(setOutput).toHaveBeenCalledWith("sarif-path", "report.sarif");
});

it("should set sarif-path to empty string when no sarifReport in output", async () => {
exec.mockImplementation(mockExec({ stdout: {} }));

await runChecks(baseInputs, "enterprise");

expect(setOutput).toHaveBeenCalledWith("sarif-path", "");
});

it("should prepend working directory to sarif-path", async () => {
exec.mockImplementation(mockExec({ stdout: { sarifReport: "report.sarif" } }));

await runChecks({ workingDirectory: "my-project" }, "enterprise");

expect(setOutput).toHaveBeenCalledWith("sarif-path", path.join("my-project", "report.sarif"));
});
});
24 changes: 21 additions & 3 deletions shared/src/check/check-for-code-review-violations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,25 @@ type CodeResultItem = { violations?: { code?: string }[] };

type CodeReviewSuccessOutput = {
htmlReport?: string;
sarifReport?: string;
individualResults?: { operation?: string; results?: CodeResultItem[] }[];
};

type CodeReviewErrorOutput = {
error?: { errorCode?: string; message?: string; results?: CodeResultItem[]; htmlReport?: string };
error?: {
errorCode?: string;
message?: string;
results?: CodeResultItem[];
htmlReport?: string;
sarifReport?: string;
};
};

type CheckForCodeReviewResult = {
exitCode: number;
result: {
reportPath?: string;
sarifReportPath?: string;
violationCount: number;
violationCodes: string[];
};
Expand All @@ -33,14 +41,24 @@ const checkForCodeReviewViolations = async (
const errorOutput = parseOutput<CodeReviewErrorOutput>(result.stdout);
errorOutput?.error?.message && core.error(errorOutput.error.message);
const violations = extractViolations(errorOutput?.error?.results ?? []);
return { exitCode: result.exitCode, result: { reportPath: errorOutput?.error?.htmlReport, ...violations } };
return {
exitCode: result.exitCode,
result: {
reportPath: errorOutput?.error?.htmlReport,
sarifReportPath: errorOutput?.error?.sarifReport,
...violations,
},
};
}

const output = parseOutput<CodeReviewSuccessOutput>(result.stdout);
const codeResults = output?.individualResults?.filter((r) => r.operation === "code");
const resultItems = codeResults?.flatMap((r) => r.results ?? []) ?? [];
const violations = extractViolations(resultItems);
return { exitCode: result.exitCode, result: { reportPath: output?.htmlReport, ...violations } };
return {
exitCode: result.exitCode,
result: { reportPath: output?.htmlReport, sarifReportPath: output?.sarifReport, ...violations },
};
} finally {
core.endGroup();
}
Expand Down
4 changes: 4 additions & 0 deletions shared/tests/check/check-for-code-review-violations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ describe("checkForCodeReviewViolations", () => {
mockExec({
stdout: {
htmlReport: "report.html",
sarifReport: "report.sarif",
individualResults: [
{
operation: "code",
Expand All @@ -52,6 +53,7 @@ describe("checkForCodeReviewViolations", () => {
exitCode: 0,
result: {
reportPath: "report.html",
sarifReportPath: "report.sarif",
violationCount: 3,
violationCodes: ["AM04", "RG06"],
},
Expand All @@ -67,6 +69,7 @@ describe("checkForCodeReviewViolations", () => {
message: "Code Analysis Violation(s) detected",
results: [{ violations: [{ code: "AM04" }] }],
htmlReport: "/tmp/report.html",
sarifReport: "/tmp/report.sarif",
},
},
exitCode: 1,
Expand All @@ -79,6 +82,7 @@ describe("checkForCodeReviewViolations", () => {
exitCode: 1,
result: {
reportPath: "/tmp/report.html",
sarifReportPath: "/tmp/report.sarif",
violationCount: 1,
violationCodes: ["AM04"],
},
Expand Down
14 changes: 14 additions & 0 deletions state/prepare/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,20 @@ steps:
undo-script-name: flyway-undo-script-${{ matrix.target }}
```

### Code Scanning

When using Flyway 12.2+, code review results are automatically uploaded to [GitHub Code Scanning](https://docs.github.com/en/code-security/code-scanning) in SARIF format. Results appear in the Security tab and as PR annotations.

This requires:
- GitHub Advanced Security to be enabled on the repository (available by default on public repos, requires GitHub Enterprise for private repos)
- The workflow to have `security-events: write` permission

If either requirement is not met, the upload is silently skipped.

| Input | Description | Required | Default |
|------------------------------|--------------------------------------------------------|----------|---------|
| `skip-code-scanning-upload` | Skip uploading SARIF results to GitHub Code Scanning | No | `false` |

## Outputs

| Output | Description |
Expand Down
11 changes: 11 additions & 0 deletions state/prepare/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ inputs:
description: 'Number of days to retain the deployment and undo script artifacts'
required: false
default: '7'
skip-code-scanning-upload:
description: 'Skip uploading SARIF results to GitHub Code Scanning'
required: false
default: 'false'

outputs:
exit-code:
Expand Down Expand Up @@ -171,3 +175,10 @@ runs:
archive: false
retention-days: ${{ inputs.deployment-script-retention-days }}
if-no-files-found: ignore
- name: Upload SARIF to GitHub Code Scanning
if: always() && inputs.skip-code-scanning-upload == 'false' && steps.prepare.outputs.sarif-path != ''
uses: github/codeql-action/upload-sarif@v3
continue-on-error: true
with:
sarif_file: ${{ steps.prepare.outputs.sarif-path }}
category: flyway-code-review
20 changes: 12 additions & 8 deletions state/prepare/dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10527,6 +10527,7 @@ var dn = () => {
exitCode: n.exitCode,
result: {
reportPath: e?.error?.htmlReport,
sarifReportPath: e?.error?.sarifReport,
...t
}
};
Expand All @@ -10536,6 +10537,7 @@ var dn = () => {
exitCode: n.exitCode,
result: {
reportPath: r?.htmlReport,
sarifReportPath: r?.sarifReport,
...i
}
};
Expand All @@ -10549,6 +10551,8 @@ var dn = () => {
violationCodes: [...new Set(t)]
};
}, kn = (e, t) => {
if (e) return f.isAbsolute(e) ? e : t ? f.join(t, e) : e;
}, An = (e, t) => {
if (e.skipCodeReview) {
cn("Skipping code review: \"skip-code-review\" set to true");
return;
Expand All @@ -10564,12 +10568,14 @@ var dn = () => {
"-check.scope=script",
`-check.scriptFilename=${t}`
];
}, An = async (e, t) => {
let n = kn(e, t);
}, jn = async (e, t) => {
let n = An(e, t);
if (!n) return;
let r = await Dn(n, e.workingDirectory);
return rn("code-violation-count", r.result.violationCount.toString()), rn("code-violation-codes", r.result.violationCodes.join(",")), r;
}, jn = async (e, t) => {
rn("code-violation-count", r.result.violationCount.toString()), rn("code-violation-codes", r.result.violationCodes.join(","));
let i = r.result.sarifReportPath;
return rn("sarif-path", i ? kn(i, e.workingDirectory) : ""), r;
}, Mn = async (e, t) => {
ln("Checking for drift");
try {
let n = await gn(e, t);
Expand Down Expand Up @@ -10608,16 +10614,14 @@ var dn = () => {
} finally {
un();
}
}, Mn = (e, t) => {
if (e) return f.isAbsolute(e) ? e : t ? f.join(t, e) : e;
}, Nn = (e) => [
"check",
"-drift",
"-check.failOnDrift=true",
...Cn(e),
...e.preDeploymentReportName ? [`-reportFilename=${e.preDeploymentReportName}`] : []
], Pn = async (e) => {
let { exitCode: t, result: n } = await jn(Nn(e), e.workingDirectory), r = Mn(n.reportPath, e.workingDirectory), i = Mn(n.driftResolutionFolder, e.workingDirectory);
let { exitCode: t, result: n } = await Mn(Nn(e), e.workingDirectory), r = kn(n.reportPath, e.workingDirectory), i = kn(n.driftResolutionFolder, e.workingDirectory);
return rn("exit-code", t.toString()), n.driftDetected !== void 0 && rn("drift-detected", n.driftDetected.toString()), r !== void 0 && rn("report-path", r), i !== void 0 && rn("drift-resolution-folder", i), {
exitCode: t,
result: n
Expand Down Expand Up @@ -10752,7 +10756,7 @@ await (async () => {
});
return;
}
let a = await An(t, i);
let a = await jn(t, i);
if (await Un({
driftStatus: n,
code: a ? {
Expand Down
Loading
Loading