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

Implement GitHub Pages plugin #409

Merged
merged 5 commits into from
Nov 16, 2023
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
4 changes: 4 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ jobs:
fallback = true
- name: Remove /opt to free up disk space
run: sudo rm -rf /opt/*
- name: Set up git user
run: |
git config --global user.name "ci-test-user"
git config --global user.email "[email protected]"
- uses: lriesebos/nix-develop-command@v1
with:
command: "just github-ci"
173 changes: 173 additions & 0 deletions ts/deployToGhPages.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
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`mkdir -p $out/assets ; echo built > $out/assets/artifact ; echo hidden > $out/.hidden`,
},
)
.add(garn.deployToGhPages((self) => self.buildProject));

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

const ansiRegexp = "\\x1b\\[[0-9;]+m";

describe("deployToGhPages", () => {
it("allows to create a commit on a new 'gh-pages' branch", () => {
const gitRepo = mkGitRepo();
const output = assertSuccess(
runExecutable(project.deployToGhPages, {
cwd: gitRepo.path,
}),
);
assertMatch(
output.stderr,
regexp`
^
warning: creating lock file [^\\n]+\\n
(
(this|these \\d+) derivations? 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", "assets"],
);
assertEquals(
Deno.readTextFileSync(`${gitRepo.path}/assets/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", "assets"],
);
assertEquals(
Deno.readTextFileSync(`${gitRepo.path}/assets/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", "assets"],
);
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", ""));
}
74 changes: 74 additions & 0 deletions ts/deployToGhPages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
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](https://pages.github.com/).
*
* @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 reference 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 -R +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 @@ -24,4 +24,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";
31 changes: 21 additions & 10 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 All @@ -19,9 +28,10 @@ const printOutput = (output: Output) => {
`);
};

export const assertSuccess = (output: Output) => {
export const assertSuccess = (output: Output): Output => {
try {
assertEquals(output.exitCode, 0);
return output;
} catch (e) {
printOutput(output);
throw e;
Expand All @@ -46,7 +56,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 +83,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,
}),
);
};