Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Check for dirty-files post-build #116

Merged
merged 6 commits into from
Aug 28, 2024
Merged
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
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ various, distinct `stack-[*-]arguments[-*]` inputs that are used in more
specific ways. See the _Inputs_ section, or `action.yml` for documentation of
the new options.

The `fast` and `pedantic` inputs were removed. Use a ternary operator (see [Operators](https://docs.github.com/en/actions/learn-github-actions/expressions#operators)) to pass a flag conditionally. Example:
The `fast` and `pedantic` inputs were removed. Use a ternary operator (see
[Operators](https://docs.github.com/en/actions/learn-github-actions/expressions#operators))
to pass a flag conditionally. Example:

```yaml
stack-build-arguments: ${{ github.ref_name != 'main' && '--fast' || '' }} --pedantic
Expand All @@ -34,7 +36,7 @@ use a separate `stack-cache-action` step any more.
## Notable Changes in v3

Previous versions of this Action ran HLint and Weeder for you. We recommend
doing that as separate actions now, so, as of `v3, those options have been
doing that as separate actions now, so, as of `v3`, those options have been
removed.

Here is an example of running separate Actions:
Expand Down Expand Up @@ -80,6 +82,7 @@ jobs:
| `stack-build-arguments-test` | <p>Additional arguments passed after <code>stack-build-arguments</code> in <code>stack build</code> invocations on the <em>Test</em> step.</p> | `false` | `""` |
| `cache-prefix` | <p>Prefix applied to all cache keys. This can be any value you like, but teams often use <code>v{N}</code> and bump it to <code>v{N+1}</code> when/if they need to explicitly bust caches.</p> | `false` | `""` |
| `cache-save-always` | <p>Save artifacts to the cache even if the build fails. This may speed up builds in subsequent runs at the expense of slightly-longer builds when a full cache-hit occurs. Since <code>@v4.2.0</code>.</p> | `false` | `false` |
| `on-dirty-files` | <p>What to do if we find changes to the cabal or lock file after a build. Value can be <code>warn</code>, or <code>error</code>. Default is <code>warn</code>.</p> | `false` | `warn` |
| `install-stack` | <p>Install stack, if necessary</p> | `false` | `true` |
| `upgrade-stack` | <p>Upgrade stack</p> | `false` | `true` |
| `compiler-tools` | <p>A list of packages to install as compiler tools, one per line. This is useful to do here rather than separate <code>run</code> commands so that their installation is incorporated in the dependency cache. Since <code>@v5.2.0</code>.</p> | `false` | `""` |
Expand Down
5 changes: 5 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ inputs:
builds in subsequent runs at the expense of slightly-longer builds when a
full cache-hit occurs. Since `@v4.2.0`.
default: false
on-dirty-files:
description: |
What to do if we find changes to the cabal or lock file after a build.
Value can be `warn`, or `error`. Default is `warn`.
default: warn
install-stack:
description: |
Install stack, if necessary
Expand Down
103 changes: 103 additions & 0 deletions dist/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,102 @@
/******/ (() => { // webpackBootstrap
/******/ var __webpack_modules__ = ({

/***/ 5644:
/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {

"use strict";

var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.isInterestingFile = exports.parseGitStatus = exports.checkDirtyFiles = exports.parseOnDirtyFiles = void 0;
const core = __importStar(__nccwpck_require__(2186));
const exec = __importStar(__nccwpck_require__(1514));
function parseOnDirtyFiles(input) {
switch (input) {
case "warn":
case "error":
return input;
default:
throw new Error(`Invalid on-dirty-files, must be warn or error, saw: ${input}`);
}
}
exports.parseOnDirtyFiles = parseOnDirtyFiles;
async function checkDirtyFiles(onDirtyFiles) {
const stdout = await readGitStatus();
const paths = parseGitStatus(stdout).filter(isInterestingFile);
if (paths.length === 0) {
return;
}
const message = `Build caused changes to ${paths.join(", ")}`;
switch (onDirtyFiles) {
case "warn":
core.warning(message);
break;
case "error":
throw new Error(message);
}
}
exports.checkDirtyFiles = checkDirtyFiles;
async function readGitStatus() {
let stdout = "";
const options = {
listeners: {
stdout: (data) => {
stdout += data.toString();
},
},
ignoreReturnCode: true,
};
await exec.exec("git", ["status", "--porcelain"], options);
return stdout;
}
function parseGitStatus(stdout) {
return stdout
.split("\n")
.filter((path) => {
return !path.startsWith("??");
})
.map((path) => {
return path.replace(/^\s*/, "").split(/\s+/).slice(1).join(" ");
})
.filter((path) => {
return path.trim() !== "";
});
}
exports.parseGitStatus = parseGitStatus;
const INTERESTING_EXTENSIONS = [".cabal", ".yaml.lock"];
function isInterestingFile(path) {
return INTERESTING_EXTENSIONS.some((ext, _index, _array) => {
return path.endsWith(ext);
});
}
exports.isInterestingFile = isInterestingFile;


/***/ }),

/***/ 9778:
/***/ ((__unused_webpack_module, exports) => {

Expand Down Expand Up @@ -121,10 +217,12 @@ exports.getInputs = void 0;
const core = __importStar(__nccwpck_require__(2186));
const Shellwords = __importStar(__nccwpck_require__(8519));
const envsubst_1 = __nccwpck_require__(9778);
const dirty_files_1 = __nccwpck_require__(5644);
function getInputs() {
const getBuildArguments = (step) => {
return getShellWordsInput("stack-build-arguments").concat(getShellWordsInput(`stack-build-arguments-${step}`));
};
const rawOnDirtyFiles = core.getInput("on-dirty-files", { required: true });
return {
workingDirectory: getInputDefault("working-directory", null),
test: core.getBooleanInput("test"),
Expand All @@ -136,6 +234,7 @@ function getInputs() {
stackBuildArgumentsTest: getBuildArguments("test"),
cachePrefix: core.getInput("cache-prefix"),
cacheSaveAlways: core.getBooleanInput("cache-save-always"),
onDirtyFiles: (0, dirty_files_1.parseOnDirtyFiles)(rawOnDirtyFiles),
installStack: core.getBooleanInput("install-stack"),
upgradeStack: core.getBooleanInput("upgrade-stack"),
compilerTools: core.getMultilineInput("compiler-tools"),
Expand Down Expand Up @@ -185,6 +284,7 @@ var __importStar = (this && this.__importStar) || function (mod) {
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
const core = __importStar(__nccwpck_require__(2186));
const dirty_files_1 = __nccwpck_require__(5644);
const stack_cli_1 = __nccwpck_require__(8939);
const get_cache_keys_1 = __nccwpck_require__(1341);
const hash_project_1 = __nccwpck_require__(4717);
Expand Down Expand Up @@ -269,6 +369,9 @@ async function run() {
saveOnError: inputs.cacheSaveAlways,
});
});
await core.group("Check for dirty files", async () => {
await (0, dirty_files_1.checkDirtyFiles)(inputs.onDirtyFiles);
});
if (inputs.test) {
await core.group("Test", async () => {
await stack.buildTest(inputs.stackBuildArgumentsTest);
Expand Down
57 changes: 57 additions & 0 deletions src/dirty-files.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { parseGitStatus, isInterestingFile } from "./dirty-files";

describe("parseGitStatus", () => {
test("parse file name, and filters untracked", () => {
const paths = parseGitStatus(
[
"A staged-file.rb",
" M action.yml",
" M src/inputs.ts",
"?? src/new-file.ts",
" M src/path with spaces.md",
].join("\n"),
);

expect(paths).toEqual([
"staged-file.rb",
"action.yml",
"src/inputs.ts",
"src/path with spaces.md",
]);
});

const empties = [
["empty", ""],
["newline", "\n"],
["spaces", " "],
["spaces+newline", " \n"],
];

test.each(empties)("handles %s as no paths", (_arg, str) => {
expect(parseGitStatus(str)).toEqual([]);
});
});

describe("isInterestingFile", () => {
const interesting = [
"foo.cabal",
"bar.cabal",
"stack.yaml.lock",
"stack-lts20.yaml.lock",
];

test.each(interesting)("considers %p interesting", (path) => {
expect(isInterestingFile(path)).toBe(true);
});

const uninteresting = [
"some-file.md",
"other file.txt",
"foo.cabal.lock",
"foo.yaml.lock2",
];

test.each(uninteresting)("considers %p uninteresting", (path) => {
expect(isInterestingFile(path)).toBe(false);
});
});
80 changes: 80 additions & 0 deletions src/dirty-files.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import * as core from "@actions/core";
import * as exec from "@actions/exec";

export type OnDirtyFiles = "warn" | "error";

export function parseOnDirtyFiles(input: string): OnDirtyFiles {
switch (input) {
case "warn":
case "error":
return input as OnDirtyFiles;
default:
throw new Error(
`Invalid on-dirty-files, must be warn or error, saw: ${input}`,
);
}
}

export async function checkDirtyFiles(onDirtyFiles: OnDirtyFiles) {
const stdout = await readGitStatus();
const paths = parseGitStatus(stdout).filter(isInterestingFile);

if (paths.length === 0) {
return;
}

const message = `Build caused changes to ${paths.join(", ")}`;

switch (onDirtyFiles) {
case "warn":
core.warning(message);
break;
case "error":
throw new Error(message);
}
}

async function readGitStatus(): Promise<string> {
let stdout = "";

const options: exec.ExecOptions = {
listeners: {
stdout: (data: Buffer) => {
stdout += data.toString();
},
},
ignoreReturnCode: true,
};

await exec.exec("git", ["status", "--porcelain"], options);

return stdout;
}

// Exported for testing
export function parseGitStatus(stdout: string): string[] {
return stdout
.split("\n")
.filter((path) => {
// We don't care about untracked files because users may be choosing not
// to commit generated files -- in which case it showing up here is
// expected.
return !path.startsWith("??");
})
.map((path) => {
// Drop leading space, split on space, drop first column and rejoin
return path.replace(/^\s*/, "").split(/\s+/).slice(1).join(" ");
})
.filter((path) => {
return path.trim() !== "";
});
}

const INTERESTING_EXTENSIONS: string[] = [".cabal", ".yaml.lock"];

// Exported for testing
export function isInterestingFile(path: string): boolean {
return INTERESTING_EXTENSIONS.some((ext, _index, _array) => {
return path.endsWith(ext);
});
}
5 changes: 5 additions & 0 deletions src/inputs.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as core from "@actions/core";
import * as Shellwords from "shellwords-ts";
import { envsubst } from "./envsubst";
import { type OnDirtyFiles, parseOnDirtyFiles } from "./dirty-files";

export type Inputs = {
workingDirectory: string | null;
Expand All @@ -13,6 +14,7 @@ export type Inputs = {
stackBuildArgumentsTest: string[];
cachePrefix: string;
cacheSaveAlways: boolean;
onDirtyFiles: OnDirtyFiles;
installStack: boolean;
upgradeStack: boolean;
compilerTools: string[];
Expand All @@ -28,6 +30,8 @@ export function getInputs(): Inputs {
);
};

const rawOnDirtyFiles = core.getInput("on-dirty-files", { required: true });

return {
workingDirectory: getInputDefault("working-directory", null),
test: core.getBooleanInput("test"),
Expand All @@ -39,6 +43,7 @@ export function getInputs(): Inputs {
stackBuildArgumentsTest: getBuildArguments("test"),
cachePrefix: core.getInput("cache-prefix"),
cacheSaveAlways: core.getBooleanInput("cache-save-always"),
onDirtyFiles: parseOnDirtyFiles(rawOnDirtyFiles),
installStack: core.getBooleanInput("install-stack"),
upgradeStack: core.getBooleanInput("upgrade-stack"),
compilerTools: core.getMultilineInput("compiler-tools"),
Expand Down
5 changes: 5 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as core from "@actions/core";

import { checkDirtyFiles } from "./dirty-files";
import { StackCLI } from "./stack-cli";
import { getCacheKeys } from "./get-cache-keys";
import { hashProject } from "./hash-project";
Expand Down Expand Up @@ -125,6 +126,10 @@ async function run() {
);
});

await core.group("Check for dirty files", async () => {
await checkDirtyFiles(inputs.onDirtyFiles);
});

if (inputs.test) {
await core.group("Test", async () => {
await stack.buildTest(inputs.stackBuildArgumentsTest);
Expand Down
Loading