Skip to content
Merged
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
12 changes: 11 additions & 1 deletion .github/workflows/test-action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: test-action
on: [pull_request, workflow_dispatch]

jobs:
test-default:
test-default-latest:
strategy:
matrix:
os: [ubuntu-latest,windows-latest,macos-latest]
Expand Down Expand Up @@ -52,6 +52,16 @@ jobs:
- name: Verify func using path
run: /tmp/func-bin/func version

test-custom-binary-source:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup func CLI with custom binarySource URL
uses: ./
with:
binarySource: 'https://github.com/knative/func/releases/download/knative-v1.19.0/func_linux_amd64'
- run: func version

test-invalid-version:
runs-on: ubuntu-latest
steps:
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ GitHub Action to download and setup the func CLI. Automatically detects OS and a
```yaml
- uses: functions-dev/action@main
with:
version: 'v1.20.0' # optional
name: 'func' # optional
version: 'v1.20.0' # optional - uses latest as default
```

## Inputs

| Input | Description | Default |
|-------|-------------|---------|
| `version` | Version to download (e.g. `v1.20.0`) | recent stable |
| `version` | Version to download (e.g. `v1.20.0`) | latest |
| `name` | Binary name | `func` |
| `binary` | Specific binary to download | auto-detected |
| `binary` | Specific binary to download from GitHub release | auto-detected |
| `destination` | Download directory | cwd |
| `binarySource` | Full URL for the func binary | empty (uses GitHub releases) |
6 changes: 3 additions & 3 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ inputs:
binary:
description: '(optional) Binary you want to download (exact string expected), otherwise will be determined via the OS of GH Runner'
version:
description: '(optional) Provide version to download. Any version in release pages works https://github.com/knative/func/tags'
description: '(optional) Version to download. Use "latest" or specify a version from release pages https://github.com/knative/func/tags'
destination:
description: '(optional) Provide a path where to move the desired downloaded binary, otherwise cwd is used'
description: '(optional) Path where to move the desired downloaded binary, otherwise cwd is used'
binarySource:
description: '(optional) Base URL for downloading binaries. Defaults to GitHub releases. Pattern: <url-base>/<version>/<os-bin-name>'
description: '(optional) Full URL for downloading the binary. Overrides version if provided. Must be curl-able returning the func binary'
runs:
using: 'node20'
main: 'index.js'
223 changes: 136 additions & 87 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,112 +3,161 @@ const exec = require('@actions/exec');
const path = require('path');
const fs = require('fs');

// Static version - update manually when new func releases are available
const DEFAULT_FUNC_VERSION = 'knative-v1.20.1';
// Using latest as default
const DEFAULT_FUNC_VERSION = 'latest';
const DEFAULT_BINARY_SOURCE = 'https://github.com/knative/func/releases/download';
const DEFAULT_LATEST_BINARY_SOURCE = 'https://github.com/knative/func/releases/latest/download';

// Returns the binary name for the current OS/arch from GitHub releases
function getOsBinName() {
const runnerOS = process.env.RUNNER_OS;
const runnerArch = process.env.RUNNER_ARCH;

if (runnerOS === 'Linux') {
switch (runnerArch) {
case 'X64': return 'func_linux_amd64';
case 'ARM64': return 'func_linux_arm64';
case 'PPC64LE': return 'func_linux_ppc64le';
case 'S390X': return 'func_linux_s390x';
default: return 'unknown';
}
} else if (runnerOS === 'macOS') {
return runnerArch === 'X64' ? 'func_darwin_amd64' : 'func_darwin_arm64';
} else if (runnerOS === 'Windows') {
return 'func_windows_amd64.exe';
} else {
return 'unknown';
}
const osBinName = core.getInput('binary');
if (osBinName !== "") {
return osBinName;
}

const runnerOS = process.env.RUNNER_OS;
const runnerArch = process.env.RUNNER_ARCH;

if (runnerOS === 'Linux') {
switch (runnerArch) {
case 'X64': return 'func_linux_amd64';
case 'ARM64': return 'func_linux_arm64';
case 'PPC64LE': return 'func_linux_ppc64le';
case 'S390X': return 'func_linux_s390x';
default: throw new Error(`unknown runner: ${runnerArch}`);
}
} else if (runnerOS === 'macOS') {
return runnerArch === 'X64' ? 'func_darwin_amd64' : 'func_darwin_arm64';
} else if (runnerOS === 'Windows') {
return 'func_windows_amd64.exe';
} else {
throw new Error(`unknown runner: ${runnerArch}`);
}
}

function resolveFullPathBin() {
const destination = core.getInput('destination') || process.cwd();
let bin = core.getInput('name') || 'func';
if (process.env.RUNNER_OS === 'Windows' && !bin.endsWith('.exe')) {
bin += '.exe';
}
if (!fs.existsSync(destination)) {
fs.mkdirSync(destination, { recursive: true });
}
return path.resolve(destination, bin);
}

