Skip to content

Commit

Permalink
feat: interactive setup
Browse files Browse the repository at this point in the history
  • Loading branch information
whilefoo committed Nov 13, 2024
1 parent a65c27a commit 6916ad3
Show file tree
Hide file tree
Showing 7 changed files with 480 additions and 2 deletions.
Binary file modified bun.lockb
Binary file not shown.
11 changes: 9 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"format:prettier": "prettier --write .",
"format:cspell": "cspell **/*",
"prepare": "node .husky/install.mjs",
"deploy": "tsx ./scripts/deploy.ts",
"deploy-dev": "wrangler deploy --env dev",
"deploy-production": "wrangler deploy --env production",
"worker": "wrangler dev --env dev --port 8787",
Expand All @@ -30,7 +31,8 @@
"knip-ci": "knip --no-exit-code --reporter json --config .github/knip.ts",
"jest:test": "jest --coverage",
"plugin:hello-world": "tsx tests/__mocks__/hello-world-plugin.ts",
"setup-kv": "bun --env-file=.dev.vars deploy/setup-kv-namespace.ts"
"setup-kv": "bun --env-file=.dev.vars deploy/setup-kv-namespace.ts",
"setup": "tsx ./scripts/setup.ts"
},
"keywords": [
"typescript",
Expand All @@ -52,7 +54,7 @@
"@octokit/webhooks": "13.3.0",
"@octokit/webhooks-types": "7.5.1",
"@sinclair/typebox": "^0.33.20",
"@ubiquity-os/plugin-sdk": "^1.0.11",
"@ubiquity-os/plugin-sdk": "^1.0.11",
"dotenv": "16.4.5",
"typebox-validators": "0.3.5",
"yaml": "2.4.5"
Expand All @@ -65,13 +67,15 @@
"@cspell/dict-software-terms": "3.4.6",
"@cspell/dict-typescript": "3.1.5",
"@eslint/js": "9.7.0",
"@inquirer/prompts": "^7.1.0",
"@jest/globals": "29.7.0",
"@mswjs/data": "0.16.1",
"@mswjs/http-middleware": "0.10.1",
"@swc/core": "1.6.5",
"@swc/jest": "0.2.36",
"@types/jest": "29.5.12",
"@types/node": "20.14.10",
"@types/node-rsa": "^1.1.4",
"cspell": "8.9.0",
"esbuild": "0.23.0",
"eslint": "9.7.0",
Expand All @@ -84,7 +88,10 @@
"jest-junit": "16.0.0",
"knip": "5.26.0",
"lint-staged": "15.2.7",
"node-rsa": "^1.1.1",
"npm-run-all": "4.1.5",
"open": "^10.1.0",
"ora": "^8.1.1",
"prettier": "3.3.3",
"smee-client": "^2.0.4",
"toml": "3.0.0",
Expand Down
230 changes: 230 additions & 0 deletions scripts/deploy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
import { confirm, input, select } from "@inquirer/prompts";
import { exec, execSync, spawn } from "child_process";
import { readFileSync, unlinkSync, writeFileSync } from "fs";
import ora from "ora";
import path from "path";
import { parse } from "dotenv";
import toml from "toml";
// @ts-expect-error No typings exist for this package
import * as tomlify from "tomlify-j0.4";

interface WranglerConfiguration {
name: string;
env: {
[env: string]: {
kv_namespaces?: {
id: string;
binding: string;
}[];
};
};
kv_namespaces: {
id: string;
binding: string;
}[];
}

const WRANGLER_PATH = path.resolve(__dirname, "..", "node_modules/.bin/wrangler");
const WRANGLER_TOML_PATH = path.resolve(__dirname, "..", "wrangler.toml");
const BINDING_NAME = "PLUGIN_CHAIN_STATE";

function checkIfWranglerInstalled() {
return new Promise((resolve) => {
exec(`${WRANGLER_PATH} --version`, (err, stdout, stderr) => {
if (err || stderr) {
resolve(false);
}
resolve(true);
});
});
}

function checkIfWranglerIsLoggedIn() {
return new Promise((resolve, reject) => {
exec(`${WRANGLER_PATH} whoami`, (err, stdout, stderr) => {
if (err) {
reject(err);
}
if (stdout.includes("You are not authenticated") || stderr.includes("You are not authenticated")) {
resolve(false);
} else {
resolve(true);
}
});
});
}

function wranglerLogin() {
return new Promise<void>((resolve, reject) => {
const loginProcess = spawn(WRANGLER_PATH, ["login"], { stdio: "inherit" });

loginProcess.on("close", (code) => {
if (code !== 0) {
reject();
} else {
resolve();
}
});
});
}

function wranglerBulkSecrets(env: string | null, filePath: string) {
return new Promise<void>((resolve, reject) => {
const args = env ? ["--env", env] : [];
const process = spawn(WRANGLER_PATH, ["secret", "bulk", filePath, ...args], { stdio: "inherit" });

process.on("close", (code) => {
if (code !== 0) {
reject();
} else {
resolve();
}
});
process.on("error", (err) => {
reject(err);
});
});
}

function wranglerDeploy(env: string | null) {
return new Promise<void>((resolve, reject) => {
const args = env ? ["--env", env] : [];
const process = spawn(WRANGLER_PATH, ["deploy", ...args], { stdio: "inherit" });

process.on("close", (code) => {
if (code !== 0) {
reject();
} else {
resolve();
}
});
});
}

function wranglerKvNamespace(projectName: string, namespace: string) {
const kvList = JSON.parse(execSync(`${WRANGLER_PATH} kv namespace list`).toString()) as { id: string; title: string }[];
const existingNamespace = kvList.find((o) => o.title === namespace || o.title === `${projectName}-${namespace}`);
if (existingNamespace) {
return existingNamespace.id;
}

const res = execSync(`${WRANGLER_PATH} kv namespace create ${namespace}`).toString();

const newId = res.match(/id = \s*"([^"]+)"/)?.[1];
if (!newId) {
console.log(res);
throw new Error(`The new ID could not be found.`);
}
return newId;
}

