Skip to content

Commit

Permalink
Implement deployToGhPages plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
soenkehahn authored and alexdavid committed Nov 16, 2023
1 parent f075a8a commit 364a0d5
Show file tree
Hide file tree
Showing 4 changed files with 258 additions and 9 deletions.
165 changes: 165 additions & 0 deletions ts/deployToGhPages.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import { describe, it } from "https://deno.land/[email protected]/testing/bdd.ts";
import {
assertEquals,
assertMatch,
assertNotEquals,
} from "https://deno.land/[email protected]/assert/mod.ts";
import * as garn from "./mod.ts";
import { assertSuccess, runCommand, runExecutable } from "./testUtils.ts";
import outdent from "https://deno.land/x/[email protected]/mod.ts";

const project = garn
.mkProject(
{
description: "",
defaultEnvironment: garn.emptyEnvironment,
},
{
buildProject: garn.build`echo built > $out/artifact ; echo hidden > $out/.hidden`,
},
)
.add(garn.deployToGhPages((self) => self.buildProject));

const mkGitRepo = () => {
const path = Deno.makeTempDirSync();
const run = (...args: Array<string>) => {
const output = runCommand(new Deno.Command("git", { args, cwd: path }));
assertSuccess(output);
return output.stdout;
};
run("init");
run("commit", "--allow-empty", "-m", "first commit");
return { run, path };
};

const ansiRegexp = "(?:\\x1b\\[[0-9;]+m)+";

describe("deployToGhPagesBranch", () => {
it("allows to create a commit on a new 'gh-pages' branch", () => {
const gitRepo = mkGitRepo();
const output = runExecutable(project.deployToGhPages, {
cwd: gitRepo.path,
});
assertSuccess(output);
assertMatch(
output.stderr,
regexp`
^
warning: creating lock file [^\\n]+\\n
(?:
this derivation will be built:\\n
[^\\n]+\\n
building[^\\n]+\\n
)?
Created commit to "gh-pages" branch, but it has not been pushed yet\\n
Run ${ansiRegexp}git push <remote> gh-pages:gh-pages${ansiRegexp} to deploy\\n
$
`,
);
gitRepo.run("checkout", "gh-pages");
assertEquals(
Array.from(Deno.readDirSync(gitRepo.path))
.map((x) => x.name)
.sort(),
[".git", ".hidden", "artifact"],
);
assertEquals(Deno.readTextFileSync(`${gitRepo.path}/artifact`), "built\n");
assertMatch(
gitRepo.run("log", "-n1", "--pretty=format:%s"),
/^Deploy [0-9a-f]{7} to gh-pages$/,
);
assertEquals(gitRepo.run("status", "--short"), "");
});

it("allows to create a commit on an existing 'gh-pages' branch", () => {
const gitRepo = mkGitRepo();
gitRepo.run("checkout", "--orphan", "gh-pages");
Deno.writeTextFileSync(`${gitRepo.path}/foo`, "some existing content");
Deno.writeTextFileSync(`${gitRepo.path}/.hidden`, "some hidden content");
gitRepo.run("add", "foo", ".hidden");
gitRepo.run("commit", "-m", "Add some files");
gitRepo.run("checkout", "master");
assertSuccess(
runExecutable(project.deployToGhPages, { cwd: gitRepo.path }),
);
gitRepo.run("checkout", "gh-pages");
assertEquals(
Array.from(Deno.readDirSync(gitRepo.path))
.map((x) => x.name)
.sort(),
[".git", ".hidden", "artifact"],
);
assertEquals(Deno.readTextFileSync(`${gitRepo.path}/artifact`), "built\n");
assertMatch(
gitRepo.run("log", "--pretty=format:%s"),
/^Deploy [0-9a-f]{7} to gh-pages\nAdd some files$/,
);
assertEquals(gitRepo.run("status", "--short"), "");
});

it("does not commit files from the source branch to the 'gh-pages' branch", () => {
const gitRepo = mkGitRepo();
Deno.writeTextFileSync(`${gitRepo.path}/foo`, "some existing content");
Deno.writeTextFileSync(`${gitRepo.path}/.hidden`, "some hidden content");
gitRepo.run("add", "foo", ".hidden");
gitRepo.run("commit", "-m", "Add some files");
assertSuccess(
runExecutable(project.deployToGhPages, { cwd: gitRepo.path }),
);
gitRepo.run("checkout", "gh-pages");
assertEquals(
Array.from(Deno.readDirSync(gitRepo.path))
.map((x) => x.name)
.sort(),
[".git", ".hidden", "artifact"],
);
assertMatch(
gitRepo.run("log", "--pretty=format:%s"),
/^Deploy [0-9a-f]{7} to gh-pages$/,
);
});

it("does not affect working-tree changes", () => {
const gitRepo = mkGitRepo();
Deno.writeTextFileSync(`${gitRepo.path}/foo`, "some existing content");
gitRepo.run("add", "foo");
gitRepo.run("commit", "-m", "Add foo");
Deno.writeTextFileSync(`${gitRepo.path}/foo`, "foo has been modified");
Deno.writeTextFileSync(
`${gitRepo.path}/untracked`,
"some untracked content",
);
assertSuccess(
runExecutable(project.deployToGhPages, { cwd: gitRepo.path }),
);
assertEquals(
Array.from(Deno.readDirSync(gitRepo.path))
.map((x) => x.name)
.sort(),
[".git", "foo", "untracked"],
);
});

it("throws an error if the gh-pages branch is already checked out", () => {
const gitRepo = mkGitRepo();
gitRepo.run("checkout", "-b", "gh-pages");
const output = runExecutable(project.deployToGhPages, {
cwd: gitRepo.path,
});
assertNotEquals(output.exitCode, 0);
assertMatch(
output.stderr,
regexp`
^
warning: creating lock file [^\\n]+\\n
${ansiRegexp}error:
${ansiRegexp} deployToGhPages cannot run if gh-pages is currently checked out. Please change branches first.${ansiRegexp}\\n
$
`,
);
});
});

