From 56a793a03cea1397bcf4781f6919aa6377c7a05b Mon Sep 17 00:00:00 2001 From: Wismill Date: Tue, 17 Jan 2023 16:28:52 +0100 Subject: [PATCH] Add support for ghcup release channel (#175) New action input `ghcup-release-channel` to provide a ghc (pre)release channel for ghcup which will be added via `ghcup config add-release-channel`. Useful for installing ghc prereleases. Tested with GHC 9.6.1 alpha (9.6.0.20230111). Fixes: #108. Co-authored-by: Andreas Abel --- .github/workflows/workflow.yml | 6 ++++++ setup/README.md | 21 +++++++++++---------- setup/action.yml | 3 +++ setup/dist/index.js | 28 ++++++++++++++++++++++++++-- setup/lib/installer.d.ts | 2 ++ setup/lib/installer.js | 8 +++++++- setup/lib/opts.d.ts | 5 +++++ setup/lib/opts.js | 17 ++++++++++++++++- setup/lib/setup-haskell.js | 3 +++ setup/src/installer.ts | 9 +++++++++ setup/src/opts.ts | 17 +++++++++++++++++ setup/src/setup-haskell.ts | 8 +++++++- 12 files changed, 112 insertions(+), 15 deletions(-) diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 1a3b77ec..aa3eb1e2 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -51,6 +51,11 @@ jobs: - os: ubuntu-18.04 ghc: "7.4.1" cabal: "3.4" + # Test ghcup pre-release channel + - os: ubuntu-latest + ghc: "9.6.0.20230111" + ghcup_release_channel: "https://raw.githubusercontent.com/haskell/ghcup-metadata/master/ghcup-prereleases-0.0.7.yaml" + cabal: "3.8" # setup does something special for 7.10.3 (issue #79) - os: ubuntu-20.04 ghc: "7.10.3" @@ -75,6 +80,7 @@ jobs: - uses: ./setup with: ghc-version: ${{ matrix.ghc }} + ghcup-release-channel: ${{ matrix.ghcup_release_channel }} cabal-version: ${{ matrix.cabal }} cabal-update: ${{ matrix.cabal_update }} diff --git a/setup/README.md b/setup/README.md index db41cd08..010e362e 100644 --- a/setup/README.md +++ b/setup/README.md @@ -98,16 +98,17 @@ jobs: ## Inputs -| Name | Description | Type | Default | -| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | --------- | ----------- | -| `ghc-version` | GHC version to use, e.g. `9.2` or `9.2.4`. | `string` | `latest` | -| `cabal-version` | Cabal version to use, e.g. `3.4`. | `string` | `latest` | -| `stack-version` | Stack version to use, e.g. `latest`. Stack will only be installed if `enable-stack` is set. | `string` | `latest` | -| `enable-stack` | If set, will setup Stack. | "boolean" | false/unset | -| `stack-no-global` | If set, `enable-stack` must be set. Prevents installing GHC and Cabal globally. | "boolean" | false/unset | -| `stack-setup-ghc` | If set, `enable-stack` must be set. Runs stack setup to install the specified GHC. (Note: setting this does _not_ imply `stack-no-global`.) | "boolean" | false/unset | -| `disable-matcher` | If set, disables match messages from GHC as GitHub CI annotations. | "boolean" | false/unset | -| `cabal-update` | If set to `false`, skip `cabal update` step. | `boolean` | `true` | +| Name | Description | Type | Default | +| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | --------- | ----------- | +| `ghc-version` | GHC version to use, e.g. `9.2` or `9.2.4`. | `string` | `latest` | +| `cabal-version` | Cabal version to use, e.g. `3.4`. | `string` | `latest` | +| `stack-version` | Stack version to use, e.g. `latest`. Stack will only be installed if `enable-stack` is set. | `string` | `latest` | +| `enable-stack` | If set, will setup Stack. | "boolean" | false/unset | +| `stack-no-global` | If set, `enable-stack` must be set. Prevents installing GHC and Cabal globally. | "boolean" | false/unset | +| `stack-setup-ghc` | If set, `enable-stack` must be set. Runs stack setup to install the specified GHC. (Note: setting this does _not_ imply `stack-no-global`.) | "boolean" | false/unset | +| `disable-matcher` | If set, disables match messages from GHC as GitHub CI annotations. | "boolean" | false/unset | +| `cabal-update` | If set to `false`, skip `cabal update` step. | `boolean` | `true` | +| `ghcup-release-channel` | If set, add a [release channel](https://www.haskell.org/ghcup/guide/#pre-release-channels) to ghcup. | `URL` | none | Note: "boolean" types are set/unset, not true/false. That is, setting any "boolean" to a value other than the empty string (`""`) will be considered true/set. diff --git a/setup/action.yml b/setup/action.yml index bc047ec3..805a6138 100644 --- a/setup/action.yml +++ b/setup/action.yml @@ -30,6 +30,9 @@ inputs: # Note: 'cabal-update' only accepts 'true' and 'false' as values. # This is different from the other flags ('enable-stack', 'disable-matcher' etc.) # which are true as soon as they are not null. + ghcup-release-channel: + required: false + description: "A release channel URL to add to ghcup via `ghcup config add-release-channel`." disable-matcher: required: false description: 'If specified, disables match messages from GHC as GitHub CI annotations.' diff --git a/setup/dist/index.js b/setup/dist/index.js index 4ec0fc5f..d5730606 100644 --- a/setup/dist/index.js +++ b/setup/dist/index.js @@ -13303,7 +13303,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.resetTool = exports.installTool = void 0; +exports.addGhcupReleaseChannel = exports.resetTool = exports.installTool = void 0; const core = __importStar(__nccwpck_require__(2186)); const exec_1 = __nccwpck_require__(1514); const io_1 = __nccwpck_require__(7436); @@ -13531,6 +13531,12 @@ async function ghcupBin(os) { await fs_1.promises.chmod(bin, 0o755); return (0, path_1.join)(await tc.cacheFile(bin, 'ghcup', 'ghcup', opts_1.ghcup_version), 'ghcup'); } +async function addGhcupReleaseChannel(channel, os) { + core.info(`Adding ghcup release channel: ${channel}`); + const bin = await ghcupBin(os); + await exec(bin, ['config', 'add-release-channel', channel.toString()]); +} +exports.addGhcupReleaseChannel = addGhcupReleaseChannel; async function ghcup(tool, version, os) { core.info(`Attempting to install ${tool} ${version} using ghcup`); const bin = await ghcupBin(os); @@ -13646,7 +13652,7 @@ var __importStar = (this && this.__importStar) || function (mod) { return result; }; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.getOpts = exports.parseYAMLBoolean = exports.releaseRevision = exports.getDefaults = exports.yamlInputs = exports.ghcup_version = exports.supported_versions = exports.release_revisions = void 0; +exports.getOpts = exports.parseURL = exports.parseYAMLBoolean = exports.releaseRevision = exports.getDefaults = exports.yamlInputs = exports.ghcup_version = exports.supported_versions = exports.release_revisions = void 0; const core = __importStar(__nccwpck_require__(2186)); const fs_1 = __nccwpck_require__(7147); const js_yaml_1 = __nccwpck_require__(1917); @@ -13713,12 +13719,24 @@ function parseYAMLBoolean(name, val) { `Supported boolean values: \`true | True | TRUE | false | False | FALSE\``); } exports.parseYAMLBoolean = parseYAMLBoolean; +function parseURL(name, val) { + if (val === '') + return undefined; + try { + return new URL(val); + } + catch (e) { + throw new TypeError(`Action input "${name}" is not a valid URL`); + } +} +exports.parseURL = parseURL; function getOpts({ ghc, cabal, stack }, os, inputs) { core.debug(`Inputs are: ${JSON.stringify(inputs)}`); const stackNoGlobal = (inputs['stack-no-global'] || '') !== ''; const stackSetupGhc = (inputs['stack-setup-ghc'] || '') !== ''; const stackEnable = (inputs['enable-stack'] || '') !== ''; const matcherDisable = (inputs['disable-matcher'] || '') !== ''; + const ghcupReleaseChannel = parseURL('ghcup-release-channel', inputs['ghcup-release-channel'] || ''); // Andreas, 2023-01-05, issue #29: // 'cabal-update' has a default value, so we should get a proper boolean always. // Andreas, 2023-01-06: This is not true if we use the action as a library. @@ -13749,6 +13767,9 @@ function getOpts({ ghc, cabal, stack }, os, inputs) { ), enable: ghcEnable }, + ghcup: { + releaseChannel: ghcupReleaseChannel + }, cabal: { raw: verInpt.cabal, resolved: resolve(verInpt.cabal, cabal.supported, 'cabal', os, cabalEnable // if true: inform user about resolution @@ -13827,6 +13848,9 @@ async function run(inputs) { core.info('Preparing to setup a Haskell environment'); const os = process.platform; const opts = (0, opts_1.getOpts)((0, opts_1.getDefaults)(os), os, inputs); + if (opts.ghcup.releaseChannel) { + await core.group(`Preparing ghcup environment`, async () => (0, installer_1.addGhcupReleaseChannel)(opts.ghcup.releaseChannel, os)); + } for (const [t, { resolved }] of Object.entries(opts).filter(o => o[1].enable)) { await core.group(`Preparing ${t} environment`, async () => (0, installer_1.resetTool)(t, resolved, os)); await core.group(`Installing ${t} version ${resolved}`, async () => (0, installer_1.installTool)(t, resolved, os)); diff --git a/setup/lib/installer.d.ts b/setup/lib/installer.d.ts index 3e14a2b4..930d6308 100644 --- a/setup/lib/installer.d.ts +++ b/setup/lib/installer.d.ts @@ -1,3 +1,5 @@ +/// import { OS, Tool } from './opts'; export declare function installTool(tool: Tool, version: string, os: OS): Promise; export declare function resetTool(tool: Tool, _version: string, os: OS): Promise; +export declare function addGhcupReleaseChannel(channel: URL, os: OS): Promise; diff --git a/setup/lib/installer.js b/setup/lib/installer.js index c396e170..83b99aa5 100644 --- a/setup/lib/installer.js +++ b/setup/lib/installer.js @@ -26,7 +26,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.resetTool = exports.installTool = void 0; +exports.addGhcupReleaseChannel = exports.resetTool = exports.installTool = void 0; const core = __importStar(require("@actions/core")); const exec_1 = require("@actions/exec"); const io_1 = require("@actions/io"); @@ -254,6 +254,12 @@ async function ghcupBin(os) { await fs_1.promises.chmod(bin, 0o755); return (0, path_1.join)(await tc.cacheFile(bin, 'ghcup', 'ghcup', opts_1.ghcup_version), 'ghcup'); } +async function addGhcupReleaseChannel(channel, os) { + core.info(`Adding ghcup release channel: ${channel}`); + const bin = await ghcupBin(os); + await exec(bin, ['config', 'add-release-channel', channel.toString()]); +} +exports.addGhcupReleaseChannel = addGhcupReleaseChannel; async function ghcup(tool, version, os) { core.info(`Attempting to install ${tool} ${version} using ghcup`); const bin = await ghcupBin(os); diff --git a/setup/lib/opts.d.ts b/setup/lib/opts.d.ts index 58ab6542..9fa53c54 100644 --- a/setup/lib/opts.d.ts +++ b/setup/lib/opts.d.ts @@ -1,3 +1,4 @@ +/// export declare const release_revisions: Revisions; export declare const supported_versions: Record; export declare const ghcup_version: string; @@ -14,6 +15,9 @@ export interface ProgramOpt { } export interface Options { ghc: ProgramOpt; + ghcup: { + releaseChannel?: URL; + }; cabal: ProgramOpt & { update: boolean; }; @@ -53,5 +57,6 @@ export declare function releaseRevision(version: string, tool: Tool, os: OS): st * @returns boolean */ export declare function parseYAMLBoolean(name: string, val: string): boolean; +export declare function parseURL(name: string, val: string): URL | undefined; export declare function getOpts({ ghc, cabal, stack }: Defaults, os: OS, inputs: Record): Options; export {}; diff --git a/setup/lib/opts.js b/setup/lib/opts.js index 7ba2f4d3..5a388737 100644 --- a/setup/lib/opts.js +++ b/setup/lib/opts.js @@ -23,7 +23,7 @@ var __importStar = (this && this.__importStar) || function (mod) { return result; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.getOpts = exports.parseYAMLBoolean = exports.releaseRevision = exports.getDefaults = exports.yamlInputs = exports.ghcup_version = exports.supported_versions = exports.release_revisions = void 0; +exports.getOpts = exports.parseURL = exports.parseYAMLBoolean = exports.releaseRevision = exports.getDefaults = exports.yamlInputs = exports.ghcup_version = exports.supported_versions = exports.release_revisions = void 0; const core = __importStar(require("@actions/core")); const fs_1 = require("fs"); const js_yaml_1 = require("js-yaml"); @@ -90,12 +90,24 @@ function parseYAMLBoolean(name, val) { `Supported boolean values: \`true | True | TRUE | false | False | FALSE\``); } exports.parseYAMLBoolean = parseYAMLBoolean; +function parseURL(name, val) { + if (val === '') + return undefined; + try { + return new URL(val); + } + catch (e) { + throw new TypeError(`Action input "${name}" is not a valid URL`); + } +} +exports.parseURL = parseURL; function getOpts({ ghc, cabal, stack }, os, inputs) { core.debug(`Inputs are: ${JSON.stringify(inputs)}`); const stackNoGlobal = (inputs['stack-no-global'] || '') !== ''; const stackSetupGhc = (inputs['stack-setup-ghc'] || '') !== ''; const stackEnable = (inputs['enable-stack'] || '') !== ''; const matcherDisable = (inputs['disable-matcher'] || '') !== ''; + const ghcupReleaseChannel = parseURL('ghcup-release-channel', inputs['ghcup-release-channel'] || ''); // Andreas, 2023-01-05, issue #29: // 'cabal-update' has a default value, so we should get a proper boolean always. // Andreas, 2023-01-06: This is not true if we use the action as a library. @@ -126,6 +138,9 @@ function getOpts({ ghc, cabal, stack }, os, inputs) { ), enable: ghcEnable }, + ghcup: { + releaseChannel: ghcupReleaseChannel + }, cabal: { raw: verInpt.cabal, resolved: resolve(verInpt.cabal, cabal.supported, 'cabal', os, cabalEnable // if true: inform user about resolution diff --git a/setup/lib/setup-haskell.js b/setup/lib/setup-haskell.js index 2abb1fd2..0c831268 100644 --- a/setup/lib/setup-haskell.js +++ b/setup/lib/setup-haskell.js @@ -48,6 +48,9 @@ async function run(inputs) { core.info('Preparing to setup a Haskell environment'); const os = process.platform; const opts = (0, opts_1.getOpts)((0, opts_1.getDefaults)(os), os, inputs); + if (opts.ghcup.releaseChannel) { + await core.group(`Preparing ghcup environment`, async () => (0, installer_1.addGhcupReleaseChannel)(opts.ghcup.releaseChannel, os)); + } for (const [t, { resolved }] of Object.entries(opts).filter(o => o[1].enable)) { await core.group(`Preparing ${t} environment`, async () => (0, installer_1.resetTool)(t, resolved, os)); await core.group(`Installing ${t} version ${resolved}`, async () => (0, installer_1.installTool)(t, resolved, os)); diff --git a/setup/src/installer.ts b/setup/src/installer.ts index 001c7a03..05755ade 100644 --- a/setup/src/installer.ts +++ b/setup/src/installer.ts @@ -303,6 +303,15 @@ async function ghcupBin(os: OS): Promise { ); } +export async function addGhcupReleaseChannel( + channel: URL, + os: OS +): Promise { + core.info(`Adding ghcup release channel: ${channel}`); + const bin = await ghcupBin(os); + await exec(bin, ['config', 'add-release-channel', channel.toString()]); +} + async function ghcup(tool: Tool, version: string, os: OS): Promise { core.info(`Attempting to install ${tool} ${version} using ghcup`); const bin = await ghcupBin(os); diff --git a/setup/src/opts.ts b/setup/src/opts.ts index 987e037c..5be74ed4 100644 --- a/setup/src/opts.ts +++ b/setup/src/opts.ts @@ -24,6 +24,7 @@ export interface ProgramOpt { export interface Options { ghc: ProgramOpt; + ghcup: {releaseChannel?: URL}; cabal: ProgramOpt & {update: boolean}; stack: ProgramOpt & {setup: boolean}; general: {matcher: {enable: boolean}}; @@ -104,6 +105,15 @@ export function parseYAMLBoolean(name: string, val: string): boolean { ); } +export function parseURL(name: string, val: string): URL | undefined { + if (val === '') return undefined; + try { + return new URL(val); + } catch (e) { + throw new TypeError(`Action input "${name}" is not a valid URL`); + } +} + export function getOpts( {ghc, cabal, stack}: Defaults, os: OS, @@ -114,6 +124,10 @@ export function getOpts( const stackSetupGhc = (inputs['stack-setup-ghc'] || '') !== ''; const stackEnable = (inputs['enable-stack'] || '') !== ''; const matcherDisable = (inputs['disable-matcher'] || '') !== ''; + const ghcupReleaseChannel = parseURL( + 'ghcup-release-channel', + inputs['ghcup-release-channel'] || '' + ); // Andreas, 2023-01-05, issue #29: // 'cabal-update' has a default value, so we should get a proper boolean always. // Andreas, 2023-01-06: This is not true if we use the action as a library. @@ -156,6 +170,9 @@ export function getOpts( ), enable: ghcEnable }, + ghcup: { + releaseChannel: ghcupReleaseChannel + }, cabal: { raw: verInpt.cabal, resolved: resolve( diff --git a/setup/src/setup-haskell.ts b/setup/src/setup-haskell.ts index 25dfed44..4b183d94 100644 --- a/setup/src/setup-haskell.ts +++ b/setup/src/setup-haskell.ts @@ -4,7 +4,7 @@ import * as fs from 'fs'; import * as path from 'path'; import {EOL} from 'os'; import {getOpts, getDefaults, Tool} from './opts'; -import {installTool, resetTool} from './installer'; +import {addGhcupReleaseChannel, installTool, resetTool} from './installer'; import type {OS} from './opts'; import {exec} from '@actions/exec'; @@ -26,6 +26,12 @@ export default async function run( const os = process.platform as OS; const opts = getOpts(getDefaults(os), os, inputs); + if (opts.ghcup.releaseChannel) { + await core.group(`Preparing ghcup environment`, async () => + addGhcupReleaseChannel(opts.ghcup.releaseChannel!, os) + ); + } + for (const [t, {resolved}] of Object.entries(opts).filter( o => o[1].enable )) {