void (async () => {
const spinner = ora("Checking if Wrangler is installed").start();
const wranglerInstalled = await checkIfWranglerInstalled();
if (!wranglerInstalled) {
spinner.fail("Wrangler is not installed. Please install it before running this script");
process.exit(1);
} else {
spinner.succeed("Wrangler is installed");
}

spinner.start("Checking if Wrangler is logged in");
const wranglerLoggedIn = await checkIfWranglerIsLoggedIn();
if (!wranglerLoggedIn) {
spinner.warn("Wrangler is not logged in. Please login to Wrangler");
await wranglerLogin();
spinner.succeed("Wrangler is now logged in");
} else {
spinner.succeed("Wrangler is logged in");
}

spinner.start("Searching environments in wrangler.toml");
const wranglerToml: WranglerConfiguration = toml.parse(readFileSync(WRANGLER_TOML_PATH, "utf-8"));
if (!wranglerToml) {
spinner.fail("Error parsing wrangler.toml");
process.exit(1);
}
const envs = Object.keys(wranglerToml.env ?? {});
let selectedEnv: string | null = null;
if (envs.length === 0) {
spinner.warn("No environments found, choosing default environment");
} else if (envs.length === 1) {
spinner.warn(`Only one environment found: ${envs[0]}`);
selectedEnv = envs[0];
} else if (envs.length > 1) {
spinner.stop();
selectedEnv = await select({
message: "Select the environment to deploy to:",
choices: envs,
});
}

const willSetSecrets = await confirm({
message: "Do you want to set secrets?",
default: true,
});
if (willSetSecrets) {
const envFile = await input({
message: "Enter the name of the env file to use:",
default: `.${selectedEnv}.vars`,
});
const spinner = ora("Setting secrets").render();
try {
const env = readFileSync(path.resolve(__dirname, "..", envFile), { encoding: "utf-8" });
const parsedEnv = parse(env);
if (parsedEnv) {
const tmpPath = path.resolve(__dirname, "..", `${envFile}.json.tmp`);
writeFileSync(tmpPath, JSON.stringify(parsedEnv));
await wranglerBulkSecrets(selectedEnv, tmpPath);
unlinkSync(tmpPath); // deletes the temporary file
spinner.succeed("Secrets set successfully");
}
} catch (err) {
spinner.fail(`Error setting secrets: ${err}`);
process.exit(1);
}
}

spinner.start("Setting up KV namespace");
try {
const kvNamespace = selectedEnv ? `${selectedEnv}-plugin-chain-state` : `plugin-chain-state`;
const namespaceId = wranglerKvNamespace(wranglerToml.name, kvNamespace);
if (selectedEnv) {
const existingBinding = wranglerToml.env[selectedEnv]?.kv_namespaces?.find((o) => o.binding === BINDING_NAME);
if (!existingBinding) {
wranglerToml.env[selectedEnv] = wranglerToml.env[selectedEnv] ?? {};
wranglerToml.env[selectedEnv].kv_namespaces = wranglerToml.env[selectedEnv].kv_namespaces ?? [];
wranglerToml.env[selectedEnv].kv_namespaces?.push({
id: namespaceId,
binding: BINDING_NAME,
});
} else {
existingBinding.id = namespaceId;
}
} else {
const existingBinding = wranglerToml.kv_namespaces.find((o) => o.binding === BINDING_NAME);
if (!existingBinding) {
wranglerToml.kv_namespaces.push({
id: namespaceId,
binding: BINDING_NAME,
});
} else {
existingBinding.id = namespaceId;
}
}
writeFileSync(WRANGLER_TOML_PATH, tomlify.toToml(wranglerToml));
spinner.succeed(`Using KV namespace ${kvNamespace} with ID: ${namespaceId}`);
} catch (err) {
spinner.fail(`Error setting up KV namespace: ${err}`);
process.exit(1);
}

spinner.start("Deploying to Cloudflare Workers").stopAndPersist();
try {
await wranglerDeploy(selectedEnv);
spinner.succeed("Deployed successfully");
} catch (err) {
spinner.fail(`Error deploying: ${err}`);
process.exit(1);
}
})();
39 changes: 39 additions & 0 deletions scripts/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Register a Github App</title>
<style>
body,
html {
height: 100%;
margin: 0;
display: flex;
justify-content: center;
align-items: center;
}
.form-container {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
flex-direction: column;
}
</style>
</head>
<body>
<div class="form-container">
<p>Click on the button and follow instructions</p>
<form action="https://github.com/settings/apps/new" method="post">
<input type="hidden" name="manifest" id="manifest" /><br />
<input type="submit" value="Register a GitHub App" />
</form>
</div>

<script>
const input = document.getElementById("manifest");
input.value = `{{ MANIFEST }}`;
</script>
</body>
</html>
34 changes: 34 additions & 0 deletions scripts/redirect.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Register a Github App</title>
<style>
body,
html {
height: 100%;
margin: 0;
display: flex;
justify-content: center;
align-items: center;
}
.form-container {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
flex-direction: column;
}
</style>
</head>
<body>
<div class="form-container">
<p>Successfully created a Github App!</p>

<form action="{{ APP_URL }}" method="GET">
<input type="submit" value="Install App" />
</form>
</div>
</body>
</html>
File renamed without changes.
Loading

0 comments on commit 6916ad3

Please sign in to comment.