diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 50074e16..12e98a5b 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -41,10 +41,13 @@ jobs: os: [ubuntu-latest, macOS-latest, windows-latest] ghc: ["latest", "8.4.4"] cabal: ["latest", "3.2.0.0"] + cabal_update: ["false"] + # The following tests do not set 'cabal-update', which defaults to 'true' then. include: - os: ubuntu-latest ghc: "8.2.2" cabal: "2.4.1.0" + cabal_update: "false" - os: ubuntu-18.04 ghc: "7.4.1" cabal: "3.4" @@ -68,24 +71,36 @@ jobs: cabal: "3.6" steps: - uses: actions/checkout@v3 + - uses: ./setup with: ghc-version: ${{ matrix.ghc }} cabal-version: ${{ matrix.cabal }} + cabal-update: ${{ matrix.cabal_update }} + - name: Test runghc run: | runghc --version runghc __tests__/hello.hs + - name: Build test project working-directory: setup/__tests__/project run: cabal build + - name: Run test project working-directory: setup/__tests__/project run: cabal run + + - name: Build and run test with Hackage dependency + if: ${{ matrix.cabal_update != 'false' }} + working-directory: setup/__tests__/project-with-hackage-dependency + run: cabal build && cabal run + - name: Show installed versions run: | cabal --version ghc --version + - name: Confirm installed and expected versions match shell: bash # check that if given in the matrix, the actual version matches: diff --git a/setup/README.md b/setup/README.md index 8c08e1ca..db41cd08 100644 --- a/setup/README.md +++ b/setup/README.md @@ -98,18 +98,22 @@ jobs: ## Inputs -| Name | Required | Description | Type | Default | -| ----------------- | :------: | ---------------------------------------------------------------------------------------------------------------------------------------- | --------- | ----------- | -| `ghc-version` | | GHC version to use, ex. `latest` | string | latest | -| `cabal-version` | | Cabal version to use, ex. `3.4` | string | latest | -| `stack-version` | | Stack version to use, ex. `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 | +| 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` | 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. +However, to avoid confusion and for forward compatibility, it is still recommended to **only use value `true` to set a "boolean" flag.** + +In contrast, a proper `boolean` input like `cabal-update` only accepts values `true` and `false`. ## Outputs diff --git a/setup/__tests__/project-with-hackage-dependency/Main.hs b/setup/__tests__/project-with-hackage-dependency/Main.hs new file mode 100644 index 00000000..29319957 --- /dev/null +++ b/setup/__tests__/project-with-hackage-dependency/Main.hs @@ -0,0 +1,4 @@ +module Main where + +main :: IO () +main = putStrLn "Hello, Hackage!" diff --git a/setup/__tests__/project-with-hackage-dependency/Setup.hs b/setup/__tests__/project-with-hackage-dependency/Setup.hs new file mode 100644 index 00000000..9a994af6 --- /dev/null +++ b/setup/__tests__/project-with-hackage-dependency/Setup.hs @@ -0,0 +1,2 @@ +import Distribution.Simple +main = defaultMain diff --git a/setup/__tests__/project-with-hackage-dependency/project-with-hackage-dependency.cabal b/setup/__tests__/project-with-hackage-dependency/project-with-hackage-dependency.cabal new file mode 100644 index 00000000..b6e7719b --- /dev/null +++ b/setup/__tests__/project-with-hackage-dependency/project-with-hackage-dependency.cabal @@ -0,0 +1,11 @@ +cabal-version: >=1.10 +name: project-with-hackage-dependency +version: 0.1.0.0 +build-type: Simple + +executable project + main-is: Main.hs + build-depends: base + -- Add a package from hackage here to see whether `cabal update` ran. + , base-orphans + default-language: Haskell2010 diff --git a/setup/action.yml b/setup/action.yml index 87df2eeb..bc047ec3 100644 --- a/setup/action.yml +++ b/setup/action.yml @@ -16,16 +16,23 @@ inputs: default: 'latest' enable-stack: required: false - description: 'If specified, will setup Stack' + description: 'If specified, will setup Stack.' stack-no-global: required: false - description: 'If specified, enable-stack must be set. Prevents installing GHC and Cabal globally' + description: 'If specified, enable-stack must be set. Prevents installing GHC and Cabal globally.' stack-setup-ghc: required: false - description: 'If specified, enable-stack must be set. Will run stack setup to install the specified GHC' + description: 'If specified, enable-stack must be set. Will run stack setup to install the specified GHC.' + cabal-update: + required: false + default: true + description: 'Set to `false` to prevent `cabal update` from being run.' + # 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. disable-matcher: required: false - description: 'If specified, disables match messages from GHC as GitHub CI annotations' + description: 'If specified, disables match messages from GHC as GitHub CI annotations.' outputs: ghc-path: description: 'The path of the ghc executable _directory_' diff --git a/setup/dist/index.js b/setup/dist/index.js index 000e479b..898a6cac 100644 --- a/setup/dist/index.js +++ b/setup/dist/index.js @@ -13646,7 +13646,7 @@ var __importStar = (this && this.__importStar) || function (mod) { return result; }; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.getOpts = exports.releaseRevision = exports.getDefaults = exports.yamlInputs = exports.ghcup_version = exports.supported_versions = exports.release_revisions = void 0; +exports.getOpts = 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); @@ -13692,12 +13692,38 @@ function releaseRevision(version, tool, os) { return result; } exports.releaseRevision = releaseRevision; +/** + * Convert a string input to a boolean according to the YAML 1.2 "core schema" specification. + * Supported boolean renderings: `true | True | TRUE | false | False | FALSE` . + * ref: https://yaml.org/spec/1.2/spec.html#id2804923 + * Adapted from: https://github.com/actions/toolkit/commit/fbdf27470cdcb52f16755d32082f1fee0bfb7d6d#diff-f63fb32fca85d8e177d6400ce078818a4815b80ac7a3319b60d3507354890992R94-R115 + * + * @param name name of the input + * @param val supposed string representation of a boolean + * @returns boolean + */ +function parseYAMLBoolean(name, val) { + const trueValue = ['true', 'True', 'TRUE']; + const falseValue = ['false', 'False', 'FALSE']; + if (trueValue.includes(val)) + return true; + if (falseValue.includes(val)) + return false; + throw new TypeError(`Action input "${name}" does not meet YAML 1.2 "Core Schema" specification: \n` + + `Supported boolean values: \`true | True | TRUE | false | False | FALSE\``); +} +exports.parseYAMLBoolean = parseYAMLBoolean; 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'] || '') !== ''; + // 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. + // Thus, need to patch with default value here. + const cabalUpdate = parseYAMLBoolean('cabal-update', inputs['cabal-update'] || 'true'); core.debug(`${stackNoGlobal}/${stackSetupGhc}/${stackEnable}`); const verInpt = { ghc: inputs['ghc-version'] || ghc.version, @@ -13727,7 +13753,8 @@ function getOpts({ ghc, cabal, stack }, os, inputs) { raw: verInpt.cabal, resolved: resolve(verInpt.cabal, cabal.supported, 'cabal', os, cabalEnable // if true: inform user about resolution ), - enable: cabalEnable + enable: cabalEnable, + update: cabalUpdate }, stack: { raw: verInpt.stack, @@ -13831,7 +13858,7 @@ async function run(inputs) { // https://github.com/haskell/cabal/issues/6823 // await exec('cabal user-config update'); } - if (!opts.stack.enable) + if (opts.cabal.update && !opts.stack.enable) await (0, exec_1.exec)('cabal update'); }); core.info(`##[add-matcher]${path.join(__dirname, '..', 'matcher.json')}`); diff --git a/setup/lib/opts.d.ts b/setup/lib/opts.d.ts index 0c0937e9..58ab6542 100644 --- a/setup/lib/opts.d.ts +++ b/setup/lib/opts.d.ts @@ -14,7 +14,9 @@ export interface ProgramOpt { } export interface Options { ghc: ProgramOpt; - cabal: ProgramOpt; + cabal: ProgramOpt & { + update: boolean; + }; stack: ProgramOpt & { setup: boolean; }; @@ -40,5 +42,16 @@ export declare const yamlInputs: Record; export declare function getDefaults(os: OS): Defaults; export declare function releaseRevision(version: string, tool: Tool, os: OS): string; +/** + * Convert a string input to a boolean according to the YAML 1.2 "core schema" specification. + * Supported boolean renderings: `true | True | TRUE | false | False | FALSE` . + * ref: https://yaml.org/spec/1.2/spec.html#id2804923 + * Adapted from: https://github.com/actions/toolkit/commit/fbdf27470cdcb52f16755d32082f1fee0bfb7d6d#diff-f63fb32fca85d8e177d6400ce078818a4815b80ac7a3319b60d3507354890992R94-R115 + * + * @param name name of the input + * @param val supposed string representation of a boolean + * @returns boolean + */ +export declare function parseYAMLBoolean(name: string, val: string): boolean; 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 6dce63e3..7ba2f4d3 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.releaseRevision = exports.getDefaults = exports.yamlInputs = exports.ghcup_version = exports.supported_versions = exports.release_revisions = void 0; +exports.getOpts = 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"); @@ -69,12 +69,38 @@ function releaseRevision(version, tool, os) { return result; } exports.releaseRevision = releaseRevision; +/** + * Convert a string input to a boolean according to the YAML 1.2 "core schema" specification. + * Supported boolean renderings: `true | True | TRUE | false | False | FALSE` . + * ref: https://yaml.org/spec/1.2/spec.html#id2804923 + * Adapted from: https://github.com/actions/toolkit/commit/fbdf27470cdcb52f16755d32082f1fee0bfb7d6d#diff-f63fb32fca85d8e177d6400ce078818a4815b80ac7a3319b60d3507354890992R94-R115 + * + * @param name name of the input + * @param val supposed string representation of a boolean + * @returns boolean + */ +function parseYAMLBoolean(name, val) { + const trueValue = ['true', 'True', 'TRUE']; + const falseValue = ['false', 'False', 'FALSE']; + if (trueValue.includes(val)) + return true; + if (falseValue.includes(val)) + return false; + throw new TypeError(`Action input "${name}" does not meet YAML 1.2 "Core Schema" specification: \n` + + `Supported boolean values: \`true | True | TRUE | false | False | FALSE\``); +} +exports.parseYAMLBoolean = parseYAMLBoolean; 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'] || '') !== ''; + // 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. + // Thus, need to patch with default value here. + const cabalUpdate = parseYAMLBoolean('cabal-update', inputs['cabal-update'] || 'true'); core.debug(`${stackNoGlobal}/${stackSetupGhc}/${stackEnable}`); const verInpt = { ghc: inputs['ghc-version'] || ghc.version, @@ -104,7 +130,8 @@ function getOpts({ ghc, cabal, stack }, os, inputs) { raw: verInpt.cabal, resolved: resolve(verInpt.cabal, cabal.supported, 'cabal', os, cabalEnable // if true: inform user about resolution ), - enable: cabalEnable + enable: cabalEnable, + update: cabalUpdate }, stack: { raw: verInpt.stack, diff --git a/setup/lib/setup-haskell.js b/setup/lib/setup-haskell.js index 0955e598..b9438e14 100644 --- a/setup/lib/setup-haskell.js +++ b/setup/lib/setup-haskell.js @@ -79,7 +79,7 @@ async function run(inputs) { // https://github.com/haskell/cabal/issues/6823 // await exec('cabal user-config update'); } - if (!opts.stack.enable) + if (opts.cabal.update && !opts.stack.enable) await (0, exec_1.exec)('cabal update'); }); core.info(`##[add-matcher]${path.join(__dirname, '..', 'matcher.json')}`); diff --git a/setup/src/opts.ts b/setup/src/opts.ts index f9b53dfe..987e037c 100644 --- a/setup/src/opts.ts +++ b/setup/src/opts.ts @@ -24,7 +24,7 @@ export interface ProgramOpt { export interface Options { ghc: ProgramOpt; - cabal: ProgramOpt; + cabal: ProgramOpt & {update: boolean}; stack: ProgramOpt & {setup: boolean}; general: {matcher: {enable: boolean}}; } @@ -83,6 +83,27 @@ export function releaseRevision(version: string, tool: Tool, os: OS): string { return result; } +/** + * Convert a string input to a boolean according to the YAML 1.2 "core schema" specification. + * Supported boolean renderings: `true | True | TRUE | false | False | FALSE` . + * ref: https://yaml.org/spec/1.2/spec.html#id2804923 + * Adapted from: https://github.com/actions/toolkit/commit/fbdf27470cdcb52f16755d32082f1fee0bfb7d6d#diff-f63fb32fca85d8e177d6400ce078818a4815b80ac7a3319b60d3507354890992R94-R115 + * + * @param name name of the input + * @param val supposed string representation of a boolean + * @returns boolean + */ +export function parseYAMLBoolean(name: string, val: string): boolean { + const trueValue = ['true', 'True', 'TRUE']; + const falseValue = ['false', 'False', 'FALSE']; + if (trueValue.includes(val)) return true; + if (falseValue.includes(val)) return false; + throw new TypeError( + `Action input "${name}" does not meet YAML 1.2 "Core Schema" specification: \n` + + `Supported boolean values: \`true | True | TRUE | false | False | FALSE\`` + ); +} + export function getOpts( {ghc, cabal, stack}: Defaults, os: OS, @@ -93,6 +114,14 @@ export function getOpts( const stackSetupGhc = (inputs['stack-setup-ghc'] || '') !== ''; const stackEnable = (inputs['enable-stack'] || '') !== ''; const matcherDisable = (inputs['disable-matcher'] || '') !== ''; + // 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. + // Thus, need to patch with default value here. + const cabalUpdate = parseYAMLBoolean( + 'cabal-update', + inputs['cabal-update'] || 'true' + ); core.debug(`${stackNoGlobal}/${stackSetupGhc}/${stackEnable}`); const verInpt = { ghc: inputs['ghc-version'] || ghc.version, @@ -136,7 +165,8 @@ export function getOpts( os, cabalEnable // if true: inform user about resolution ), - enable: cabalEnable + enable: cabalEnable, + update: cabalUpdate }, stack: { raw: verInpt.stack, diff --git a/setup/src/setup-haskell.ts b/setup/src/setup-haskell.ts index 9e5c5ab3..077c0c6b 100644 --- a/setup/src/setup-haskell.ts +++ b/setup/src/setup-haskell.ts @@ -73,7 +73,7 @@ export default async function run( // https://github.com/haskell/cabal/issues/6823 // await exec('cabal user-config update'); } - if (!opts.stack.enable) await exec('cabal update'); + if (opts.cabal.update && !opts.stack.enable) await exec('cabal update'); }); core.info(`##[add-matcher]${path.join(__dirname, '..', 'matcher.json')}`);