Expose go env variables as action outputs (#791)

* feature implementation - output go env

* update action.yml

* fix action.yml

---------

Co-authored-by: mahabaleshwars <147705296+mahabaleshwars@users.noreply.github.com>
This commit is contained in:
v-mahabaleshwars
2026-09-18 20:35:51 +05:30
committed by GitHub
parent 468e940882
commit e626ada899
8 changed files with 310 additions and 2 deletions
+25
View File
@@ -43,6 +43,31 @@ jobs:
- name: Verify Go
run: go version
go-env-outputs:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Go
id: setup-go
uses: ./
with:
go-version: stable
- name: Show Go environment outputs
run: |
echo "go-path: ${{ steps.setup-go.outputs.go-path }}"
echo "go-bin: ${{ steps.setup-go.outputs.go-bin }}"
echo "go-bin-path: ${{ steps.setup-go.outputs.go-bin-path }}"
echo "go-root: ${{ steps.setup-go.outputs.go-root }}"
echo "go-cache: ${{ steps.setup-go.outputs.go-cache }}"
echo "go-mod-cache: ${{ steps.setup-go.outputs.go-mod-cache }}"
echo "go-os: ${{ steps.setup-go.outputs.go-os }}"
echo "go-arch: ${{ steps.setup-go.outputs.go-arch }}"
echo "go-tool-dir: ${{ steps.setup-go.outputs.go-tool-dir }}"
aliases-arch:
runs-on: ${{ matrix.os }}
strategy:
+104
View File
@@ -28,6 +28,8 @@ jest.unstable_mockModule('@actions/core', () => ({
getBooleanInput: jest.fn(),
info: jest.fn(),
debug: jest.fn(),
warning: jest.fn(),
setOutput: jest.fn(),
exportVariable: jest.fn()
}));
@@ -110,6 +112,8 @@ describe('setup-go', () => {
let findSpy: jest.Mock;
let cnSpy: jest.SpiedFunction<typeof process.stdout.write>;
let logSpy: jest.Mock;
let setOutputSpy: jest.Mock<typeof core.setOutput>;
let warningSpy: jest.Mock<typeof core.warning>;
let getSpy: jest.Mock;
let platSpy: jest.Mock;
let archSpy: jest.Mock;
@@ -198,6 +202,8 @@ describe('setup-go', () => {
// writes
cnSpy = jest.spyOn(process.stdout, 'write');
logSpy = core.info as jest.Mock;
setOutputSpy = core.setOutput as jest.Mock<typeof core.setOutput>;
warningSpy = core.warning as jest.Mock<typeof core.warning>;
dbgSpy = core.debug as jest.Mock;
getSpy.mockImplementation(() => goJsonData as IGoVersion[] | null);
cnSpy.mockImplementation(() => true);
@@ -646,6 +652,104 @@ describe('setup-go', () => {
expect(added).toBeTruthy();
});
describe('go env outputs', () => {
const goEnv = {
GOPATH: '/Users/testuser/go',
GOBIN: '',
GOROOT: '/usr/local/go',
GOCACHE: '/Users/testuser/.cache/go-build',
GOMODCACHE: '/Users/testuser/go/pkg/mod',
GOOS: 'darwin',
GOARCH: 'arm64',
GOTOOLDIR: '/usr/local/go/pkg/tool/darwin_arm64'
};
it('parses the output of go env -json', () => {
execFileSpy.mockImplementation(() => JSON.stringify(goEnv));
expect(main.readGoEnv('/usr/local/go/bin/go')).toEqual(goEnv);
});
it('returns undefined when go env -json is not supported', () => {
execFileSpy.mockImplementation(() => {
throw new Error('flag provided but not defined: -json');
});
expect(main.readGoEnv('/usr/local/go/bin/go')).toBeUndefined();
expect(logSpy).toHaveBeenCalledWith(
expect.stringContaining('flag provided but not defined: -json')
);
expect(warningSpy).not.toHaveBeenCalled();
});
it('returns undefined when the output is not valid JSON', () => {
execFileSpy.mockImplementation(() => 'GOPATH="/Users/testuser/go"');
expect(main.readGoEnv('/usr/local/go/bin/go')).toBeUndefined();
});
it('returns undefined when the output is not a JSON object', () => {
execFileSpy.mockImplementation(() => '[]');
expect(main.readGoEnv('/usr/local/go/bin/go')).toBeUndefined();
});
it('sets an output for each exposed variable', () => {
main.setGoEnvOutputs(goEnv);
expect(setOutputSpy).toHaveBeenCalledWith('go-path', goEnv.GOPATH);
expect(setOutputSpy).toHaveBeenCalledWith('go-bin', '');
expect(setOutputSpy).toHaveBeenCalledWith('go-root', goEnv.GOROOT);
expect(setOutputSpy).toHaveBeenCalledWith('go-cache', goEnv.GOCACHE);
expect(setOutputSpy).toHaveBeenCalledWith(
'go-mod-cache',
goEnv.GOMODCACHE
);
expect(setOutputSpy).toHaveBeenCalledWith('go-os', goEnv.GOOS);
expect(setOutputSpy).toHaveBeenCalledWith('go-arch', goEnv.GOARCH);
expect(setOutputSpy).toHaveBeenCalledWith('go-tool-dir', goEnv.GOTOOLDIR);
});
it('falls back to $GOPATH/bin when GOBIN is empty', () => {
main.setGoEnvOutputs(goEnv);
expect(setOutputSpy).toHaveBeenCalledWith(
'go-bin-path',
'/Users/testuser/go/bin'
);
});
it('prefers GOBIN over $GOPATH/bin when it is set', () => {
main.setGoEnvOutputs({...goEnv, GOBIN: '/Users/testuser/bin'});
expect(setOutputSpy).toHaveBeenCalledWith(
'go-bin-path',
'/Users/testuser/bin'
);
});
it('uses the first entry of a multi-entry GOPATH', () => {
const GOPATH = ['/Users/testuser/go', '/Users/testuser/other'].join(
path.delimiter
);
main.setGoEnvOutputs({...goEnv, GOPATH});
expect(setOutputSpy).toHaveBeenCalledWith('go-path', GOPATH);
expect(setOutputSpy).toHaveBeenCalledWith(
'go-bin-path',
'/Users/testuser/go/bin'
);
});
it('leaves variables missing from go env -json empty', () => {
main.setGoEnvOutputs({});
expect(setOutputSpy).toHaveBeenCalledWith('go-path', '');
expect(setOutputSpy).toHaveBeenCalledWith('go-bin-path', '');
});
});
interface Annotation {
file: string;
line: number;
+18
View File
@@ -26,6 +26,24 @@ outputs:
description: 'The installed Go version. Useful when given a version range as input.'
cache-hit:
description: 'A boolean value to indicate if a cache was hit'
go-path:
description: 'The value of `go env GOPATH`.'
go-bin:
description: 'The value of `go env GOBIN`.'
go-bin-path:
description: 'The base Go binary directory: `GOBIN`, otherwise `bin` under the first `GOPATH` entry.'
go-root:
description: 'The value of `go env GOROOT`.'
go-cache:
description: 'The value of `go env GOCACHE`, the build cache directory.'
go-mod-cache:
description: 'The value of `go env GOMODCACHE`, the module cache directory.'
go-os:
description: 'The value of `go env GOOS`, the target OS in Go notation (linux, darwin, windows).'
go-arch:
description: 'The value of `go env GOARCH`, the target architecture in Go notation (amd64, arm64).'
go-tool-dir:
description: 'The value of `go env GOTOOLDIR`, the directory holding the toolchain binaries.'
runs:
using: 'node24'
main: 'dist/setup/index.js'
+9
View File
@@ -95641,6 +95641,15 @@ var State;
var Outputs;
(function (Outputs) {
Outputs["CacheHit"] = "cache-hit";
Outputs["GoPath"] = "go-path";
Outputs["GoBin"] = "go-bin";
Outputs["GoBinPath"] = "go-bin-path";
Outputs["GoRoot"] = "go-root";
Outputs["GoCache"] = "go-cache";
Outputs["GoModCache"] = "go-mod-cache";
Outputs["GoOs"] = "go-os";
Outputs["GoArch"] = "go-arch";
Outputs["GoToolDir"] = "go-tool-dir";
})(Outputs || (Outputs = {}));
;// CONCATENATED MODULE: ./src/package-managers.ts
+53
View File
@@ -100441,6 +100441,15 @@ var State;
var Outputs;
(function (Outputs) {
Outputs["CacheHit"] = "cache-hit";
Outputs["GoPath"] = "go-path";
Outputs["GoBin"] = "go-bin";
Outputs["GoBinPath"] = "go-bin-path";
Outputs["GoRoot"] = "go-root";
Outputs["GoCache"] = "go-cache";
Outputs["GoModCache"] = "go-mod-cache";
Outputs["GoOs"] = "go-os";
Outputs["GoArch"] = "go-arch";
Outputs["GoToolDir"] = "go-tool-dir";
})(Outputs || (Outputs = {}));
;// CONCATENATED MODULE: ./src/package-managers.ts
@@ -100567,6 +100576,7 @@ const findDependencyFile = (packageManager) => {
async function run() {
try {
//
@@ -100616,6 +100626,10 @@ async function run() {
core_debug(`add bin ${added}`);
const goPath = await which('go');
const goVersion = (external_child_process_default().execSync(`${goPath} version`) || '').toString();
const goEnvJson = readGoEnv(goPath);
if (goEnvJson) {
setGoEnvOutputs(goEnvJson);
}
if (cache && isCacheFeatureAvailable()) {
const packageManager = 'default';
const cacheDependencyPath = getInput('cache-dependency-path');
@@ -100641,6 +100655,45 @@ async function run() {
setFailed(error.message);
}
}
function readGoEnv(goPath) {
try {
const rawGoEnv = external_child_process_default().execFileSync(goPath, ['env', '-json'], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe']
});
const parsed = JSON.parse(rawGoEnv);
if (typeof parsed !== 'object' ||
parsed === null ||
Array.isArray(parsed)) {
throw new Error("'go env -json' did not return a JSON object");
}
return parsed;
}
catch (error) {
core_info(`Unable to read 'go env -json', the Go environment outputs will not be set: ${error.message}`);
return undefined;
}
}
const goEnvOutputs = [
[Outputs.GoPath, 'GOPATH'],
[Outputs.GoBin, 'GOBIN'],
[Outputs.GoRoot, 'GOROOT'],
[Outputs.GoCache, 'GOCACHE'],
[Outputs.GoModCache, 'GOMODCACHE'],
[Outputs.GoOs, 'GOOS'],
[Outputs.GoArch, 'GOARCH'],
[Outputs.GoToolDir, 'GOTOOLDIR']
];
function setGoEnvOutputs(goEnv) {
for (const [output, variable] of goEnvOutputs) {
setOutput(output, goEnv[variable] ?? '');
}
setOutput(Outputs.GoBinPath, goEnv['GOBIN'] || goPathBin(goEnv));
}
function goPathBin(goEnv) {
const goPath = (goEnv['GOPATH'] ?? '').split((external_path_default()).delimiter)[0];
return goPath ? external_path_default().join(goPath, 'bin') : '';
}
async function addBinToPath() {
let added = false;
const g = await which('go');
+33
View File
@@ -12,6 +12,9 @@
- [Restore-only caches](advanced-usage.md#restore-only-caches)
- [Parallel builds](advanced-usage.md#parallel-builds)
- [Outputs](advanced-usage.md#outputs)
- [go-version](advanced-usage.md#go-version)
- [cache-hit](advanced-usage.md#cache-hit)
- [Go environment outputs](advanced-usage.md#go-environment-outputs)
- [Custom download URL](advanced-usage.md#custom-download-url)
- [Using `setup-go` on GHES](advanced-usage.md#using-setup-go-on-ghes)
@@ -420,6 +423,36 @@ jobs:
- run: echo "Was the Go cache restored? ${{ steps.go124.outputs.cache-hit }}" # true if cache-hit occurred
```
### Go environment outputs
Once Go is on the `PATH`, the action exposes the most commonly needed `go env` variables as outputs, so workflows don't have to query them in duplicated `bash`/`pwsh` steps — one expression works on Linux, macOS, and Windows. The values are a snapshot from setup time: anything a later step changes, such as setting `GOOS`/`GOARCH` to cross-compile, is not reflected.
| Output | `go env` variable | Notes |
| --- | --- | --- |
| `go-path` | `GOPATH` | Go workspace root; `bin` and `pkg/mod` live under it |
| `go-bin` | `GOBIN` | Go 1.27+ reports an implicit `$GOPATH/bin` default; earlier releases are empty unless `GOBIN` is set |
| `go-bin-path` | `GOBIN` or `$GOPATH/bin` | The base Go binary directory, using the first `GOPATH` entry. Cross-compiled binaries go one level deeper, in `$GOPATH/bin/$GOOS_$GOARCH` |
| `go-root` | `GOROOT` | Installation directory of the Go toolchain in use |
| `go-cache` | `GOCACHE` | Build cache directory |
| `go-mod-cache` | `GOMODCACHE` | Module cache directory |
| `go-os` | `GOOS` | Go notation (`linux`, `darwin`, `windows`), unlike `runner.os` |
| `go-arch` | `GOARCH` | Go notation (`amd64`, `arm64`), unlike `runner.arch` or the `x64` in the action's cache key |
| `go-tool-dir` | `GOTOOLDIR` | Holds `compile`, `link`, `vet` and the other toolchain binaries |
```yaml
steps:
- uses: actions/setup-go@v7
id: setup-go
with:
go-version: '1.25.5'
- run: echo "Modules are cached in ${{ steps.setup-go.outputs.go-mod-cache }}"
```
Quote `go-bin-path` in shell commands, since it may contain spaces. Variables without a dedicated output are not exposed; run `go env <NAME>` in a step when you need one.
> [!NOTE]
> These outputs need Go 1.9 or newer (`go env -json`); on older releases the action logs a message, leaves them unset and does not fail. `go-cache` needs Go 1.10 and `go-mod-cache` needs Go 1.15.
## Custom download URL
The `go-download-base-url` input lets you download Go from a mirror or alternative source instead of the default `https://go.dev/dl`. This can also be set via the `GO_DOWNLOAD_BASE_URL` environment variable; the input takes precedence over the environment variable.
+10 -1
View File
@@ -4,5 +4,14 @@ export enum State {
}
export enum Outputs {
CacheHit = 'cache-hit'
CacheHit = 'cache-hit',
GoPath = 'go-path',
GoBin = 'go-bin',
GoBinPath = 'go-bin-path',
GoRoot = 'go-root',
GoCache = 'go-cache',
GoModCache = 'go-mod-cache',
GoOs = 'go-os',
GoArch = 'go-arch',
GoToolDir = 'go-tool-dir'
}
+57
View File
@@ -10,6 +10,7 @@ import cp from 'child_process';
import fs from 'fs';
import os from 'os';
import {Architecture} from './types.js';
import {Outputs} from './constants.js';
export async function run() {
try {
@@ -82,6 +83,11 @@ export async function run() {
const goPath = await io.which('go');
const goVersion = (cp.execSync(`${goPath} version`) || '').toString();
const goEnvJson = readGoEnv(goPath);
if (goEnvJson) {
setGoEnvOutputs(goEnvJson);
}
if (cache && isCacheFeatureAvailable()) {
const packageManager = 'default';
@@ -119,6 +125,57 @@ export async function run() {
}
}
export function readGoEnv(goPath: string): Record<string, string> | undefined {
try {
const rawGoEnv = cp.execFileSync(goPath, ['env', '-json'], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe']
});
const parsed: unknown = JSON.parse(rawGoEnv);
if (
typeof parsed !== 'object' ||
parsed === null ||
Array.isArray(parsed)
) {
throw new Error("'go env -json' did not return a JSON object");
}
return parsed as Record<string, string>;
} catch (error) {
core.info(
`Unable to read 'go env -json', the Go environment outputs will not be set: ${
(error as Error).message
}`
);
return undefined;
}
}
const goEnvOutputs: ReadonlyArray<[Outputs, string]> = [
[Outputs.GoPath, 'GOPATH'],
[Outputs.GoBin, 'GOBIN'],
[Outputs.GoRoot, 'GOROOT'],
[Outputs.GoCache, 'GOCACHE'],
[Outputs.GoModCache, 'GOMODCACHE'],
[Outputs.GoOs, 'GOOS'],
[Outputs.GoArch, 'GOARCH'],
[Outputs.GoToolDir, 'GOTOOLDIR']
];
export function setGoEnvOutputs(goEnv: Record<string, string>): void {
for (const [output, variable] of goEnvOutputs) {
core.setOutput(output, goEnv[variable] ?? '');
}
core.setOutput(Outputs.GoBinPath, goEnv['GOBIN'] || goPathBin(goEnv));
}
function goPathBin(goEnv: Record<string, string>): string {
const goPath = (goEnv['GOPATH'] ?? '').split(path.delimiter)[0];
return goPath ? path.join(goPath, 'bin') : '';
}
export async function addBinToPath(): Promise<boolean> {
let added = false;
const g = await io.which('go');