Skip to content
Draft
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
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,17 @@ This action sets up a Julia environment for use in actions by downloading a spec
# Default: '1'
version: '1'

# Read the Julia version from a .tool-versions file.
#
# This is useful for repositories that already pin Julia with asdf or mise-en-place.
# The file must contain a Julia entry with major, minor, and patch versions, for example:
# julia 1.10.11
#
# The `version-file` input cannot be used together with the `version` input.
#
# Default: ''
version-file: '.tool-versions'

# The architecture of the Julia binaries.
#
# Please note that installing aarch64 binaries only makes sense on self-hosted aarch64 runners.
Expand Down Expand Up @@ -128,6 +139,24 @@ You can either specify specific Julia versions or version ranges. If you specify
>
> It is strongly recommended to wrap versions in quotes. Otherwise, the YAML parser used by GitHub Actions parses certain versions as numbers which causes the wrong version to be selected. For example, `1.0` may be parsed as `1`.

Alternatively, set `version-file` to a `.tool-versions` file containing a Julia entry:

```yaml
steps:
- uses: actions/checkout@v6
- uses: julia-actions/setup-julia@v3
with:
version-file: '.tool-versions'
```

For example, the `.tool-versions` file might contain:

```text
Comment thread
DilumAluthge marked this conversation as resolved.
julia 1.10.11
```

The Julia entry must specify major, minor, and patch versions. The `version` and `version-file` inputs cannot be used together.

#### Examples

- `'1.2.0'` is a valid semver version. The action will try to download exactly this version. If it's not available, the build step will fail.
Expand Down
81 changes: 81 additions & 0 deletions __tests__/installer.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// The testing setup has been derived from the actions/setup-go@bc6edb5 action.
// Check README.md for licence information.

import * as fs from 'fs'
import * as os from 'os'
import * as path from 'path'

import * as io from '@actions/io'
Expand Down Expand Up @@ -40,6 +42,85 @@ process.env['RUNNER_TEMP'] = tempDir
import * as installer from '../src/installer'
import exp from 'constants'

describe("readJuliaVersionFromToolVersionsFile tests", () => {
let tempDirForToolVersions: string

beforeEach(() => {
tempDirForToolVersions = fs.mkdtempSync(path.join(os.tmpdir(), "setup-julia-tool-versions-"))
})

afterEach(() => {
fs.rmSync(tempDirForToolVersions, { force: true, recursive: true })
})

function writeToolVersions(content: string): string {
const versionFilePath = path.join(tempDirForToolVersions, ".tool-versions")
fs.writeFileSync(versionFilePath, content)
return versionFilePath
}

it("Reads a Julia version", () => {
const versionFilePath = writeToolVersions("julia 1.10.4\n")
expect(installer.readJuliaVersionFromToolVersionsFile(versionFilePath)).toEqual("1.10.4")
})

it("Handles comments and blank lines", () => {
const versionFilePath = writeToolVersions("\n# tools\n\njulia 1.11.0\n")
expect(installer.readJuliaVersionFromToolVersionsFile(versionFilePath)).toEqual("1.11.0")
})

it("Finds Julia among multiple tools", () => {
const versionFilePath = writeToolVersions("nodejs 24.11.1\njulia 1.10.7\npython 3.14.0\n")
expect(installer.readJuliaVersionFromToolVersionsFile(versionFilePath)).toEqual("1.10.7")
})

it("Uses the first Julia version token", () => {
const versionFilePath = writeToolVersions("julia 1.10.4 1.11.1 # fallback\n")
expect(installer.readJuliaVersionFromToolVersionsFile(versionFilePath)).toEqual("1.10.4")
})

it("Reads a prerelease Julia version", () => {
const versionFilePath = writeToolVersions("julia 1.11.0-rc1\n")
expect(installer.readJuliaVersionFromToolVersionsFile(versionFilePath)).toEqual("1.11.0-rc1")
})

it("Throws when the Julia entry only specifies major and minor", () => {
const versionFilePath = writeToolVersions("julia 1.10\n")
expect(() => installer.readJuliaVersionFromToolVersionsFile(versionFilePath)).toThrow("must specify major, minor, and patch")
})

it("Throws when the Julia entry only specifies major", () => {
const versionFilePath = writeToolVersions("julia 1\n")
expect(() => installer.readJuliaVersionFromToolVersionsFile(versionFilePath)).toThrow("must specify major, minor, and patch")
})

it("Throws when the Julia entry uses a version keyword", () => {
const versionFilePath = writeToolVersions("julia lts\n")
expect(() => installer.readJuliaVersionFromToolVersionsFile(versionFilePath)).toThrow("must specify major, minor, and patch")
})

it("Throws when no Julia entry exists", () => {
const versionFilePath = writeToolVersions("nodejs 24.11.1\npython 3.14.0\n")
expect(() => installer.readJuliaVersionFromToolVersionsFile(versionFilePath)).toThrow("No Julia version found")
})

it("Throws when the Julia entry has no version", () => {
const versionFilePath = writeToolVersions("julia\n")
expect(() => installer.readJuliaVersionFromToolVersionsFile(versionFilePath)).toThrow("No Julia version found")
})

it("Throws when the version file is not named .tool-versions", () => {
const versionFilePath = path.join(tempDirForToolVersions, "julia-version")
fs.writeFileSync(versionFilePath, "julia 1.10.4\n")
expect(() => installer.readJuliaVersionFromToolVersionsFile(versionFilePath)).toThrow("only supports .tool-versions")
})

it("Throws when the version file does not exist", () => {
const versionFilePath = path.join(tempDirForToolVersions, ".tool-versions")
expect(() => installer.readJuliaVersionFromToolVersionsFile(versionFilePath)).toThrow("does not exist")
})
})