// Normalizes version to release tag format: knative-vX.Y.Z
// Ex.: '1.16' or 'v1.16' will return 'knative-v1.16.0'
function smartVersionUpdate(version) {
const versionRegex = /^(?<knprefix>knative-)?(?<prefix>v?)(?<major>\d+)\.(?<minor>\d+)(.(?<patch>\d+))?$/;
const match = version.match(versionRegex);
if (!match) {
throw new Error(`Invalid version format (${version}). Expected format: "1.16[.X]" or "v1.16[.X]"`);
}
const knprefix = 'knative-';
const prefix = 'v';
const patch = match.groups.patch ?? 0;
return `${knprefix}${prefix}${match.groups.major}.${match.groups.minor}.${patch}`;
const versionRegex = /^(?<knprefix>knative-)?(?<prefix>v?)(?<major>\d+)\.(?<minor>\d+)(\.(?<patch>\d+))?$/;
const match = version.match(versionRegex);
if (!match) {
throw new Error(`Invalid version format (${version}). Expected format: "1.16[.X]" or "v1.16[.X]"`);
}
const knprefix = 'knative-';
const prefix = 'v';
const patch = match.groups.patch ?? 0;
return `${knprefix}${prefix}${match.groups.major}.${match.groups.minor}.${patch}`;
}

const DEFAULT_BINARY_SOURCE = 'https://github.com/knative/func/releases/download';

// Downloads binary from release URL and makes it executable
async function downloadFuncBinary(version, osBinName, binPath, binarySource) {
const url = `${binarySource}/${version}/${osBinName}`;
core.info(`Downloading from: ${url}`);
async function downloadFuncBinary(url, binPath) {
core.info(`Downloading from: ${url}`);

await exec.exec('curl', ['-L', '--fail', '-o', binPath, url]);
await exec.exec('curl', ['-L', '--fail', '-o', binPath, url]);

if (!fs.existsSync(binPath)) {
throw new Error("Download failed, couldn't find the binary on disk");
}
if (!fs.existsSync(binPath)) {
throw new Error("Download failed, couldn't find the binary on disk");
}

if (process.env.RUNNER_OS !== 'Windows') {
await exec.exec('chmod', ['+x', binPath]);
}
if (process.env.RUNNER_OS !== 'Windows') {
await exec.exec('chmod', ['+x', binPath]);
}
}

// Adds binary directory to PATH for current and subsequent steps
async function addBinToPath(binPath) {
const dir = path.dirname(binPath);
fs.appendFileSync(process.env.GITHUB_PATH, `\n${dir}`);

if (!process.env.PATH.includes(dir)) {
process.env.PATH = process.env.PATH + path.delimiter + dir;
core.info(`${dir} added to PATH`);
}
function addBinToPath(binPath) {
const dir = path.dirname(binPath);
fs.appendFileSync(process.env.GITHUB_PATH, `\n${dir}`);

if (!process.env.PATH.split(path.delimiter).includes(dir)) {
process.env.PATH = process.env.PATH + path.delimiter + dir;
core.info(`${dir} added to PATH`);
}
}

// Resolve download url based on given input
// binName: name of func binary when it is to be constructed for full URL
// (when not using binarySource)
function resolveDownloadUrl(binName) {
const binarySource = core.getInput('binarySource');
if (binarySource !== "") {
core.info(`Using custom binary source: ${binarySource}`);
return binarySource;
}

const versionInput = core.getInput('version') || DEFAULT_FUNC_VERSION;
if (versionInput.toLowerCase().trim() === DEFAULT_FUNC_VERSION) {
core.info("Using latest version...");
return buildUrlString(DEFAULT_FUNC_VERSION);
}
const version = smartVersionUpdate(versionInput);
core.info(`Using specific version ${version}`);
return buildUrlString(version);

function buildUrlString(version) {
return version === DEFAULT_FUNC_VERSION
? `${DEFAULT_LATEST_BINARY_SOURCE}/${binName}`
: `${DEFAULT_BINARY_SOURCE}/${version}/${binName}`;
}
}

async function run() {
const osBinName = core.getInput('binary') || getOsBinName();
if (osBinName === "unknown") {
core.setFailed("Invalid os binary determination, try setting it specifically using 'binary'");
return;
}

const versionInput = core.getInput('version') || DEFAULT_FUNC_VERSION;
const destination = core.getInput('destination') || process.cwd();
const binarySource = core.getInput('binarySource') || DEFAULT_BINARY_SOURCE;
let bin = core.getInput('name') || 'func';
if (process.env.RUNNER_OS === 'Windows' && !bin.endsWith('.exe')) {
bin += '.exe';
}

let version;
try {
version = smartVersionUpdate(versionInput);
} catch (error) {
core.setFailed(error.message);
return;
}

if (!fs.existsSync(destination)) {
fs.mkdirSync(destination, { recursive: true });
}

const fullPathBin = path.resolve(destination, bin);

try {
await downloadFuncBinary(version, osBinName, fullPathBin, binarySource);
} catch (error) {
core.setFailed(`Download failed: ${error.message}`);
return;
}

await addBinToPath(fullPathBin);
await exec.exec(fullPathBin, ['version']);
let osBinName;
try {
osBinName = getOsBinName();
} catch (error) {
core.setFailed(error.message);
return;
}

let url;
try {
url = resolveDownloadUrl(osBinName);
} catch (error) {
core.setFailed(`Failed to resolve url: ${error.message}`);
return;
}

let fullPathBin;
try {
fullPathBin = resolveFullPathBin();
} catch (error) {
core.setFailed(error.message);
return;
}

try {
await downloadFuncBinary(url, fullPathBin);
} catch (error) {
core.setFailed(`Download failed: ${error.message}`);
return;
}

try {
addBinToPath(fullPathBin);
} catch (error) {
core.setFailed(error.message);
return;
}

try {
await exec.exec(fullPathBin, ['version']);
} catch (error) {
core.setFailed(error.message);
return;
}
}

run();
run();
Loading