Skip to content

Commit 0cb200a

Browse files
kevinwang5658github-actions[bot]
authored andcommitted
feat: Add go-version-manager resource (auto-generated from issue #34)
1 parent 7bae275 commit 0cb200a

8 files changed

Lines changed: 327 additions & 0 deletions

File tree

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
---
2+
title: goenv
3+
description: A reference page for the goenv resource
4+
---
5+
6+
The goenv resource installs [goenv](https://github.com/go-nv/goenv), a Go version manager modelled after pyenv and rbenv that lets you install and switch between multiple Go versions. On macOS it is installed via Homebrew; on Linux it is cloned from GitHub into `~/.goenv`.
7+
8+
## Parameters:
9+
10+
- **goVersions**: *(array[string])* Go versions to install via goenv (e.g. `["1.22.0", "1.21.5"]`). Versions must match those available from `goenv install --list`. Codify adds missing versions and removes versions that are no longer listed.
11+
12+
- **global**: *(string)* The Go version to set as the global default (equivalent to running `goenv global <version>`). The version must be in `goVersions` or already installed on the system.
13+
14+
## Example usage:
15+
16+
### Install goenv with a single Go version
17+
18+
```json title="codify.jsonc"
19+
[
20+
{
21+
"type": "goenv",
22+
"goVersions": ["1.22.0"],
23+
"global": "1.22.0"
24+
}
25+
]
26+
```
27+
28+
### Install goenv with multiple Go versions
29+
30+
```json title="codify.jsonc"
31+
[
32+
{
33+
"type": "goenv",
34+
"goVersions": ["1.21.0", "1.22.0", "1.23.0"],
35+
"global": "1.23.0"
36+
}
37+
]
38+
```
39+
40+
### Install goenv without installing any Go versions
41+
42+
```json title="codify.jsonc"
43+
[
44+
{
45+
"type": "goenv"
46+
}
47+
]
48+
```
49+
50+
## Notes:
51+
52+
- On macOS, Homebrew must be installed before applying the goenv resource. The [homebrew](/docs/resources/package-managers/homebrew) resource can install it. The `mercurial` and `bison` packages are installed automatically as part of the goenv install.
53+
- On Linux, several system dependencies are required (`curl`, `git`, `mercurial`, `make`, `binutils`, `bison`, `gcc`, `build-essential`). Codify installs these automatically via the system package manager before cloning goenv.
54+
- On Linux, goenv is cloned to `~/.goenv` and added to `PATH` in your shell RC file. On macOS, Homebrew manages the installation path.
55+
- After applying, open a new terminal session (or source your shell RC file) for the `goenv` shims and `GOROOT`/`GOPATH` environment variables to become active.
56+
- To find available Go versions, run `goenv install --list` after installing goenv.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"title": "go",
3+
"pages": ["goenv"]
4+
}

docs/resources/index.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ Install and manage multiple versions of programming languages:
3333
- **[rbenv](/docs/resources/ruby/rbenv)** - Ruby version management
3434
- **[jenv](/docs/resources/jenv)** - Java version management
3535
- **[asdf](/docs/resources/asdf/asdf)** - Universal version manager for multiple languages
36+
- **[goenv](/docs/resources/go/goenv)** - Go version management
3637

3738
### Programming Languages & Tools
3839

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { AsdfPluginResource } from './resources/asdf/asdf-plugin.js';
77
import { AwsCliResource } from './resources/aws-cli/cli/aws-cli.js';
88
import { AwsProfileResource } from './resources/aws-cli/profile/aws-profile.js';
99
import { DnfResource } from './resources/dnf/dnf.js';
10+
import { GoenvResource } from './resources/go/goenv/goenv.js';
1011
import { DockerResource } from './resources/docker/docker.js';
1112
import { FileResource } from './resources/file/file.js';
1213
import { RemoteFileResource } from './resources/file/remote-file.js';
@@ -73,6 +74,7 @@ runPlugin(Plugin.create(
7374
new TerraformResource(),
7475
new NvmResource(),
7576
new JenvResource(),
77+
new GoenvResource(),
7678
new PgcliResource(),
7779
new VscodeResource(),
7880
new GitRepositoryResource(),
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { getPty, ParameterSetting, SpawnStatus, StatefulParameter } from '@codifycli/plugin-core';
2+
3+
import { GoenvConfig } from './goenv.js';
4+
5+
export class GoenvGlobalParameter extends StatefulParameter<GoenvConfig, string> {
6+
getSettings(): ParameterSetting {
7+
return {
8+
type: 'version',
9+
};
10+
}
11+
12+
override async refresh(): Promise<string | null> {
13+
const $ = getPty();
14+
const { data, status } = await $.spawnSafe('goenv global');
15+
if (status === SpawnStatus.ERROR) {
16+
return null;
17+
}
18+
return parseGlobalVersion(data);
19+
}
20+
21+
override async add(valueToAdd: string): Promise<void> {
22+
const $ = getPty();
23+
await $.spawn(`goenv global ${valueToAdd}`, { interactive: true });
24+
}
25+
26+
override async modify(newValue: string): Promise<void> {
27+
const $ = getPty();
28+
await $.spawn(`goenv global ${newValue}`, { interactive: true });
29+
}
30+
31+
override async remove(): Promise<void> {
32+
const $ = getPty();
33+
await $.spawn('goenv global system', { interactive: true });
34+
}
35+
}
36+
37+
function parseGlobalVersion(output: string): string | null {
38+
const version = output.trim();
39+
return version === 'system' ? null : version;
40+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { ArrayStatefulParameter, getPty } from '@codifycli/plugin-core';
2+
3+
import { GoenvConfig } from './goenv.js';
4+
5+
export class GoVersionsParameter extends ArrayStatefulParameter<GoenvConfig, string> {
6+
override async refresh(_desired: string[] | null): Promise<string[] | null> {
7+
const $ = getPty();
8+
const { data } = await $.spawnSafe('goenv versions --bare');
9+
return parseInstalledVersions(data);
10+
}
11+
12+
override async addItem(version: string): Promise<void> {
13+
const $ = getPty();
14+
await $.spawn(`goenv install ${version}`, { interactive: true });
15+
}
16+
17+
override async removeItem(version: string): Promise<void> {
18+
const $ = getPty();
19+
await $.spawn(`goenv uninstall --force ${version}`, { interactive: true });
20+
}
21+
}
22+
23+
function parseInstalledVersions(output: string): string[] {
24+
return output
25+
.split('\n')
26+
.map((line) => line.trim())
27+
.filter(Boolean);
28+
}

src/resources/go/goenv/goenv.ts

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import {
2+
ExampleConfig,
3+
FileUtils,
4+
getPty,
5+
Resource,
6+
ResourceSettings,
7+
SpawnStatus,
8+
Utils,
9+
z,
10+
} from '@codifycli/plugin-core';
11+
import { OS } from '@codifycli/schemas';
12+
import os from 'node:os';
13+
import path from 'node:path';
14+
15+
import { GoenvGlobalParameter } from './global-parameter.js';
16+
import { GoVersionsParameter } from './go-versions-parameter.js';
17+
18+
const GOENV_ROOT = path.join(os.homedir(), '.goenv');
19+
const GOENV_ROOT_EXPORT = 'export GOENV_ROOT="$HOME/.goenv"';
20+
const GOENV_PATH_EXPORT = 'export PATH="$GOENV_ROOT/bin:$PATH"';
21+
const GOENV_INIT = 'eval "$(goenv init -)"';
22+
23+
const schema = z
24+
.object({
25+
goVersions: z
26+
.array(z.string())
27+
.describe('Go versions to install via goenv (e.g. ["1.22.0", "1.21.5"])')
28+
.optional(),
29+
global: z
30+
.string()
31+
.describe('The global Go version set by goenv.')
32+
.optional(),
33+
})
34+
.describe('goenv resource — install and manage multiple Go versions');
35+
36+
export type GoenvConfig = z.infer<typeof schema>;
37+
38+
const defaultConfig: Partial<GoenvConfig> = {
39+
goVersions: [],
40+
};
41+
42+
const exampleBasic: ExampleConfig = {
43+
title: 'Install goenv with a global Go version',
44+
description: 'Install goenv, download Go 1.22.0, and set it as the default global version.',
45+
configs: [
46+
{
47+
type: 'goenv',
48+
goVersions: ['1.22.0'],
49+
global: '1.22.0',
50+
},
51+
],
52+
};
53+
54+
const exampleMultiVersion: ExampleConfig = {
55+
title: 'Manage multiple Go versions',
56+
description: 'Install goenv with multiple Go versions for cross-version testing, setting the latest as the default.',
57+
configs: [
58+
{
59+
type: 'goenv',
60+
goVersions: ['1.21.0', '1.22.0', '1.23.0'],
61+
global: '1.23.0',
62+
},
63+
],
64+
};
65+
66+
export class GoenvResource extends Resource<GoenvConfig> {
67+
getSettings(): ResourceSettings<GoenvConfig> {
68+
return {
69+
id: 'goenv',
70+
defaultConfig,
71+
exampleConfigs: {
72+
example1: exampleBasic,
73+
example2: exampleMultiVersion,
74+
},
75+
operatingSystems: [OS.Darwin, OS.Linux],
76+
schema,
77+
parameterSettings: {
78+
goVersions: { type: 'stateful', definition: new GoVersionsParameter(), order: 1 },
79+
global: { type: 'stateful', definition: new GoenvGlobalParameter(), order: 2 },
80+
},
81+
};
82+
}
83+
84+
override async refresh(): Promise<Partial<GoenvConfig> | null> {
85+
const $ = getPty();
86+
const { status } = await $.spawnSafe('goenv --version');
87+
if (status === SpawnStatus.SUCCESS) return {};
88+
89+
// goenv may be installed via git clone but not yet on PATH in the current session
90+
const { status: binStatus } = await $.spawnSafe(
91+
`test -f ${path.join(GOENV_ROOT, 'bin', 'goenv')}`
92+
);
93+
return binStatus === SpawnStatus.SUCCESS ? {} : null;
94+
}
95+
96+
override async create(): Promise<void> {
97+
if (Utils.isMacOS()) {
98+
await installOnMacOS();
99+
} else {
100+
await installOnLinux();
101+
}
102+
}
103+
104+
override async destroy(): Promise<void> {
105+
if (Utils.isMacOS()) {
106+
await uninstallOnMacOS();
107+
} else {
108+
await uninstallOnLinux();
109+
}
110+
}
111+
}
112+
113+
async function installOnMacOS(): Promise<void> {
114+
const $ = getPty();
115+
await $.spawn('brew install goenv mercurial bison', {
116+
interactive: true,
117+
env: { HOMEBREW_NO_AUTO_UPDATE: '1' },
118+
});
119+
await FileUtils.addToShellRc(GOENV_INIT);
120+
}
121+
122+
async function installOnLinux(): Promise<void> {
123+
await Utils.installViaPkgMgr(
124+
'curl git mercurial make binutils bison gcc build-essential'
125+
);
126+
127+
const $ = getPty();
128+
await $.spawnSafe(`git clone https://github.com/go-nv/goenv.git ${GOENV_ROOT}`, {
129+
interactive: true,
130+
});
131+
132+
await FileUtils.addAllToShellRc([GOENV_ROOT_EXPORT, GOENV_PATH_EXPORT, GOENV_INIT]);
133+
}
134+
135+
async function uninstallOnMacOS(): Promise<void> {
136+
const $ = getPty();
137+
await $.spawnSafe('brew uninstall goenv', {
138+
env: { HOMEBREW_NO_AUTO_UPDATE: '1' },
139+
});
140+
await removeGoenvFromShellRc([GOENV_INIT]);
141+
}
142+
143+
async function uninstallOnLinux(): Promise<void> {
144+
const $ = getPty();
145+
await $.spawnSafe(`rm -rf ${GOENV_ROOT}`);
146+
await removeGoenvFromShellRc([GOENV_ROOT_EXPORT, GOENV_PATH_EXPORT, GOENV_INIT]);
147+
}
148+
149+
async function removeGoenvFromShellRc(lines: string[]): Promise<void> {
150+
const shellRc = Utils.getPrimaryShellRc();
151+
if (!(await FileUtils.fileExists(shellRc))) {
152+
return;
153+
}
154+
for (const line of lines) {
155+
await FileUtils.removeLineFromShellRc(line);
156+
}
157+
}

test/go/goenv.test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { SpawnStatus } from '@codifycli/plugin-core';
2+
import { PluginTester, testSpawn } from '@codifycli/plugin-test';
3+
import { describe, expect, it } from 'vitest';
4+
import * as fs from 'node:fs';
5+
import * as path from 'node:path';
6+
7+
import { TestUtils } from '../test-utils.js';
8+
9+
describe('Goenv resource integration tests', () => {
10+
const pluginPath = path.resolve('./src/index.ts');
11+
12+
it('Installs goenv, installs a Go version, and sets a global', { timeout: 600000 }, async () => {
13+
await PluginTester.fullTest(pluginPath, [
14+
{
15+
type: 'goenv',
16+
goVersions: ['1.22.0'],
17+
global: '1.22.0',
18+
},
19+
], {
20+
validateApply: async () => {
21+
const goenvCheck = await testSpawn('goenv --version');
22+
expect(goenvCheck.status).toBe(SpawnStatus.SUCCESS);
23+
24+
const { data: versions } = await testSpawn('goenv versions');
25+
expect(versions).toContain('1.22.0');
26+
27+
const { data: globalVersion } = await testSpawn('goenv global');
28+
expect(globalVersion.trim()).toBe('1.22.0');
29+
},
30+
validateDestroy: () => {
31+
const shellRc = TestUtils.getPrimaryShellRc();
32+
if (fs.existsSync(shellRc)) {
33+
const shellRcContents = fs.readFileSync(shellRc, 'utf-8');
34+
expect(shellRcContents).not.toContain('goenv init');
35+
}
36+
},
37+
});
38+
});
39+
});

0 commit comments

Comments
 (0)