describe("getProjectFilePath tests", () => {
let orgJuliaProject
let orgWorkingDir
Expand Down
7 changes: 5 additions & 2 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@ description: 'Setup a Julia environment and add it to the PATH'
author: 'Sascha Mann'
inputs:
version:
description: 'The Julia version to download (if necessary) and use. Use a string input to avoid unwanted decimal conversion e.g. 1.10 without quotes will be interpreted as 1.1. Examples: "1", "1.10", "lts", "pre"'
default: '1'
description: 'The Julia version to download (if necessary) and use. Use a string input to avoid unwanted decimal conversion e.g. 1.10 without quotes will be interpreted as 1.1. Examples: "1", "1.10", "lts", "pre". Defaults to "1" when neither version nor version-file is set.'
Comment thread
DilumAluthge marked this conversation as resolved.
required: false
version-file:
description: 'Path to a .tool-versions file containing the Julia version to download (if necessary) and use. Cannot be used simultaneously with the `version` input.'
required: false
include-all-prereleases:
description: 'Include prereleases when matching the Julia version to available versions.'
required: false
Expand Down
66 changes: 59 additions & 7 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.getJuliaVersionInfo = getJuliaVersionInfo;
exports.getJuliaVersions = getJuliaVersions;
exports.readJuliaVersionFromToolVersionsFile = readJuliaVersionFromToolVersionsFile;
exports.getProjectFilePath = getProjectFilePath;
exports.validJuliaCompatRange = validJuliaCompatRange;
exports.readJuliaCompatRange = readJuliaCompatRange;
Expand Down Expand Up @@ -133,6 +134,38 @@ function getJuliaVersions(versionInfo) {
return versions;
});
}
/**
* @returns The Julia version specified in an asdf/mise-en-place .tool-versions file.
*/
function readJuliaVersionFromToolVersionsFile(versionFilePath) {
var _a;
if (path.basename(versionFilePath) !== ".tool-versions") {
throw new Error(`The version-file input only supports .tool-versions files: ${versionFilePath}`);
}
if (!fs.existsSync(versionFilePath)) {
throw new Error(`The specified version-file does not exist: ${versionFilePath}`);
}
const lines = fs.readFileSync(versionFilePath, 'utf8').split(/\r\n|\r|\n/);
for (let line of lines) {
line = line.trim();
if (!line || line.startsWith("#")) {
continue;
}
const match = line.match(/^julia(?:\s+(.+))?$/);
if (!match) {
continue;
}
const version = ((_a = match[1]) === null || _a === void 0 ? void 0 : _a.trim().split(/\s+/)[0]) || "";
if (!version || version.startsWith("#")) {
throw new Error(`No Julia version found in ${versionFilePath}`);
}
if (!semver.valid(version)) {
throw new Error(`The Julia version in ${versionFilePath} must specify major, minor, and patch versions: ${version}`);
}
return version;
}
throw new Error(`No Julia version found in ${versionFilePath}`);
}
/**
* @returns The path to the Julia project file
*/
Expand Down Expand Up @@ -599,19 +632,38 @@ function run() {
}
// Inputs.
// Note that we intentionally strip leading and lagging whitespace by using `.trim()`
const versionInput = core.getInput('version').trim();
const rawVersionInput = core.getInput('version').trim();
const versionFileInput = core.getInput('version-file').trim();
const includePrereleases = core.getInput('include-all-prereleases').trim() == 'true';
const originalArchInput = core.getInput('arch').trim();
const forceArch = core.getInput('force-arch').trim() == 'true';
const projectInput = core.getInput('project').trim(); // Julia project file
// It can easily happen that, for example, a workflow file contains an input `version: ${{ matrix.julia-version }}`
// while the strategy matrix only contains a key `${{ matrix.version }}`.
// In that case, we want the action to fail, rather than trying to download julia from an URL that's missing parts and 404ing.
// We _could_ fall back to the default but that means that builds silently do things differently than they're meant to, which
// is worse than failing the build.
if (!versionInput) { // if `versionInput` is an empty string
// `core.getInput('version')` returns an empty string both when the input is
// omitted and when it is explicitly set to an empty value. Those cases now
// differ: omitted means use `version-file` or the default `1`, while an
// explicitly empty value usually indicates a typo like
// `version: ${{ matrix.julia-version }}` with no matching matrix key.
// GitHub exposes provided inputs as `INPUT_*` environment variables, so
// `INPUT_VERSION` lets us preserve the old explicit-empty error.
const versionInputWasProvided = process.env.INPUT_VERSION !== undefined;
if (versionInputWasProvided && !rawVersionInput) { // if `rawVersionInput` is an empty string
throw new Error('Version input must not be null');
}
if (rawVersionInput && versionFileInput) {
throw new Error('The "version" and "version-file" inputs cannot both be set');
}
let versionInput = rawVersionInput;
if (!versionInput && versionFileInput) {
// GitHub Actions does not apply a step-specific working directory to `uses:` steps,
// so relative `version-file` paths are resolved from the checked-out workspace.
const workspace = process.env.GITHUB_WORKSPACE || process.cwd();
const versionFilePath = path.isAbsolute(versionFileInput) ? versionFileInput : path.join(workspace, versionFileInput);
versionInput = installer.readJuliaVersionFromToolVersionsFile(versionFilePath);
core.info(`Resolved ${versionFileInput} as ${versionInput}`);
}
if (!versionInput) {
versionInput = '1';
}
if (versionInput == '1.6') {
core.notice('[setup-julia] If you are testing 1.6 as a Long Term Support (lts) version, consider using the new "lts" version specifier instead of "1.6" explicitly, which will automatically resolve the current lts.');
}
Expand Down
33 changes: 33 additions & 0 deletions lib/installer.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 26 additions & 7 deletions lib/setup-julia.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

39 changes: 39 additions & 0 deletions src/installer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,45 @@ export async function getJuliaVersions(versionInfo): Promise<string[]> {
return versions
}

/**
* @returns The Julia version specified in an asdf/mise-en-place .tool-versions file.
*/
export function readJuliaVersionFromToolVersionsFile(versionFilePath: string): string {
if (path.basename(versionFilePath) !== ".tool-versions") {
throw new Error(`The version-file input only supports .tool-versions files: ${versionFilePath}`)
}

if (!fs.existsSync(versionFilePath)) {
throw new Error(`The specified version-file does not exist: ${versionFilePath}`)
}

const lines = fs.readFileSync(versionFilePath, 'utf8').split(/\r\n|\r|\n/)
for (let line of lines) {
line = line.trim()

if (!line || line.startsWith("#")) {
continue
}

const match = line.match(/^julia(?:\s+(.+))?$/)
if (!match) {
continue
}

const version = match[1]?.trim().split(/\s+/)[0] || ""
if (!version || version.startsWith("#")) {
throw new Error(`No Julia version found in ${versionFilePath}`)
}
if (!semver.valid(version)) {
throw new Error(`The Julia version in ${versionFilePath} must specify major, minor, and patch versions: ${version}`)
}

return version
}

throw new Error(`No Julia version found in ${versionFilePath}`)
}

/**
* @returns The path to the Julia project file
*/
Expand Down
Loading
Loading