Merge pull request #273 from coder/mes/readme-update

fix: add missing README information and clean up types
pull/281/head
Michael Smith 9 months ago committed by GitHub
commit 310d0262bd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -1,24 +1,47 @@
# Contributing # Contributing
To create a new module, clone this repository and run: ## Getting started
This repo uses the [Bun runtime](https://bun.sh/) to to run all code and tests. To install Bun, you can run this command on Linux/MacOS:
```shell
curl -fsSL https://bun.sh/install | bash
```
Or this command on Windows:
```shell ```shell
./new.sh MODULE_NAME powershell -c "irm bun.sh/install.ps1 | iex"
```
Follow the instructions to ensure that Bun is available globally. Once Bun has been installed, clone this repository. From there, run this script to create a new module:
```shell
./new.sh NAME_OF_NEW_MODULE
``` ```
## Testing a Module ## Testing a Module
> **Note:** It is the responsibility of the module author to implement tests for their module. The author must test the module locally before submitting a PR.
A suite of test-helpers exists to run `terraform apply` on modules with variables, and test script output against containers. A suite of test-helpers exists to run `terraform apply` on modules with variables, and test script output against containers.
The testing suite must be able to run docker containers with the `--network=host` flag, which typically requires running the tests on Linux as this flag does not apply to Docker Desktop for MacOS and Windows. MacOS users can work around this by using something like [colima](https://github.com/abiosoft/colima) or [Orbstack](https://orbstack.dev/) instead of Docker Desktop. The testing suite must be able to run docker containers with the `--network=host` flag. This typically requires running the tests on Linux as this flag does not apply to Docker Desktop for MacOS and Windows. MacOS users can work around this by using something like [colima](https://github.com/abiosoft/colima) or [Orbstack](https://orbstack.dev/) instead of Docker Desktop.
Reference the existing `*.test.ts` files to get an idea for how to set up tests.
Reference existing `*.test.ts` files for implementation. You can run all tests in a specific file with this command:
```shell ```shell
# Run tests for a specific module!
$ bun test -t '<module>' $ bun test -t '<module>'
``` ```
Or run all tests by running this command:
```shell
$ bun test
```
You can test a module locally by updating the source as follows You can test a module locally by updating the source as follows
```tf ```tf
@ -27,4 +50,25 @@ module "example" {
} }
``` ```
> **Note:** This is the responsibility of the module author to implement tests for their module. and test the module locally before submitting a PR. ## Releases
> [!WARNING]
> When creating a new release, make sure that your new version number is fully accurate. If a version number is incorrect or does not exist, we may end up serving incorrect/old data for our various tools and providers.
Much of our release process is automated. To cut a new release:
1. Navigate to [GitHub's Releases page](https://github.com/coder/modules/releases)
2. Click "Draft a new release"
3. Click the "Choose a tag" button and type a new release number in the format `v<major>.<minor>.<patch>` (e.g., `v1.18.0`). Then click "Create new tag".
4. Click the "Generate release notes" button, and clean up the resulting README. Be sure to remove any notes that would not be relevant to end-users (e.g., bumping dependencies).
5. Once everything looks good, click the "Publish release" button.
Once the release has been cut, a script will run to check whether there are any modules that will require that the new release number be published to Terraform. If there are any, a new pull request will automatically be generated. Be sure to approve this PR and merge it into the `main` branch.
Following that, our automated processes will handle publishing new data for [`registry.coder.com`](https://github.com/coder/registry.coder.com/):
1. Publishing new versions to Coder's [Terraform Registry](https://registry.terraform.io/providers/coder/coder/latest)
2. Publishing new data to the [Coder Registry](https://registry.coder.com)
> [!NOTE]
> Some data in `registry.coder.com` is fetched on demand from the Module repo's main branch. This data should be updated almost immediately after a new release, but other changes will take some time to propagate.

@ -3,14 +3,14 @@
Modules Modules
</h1> </h1>
[Registry](https://registry.coder.com) | [Coder Docs](https://coder.com/docs) | [Why Coder](https://coder.com/why) | [Coder Enterprise](https://coder.com/docs/v2/latest/enterprise) [Module Registry](https://registry.coder.com) | [Coder Docs](https://coder.com/docs) | [Why Coder](https://coder.com/why) | [Coder Enterprise](https://coder.com/docs/v2/latest/enterprise)
[![discord](https://img.shields.io/discord/747933592273027093?label=discord)](https://discord.gg/coder) [![discord](https://img.shields.io/discord/747933592273027093?label=discord)](https://discord.gg/coder)
[![license](https://img.shields.io/github/license/coder/modules)](./LICENSE) [![license](https://img.shields.io/github/license/coder/modules)](./LICENSE)
</div> </div>
Modules extend Templates to create reusable components for your development environment. Modules extend Coder Templates to create reusable components for your development environment.
e.g. e.g.

@ -5,14 +5,15 @@ import grayMatter from "gray-matter";
const files = await readdir(".", { withFileTypes: true }); const files = await readdir(".", { withFileTypes: true });
const dirs = files.filter( const dirs = files.filter(
(f) => f.isDirectory() && !f.name.startsWith(".") && f.name !== "node_modules" (f) =>
f.isDirectory() && !f.name.startsWith(".") && f.name !== "node_modules",
); );
let badExit = false; let badExit = false;
// error reports an error to the console and sets badExit to true // error reports an error to the console and sets badExit to true
// so that the process will exit with a non-zero exit code. // so that the process will exit with a non-zero exit code.
const error = (...data: any[]) => { const error = (...data: unknown[]) => {
console.error(...data); console.error(...data);
badExit = true; badExit = true;
}; };
@ -22,7 +23,7 @@ const verifyCodeBlocks = (
res = { res = {
codeIsTF: false, codeIsTF: false,
codeIsHCL: false, codeIsHCL: false,
} },
) => { ) => {
for (const token of tokens) { for (const token of tokens) {
// Check in-depth. // Check in-depth.
@ -30,7 +31,12 @@ const verifyCodeBlocks = (
verifyCodeBlocks(token.items, res); verifyCodeBlocks(token.items, res);
continue; continue;
} }
if (token.type === "list_item") { if (token.type === "list_item") {
if (token.tokens === undefined) {
throw new Error("Tokens are missing for type list_item");
}
verifyCodeBlocks(token.tokens, res); verifyCodeBlocks(token.tokens, res);
continue; continue;
} }
@ -80,8 +86,9 @@ for (const dir of dirs) {
if (!data.maintainer_github) { if (!data.maintainer_github) {
error(dir.name, "missing maintainer_github"); error(dir.name, "missing maintainer_github");
} }
try { try {
await stat(path.join(".", dir.name, data.icon)); await stat(path.join(".", dir.name, data.icon ?? ""));
} catch (ex) { } catch (ex) {
error(dir.name, "icon does not exist", data.icon); error(dir.name, "icon does not exist", data.icon);
} }

@ -126,7 +126,10 @@ const assertSlackMessage = async (opts: {
durationMS?: number; durationMS?: number;
output: string; output: string;
}) => { }) => {
let url: URL; // Have to use non-null assertion because TS can't tell when the fetch
// function will run
let url!: URL;
const fakeSlackHost = serve({ const fakeSlackHost = serve({
fetch: (req) => { fetch: (req) => {
url = new URL(req.url); url = new URL(req.url);
@ -138,15 +141,16 @@ const assertSlackMessage = async (opts: {
}, },
port: 0, port: 0,
}); });
const { instance, id } = await setupContainer( const { instance, id } = await setupContainer(
"alpine/curl", "alpine/curl",
opts.format && { opts.format ? { slack_message: opts.format } : undefined,
slack_message: opts.format,
},
); );
await writeCoder(id, "echo 'token'"); await writeCoder(id, "echo 'token'");
let exec = await execContainer(id, ["sh", "-c", instance.script]); let exec = await execContainer(id, ["sh", "-c", instance.script]);
expect(exec.exitCode).toBe(0); expect(exec.exitCode).toBe(0);
exec = await execContainer(id, [ exec = await execContainer(id, [
"sh", "sh",
"-c", "-c",
@ -154,6 +158,7 @@ const assertSlackMessage = async (opts: {
fakeSlackHost.hostname fakeSlackHost.hostname
}:${fakeSlackHost.port}" slackme ${opts.command}`, }:${fakeSlackHost.port}" slackme ${opts.command}`,
]); ]);
expect(exec.stderr.trim()).toBe(""); expect(exec.stderr.trim()).toBe("");
expect(url.pathname).toEqual("/api/chat.postMessage"); expect(url.pathname).toEqual("/api/chat.postMessage");
expect(url.searchParams.get("channel")).toEqual("token"); expect(url.searchParams.get("channel")).toEqual("token");

@ -90,17 +90,21 @@ type TerraformStateResource = {
type: string; type: string;
name: string; name: string;
provider: string; provider: string;
instances: [{ attributes: Record<string, any> }];
instances: [
{
attributes: Record<string, JsonValue>;
},
];
}; };
export interface TerraformState { type TerraformOutput = {
outputs: { type: string;
[key: string]: { value: JsonValue;
type: string; };
value: any;
};
};
export interface TerraformState {
outputs: Record<string, TerraformOutput>;
resources: [TerraformStateResource, ...TerraformStateResource[]]; resources: [TerraformStateResource, ...TerraformStateResource[]];
} }
@ -149,19 +153,25 @@ export const testRequiredVariables = <TVars extends Record<string, string>>(
it("required variables", async () => { it("required variables", async () => {
await runTerraformApply(dir, vars); await runTerraformApply(dir, vars);
}); });
const varNames = Object.keys(vars); const varNames = Object.keys(vars);
varNames.forEach((varName) => { varNames.forEach((varName) => {
// Ensures that every variable provided is required! // Ensures that every variable provided is required!
it("missing variable " + varName, async () => { it("missing variable " + varName, async () => {
const localVars = {}; const localVars: Record<string, string> = {};
varNames.forEach((otherVarName) => { varNames.forEach((otherVarName) => {
if (otherVarName !== varName) { if (otherVarName !== varName) {
localVars[otherVarName] = vars[otherVarName]; localVars[otherVarName] = vars[otherVarName];
} }
}); });
try { try {
await runTerraformApply(dir, localVars); await runTerraformApply(dir, localVars);
} catch (ex) { } catch (ex) {
if (!(ex instanceof Error)) {
throw new Error("Unknown error generated");
}
expect(ex.message).toContain( expect(ex.message).toContain(
`input variable \"${varName}\" is not set`, `input variable \"${varName}\" is not set`,
); );

@ -2,6 +2,7 @@
"compilerOptions": { "compilerOptions": {
"target": "esnext", "target": "esnext",
"module": "esnext", "module": "esnext",
"strict": true,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"moduleResolution": "nodenext", "moduleResolution": "nodenext",
"types": ["bun-types"] "types": ["bun-types"]

@ -24,9 +24,10 @@ describe("vscode-desktop", async () => {
const coder_app = state.resources.find( const coder_app = state.resources.find(
(res) => res.type == "coder_app" && res.name == "vscode", (res) => res.type == "coder_app" && res.name == "vscode",
); );
expect(coder_app).not.toBeNull(); expect(coder_app).not.toBeNull();
expect(coder_app.instances.length).toBe(1); expect(coder_app?.instances.length).toBe(1);
expect(coder_app.instances[0].attributes.order).toBeNull(); expect(coder_app?.instances[0].attributes.order).toBeNull();
}); });
it("adds folder", async () => { it("adds folder", async () => {
@ -80,8 +81,9 @@ describe("vscode-desktop", async () => {
const coder_app = state.resources.find( const coder_app = state.resources.find(
(res) => res.type == "coder_app" && res.name == "vscode", (res) => res.type == "coder_app" && res.name == "vscode",
); );
expect(coder_app).not.toBeNull(); expect(coder_app).not.toBeNull();
expect(coder_app.instances.length).toBe(1); expect(coder_app?.instances.length).toBe(1);
expect(coder_app.instances[0].attributes.order).toBe(22); expect(coder_app?.instances[0].attributes.order).toBe(22);
}); });
}); });

@ -24,7 +24,10 @@ function findWindowsRdpScript(state: TerraformState): string | null {
} }
for (const instance of resource.instances) { for (const instance of resource.instances) {
if (instance.attributes.display_name === "windows-rdp") { if (
instance.attributes.display_name === "windows-rdp" &&
typeof instance.attributes.script === "string"
) {
return instance.attributes.script; return instance.attributes.script;
} }
} }
@ -100,11 +103,11 @@ describe("Web RDP", async () => {
const defaultRdpScript = findWindowsRdpScript(defaultState); const defaultRdpScript = findWindowsRdpScript(defaultState);
expect(defaultRdpScript).toBeString(); expect(defaultRdpScript).toBeString();
const { username: defaultUsername, password: defaultPassword } = const defaultResultsGroup =
formEntryValuesRe.exec(defaultRdpScript)?.groups ?? {}; formEntryValuesRe.exec(defaultRdpScript ?? "")?.groups ?? {};
expect(defaultUsername).toBe("Administrator"); expect(defaultResultsGroup.username).toBe("Administrator");
expect(defaultPassword).toBe("coderRDP!"); expect(defaultResultsGroup.password).toBe("coderRDP!");
// Test that custom usernames/passwords are also forwarded correctly // Test that custom usernames/passwords are also forwarded correctly
const customAdminUsername = "crouton"; const customAdminUsername = "crouton";
@ -122,10 +125,10 @@ describe("Web RDP", async () => {
const customRdpScript = findWindowsRdpScript(customizedState); const customRdpScript = findWindowsRdpScript(customizedState);
expect(customRdpScript).toBeString(); expect(customRdpScript).toBeString();
const { username: customUsername, password: customPassword } = const customResultsGroup =
formEntryValuesRe.exec(customRdpScript)?.groups ?? {}; formEntryValuesRe.exec(customRdpScript ?? "")?.groups ?? {};
expect(customUsername).toBe(customAdminUsername); expect(customResultsGroup.username).toBe(customAdminUsername);
expect(customPassword).toBe(customAdminPassword); expect(customResultsGroup.password).toBe(customAdminPassword);
}); });
}); });

Loading…
Cancel
Save