function regexp(f: TemplateStringsArray, ...templates: Array<string>) {
return new RegExp(outdent(f, ...templates).replaceAll("\n", ""));
}
73 changes: 73 additions & 0 deletions ts/deployToGhPages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { Executable } from "./executable.ts";
import { Package } from "./package.ts";
import { Plugin } from "./project.ts";

const ansiBold = "\\e[0;1m";
const ansiReset = "\\e[0m";
const ansiRedBold = "\\e[31;1m";

/**
* A garn plugin that allows easy deployment of a package to GitHub pages.
*
* @param pkg - The `Package` whose artifacts will be committed to the
* `gh-pages` branch. For convenience, this can also be a function that takes
* in a refrence to the project and returns the `Package`.
*
* Example:
* ```typescript
* export const myProject = mkNpmProject({ ... })
* .addPackage("genStaticSite", "npm run build && mv dist/* $out")
* .add(garn.deployToGhPages(project => project.genStaticSite));
* ```
*
* Then running `garn run myProject.deployToGhPages` will create the `gh-pages`
* branch for you if it doesn't already exist, and add a commit containing the
* artifacts from the specified project (`genStaticSite` in the example above)
* to the branch.
*
* At this point all you need to do to deploy is push the branch to GitHub with
* `git push <remote> gh-pages:gh-pages`
*/
export function deployToGhPages<T>(
pkg: Package | ((t: T) => Package),
): Plugin<{ deployToGhPages: Executable }, T> {
return (p) => {
if (p.defaultEnvironment == null) {
throw new Error(
`'deployToGhPages' can only be added to projects with a default environment`,
);
}
return {
deployToGhPages: p.defaultEnvironment!.shell`
set -eu
REPO_DIR=$(git rev-parse --show-toplevel)
TMP_DIR=$(mktemp -d)
TMP_SRC="$TMP_DIR/src"
TMP_DST="$TMP_DIR/dst"
VERSION_NAME=$(git describe --tags --dirty --always)
function cleanup() {
rm -rf "$TMP_DIR"
}
trap cleanup EXIT
if [ "$(git rev-parse --abbrev-ref HEAD)" = gh-pages ]; then
>&2 echo -e '${ansiRedBold}error:${ansiBold} deployToGhPages cannot run if gh-pages is currently checked out. Please change branches first.${ansiReset}'
exit 1
fi
git clone --quiet "$REPO_DIR" "$TMP_SRC"
git -C "$TMP_SRC" checkout gh-pages 2>/dev/null || git -C "$TMP_SRC" checkout --quiet --orphan gh-pages
cp -rv ${typeof pkg === "function" ? pkg(p) : pkg} "$TMP_DST"
chmod +w "$TMP_DST"
mv "$TMP_SRC/.git" "$TMP_DST"
git -C "$TMP_DST" add .
git -C "$TMP_DST" commit -m "Deploy $VERSION_NAME to gh-pages"
git fetch --quiet "$TMP_DST" gh-pages:gh-pages
>&2 echo -e 'Created commit to "gh-pages" branch, but it has not been pushed yet'
>&2 echo -e 'Run ${ansiBold}git push <remote> gh-pages:gh-pages${ansiReset} to deploy'
`,
};
};
}
1 change: 1 addition & 0 deletions ts/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ export * as javascript from "./javascript/mod.ts";

// tools
export { processCompose } from "./process_compose.ts";
export { deployToGhPages } from "./deployToGhPages.ts";
export * as nix from "./nix.ts";
28 changes: 19 additions & 9 deletions ts/testUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@ type Output = {
stderr: string;
};

export const runCommand = (command: Deno.Command): Output => {
const output = command.outputSync();
return {
exitCode: output.code,
stdout: new TextDecoder().decode(output.stdout),
stderr: new TextDecoder().decode(output.stderr),
};
};

const printOutput = (output: Output) => {
console.error(`
exitcode: ${output.exitCode}
Expand Down Expand Up @@ -46,7 +55,10 @@ export const assertStderr = (output: Output, expected: string) => {
}
};

export const runExecutable = (executable: garn.Executable): Output => {
export const runExecutable = (
executable: garn.Executable,
options: { cwd?: string } = {},
): Output => {
const tempDir = Deno.makeTempDirSync({ prefix: "garn-test" });
const nixpkgsInput = nix.nixFlakeDep("nixpkgs-repo", {
url: "github:NixOS/nixpkgs/6fc7203e423bbf1c8f84cccf1c4818d097612566",
Expand All @@ -70,12 +82,10 @@ export const runExecutable = (executable: garn.Executable): Output => {
}),
);
Deno.writeTextFileSync(`${tempDir}/flake.nix`, flakeFile);
const output = new Deno.Command("nix", {
args: ["run", tempDir],
}).outputSync();
return {
exitCode: output.code,
stdout: new TextDecoder().decode(output.stdout),
stderr: new TextDecoder().decode(output.stderr),
};
return runCommand(
new Deno.Command("nix", {
args: ["run", tempDir],
cwd: options.cwd,
}),
);
};

0 comments on commit 364a0d5

Please sign in to comment.