diff --git a/.github/.jira_sync_config.yaml b/.github/.jira_sync_config.yaml new file mode 100644 index 00000000..f897418d --- /dev/null +++ b/.github/.jira_sync_config.yaml @@ -0,0 +1,16 @@ +# See https://github.com/canonical/gh-jira-sync-bot for config +settings: + jira_project_key: "ISD" + + status_mapping: + opened: Untriaged + closed: done + not_planned: rejected + + add_gh_comment: true + + epic_key: ISD-3981 + + label_mapping: + bug: Bug + enhancement: Story diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..a300d58f --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,21 @@ +Applicable spec: + +### Overview + + + +### Rationale + + + +### Module Changes + + + +### Checklist + +- [ ] The [contributing guide](https://github.com/canonical/is-charms-contributing-guide) was applied +- [ ] The documentation on README.md is updated. +- [ ] The PR is tagged with appropriate label (`urgent`, `trivial`, `complex`) + + diff --git a/.github/workflows/checkin.yml b/.github/workflows/checkin.yml index fe74feb7..88f01383 100644 --- a/.github/workflows/checkin.yml +++ b/.github/workflows/checkin.yml @@ -8,11 +8,19 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Use Node.js uses: actions/setup-node@v3 with: node-version: 20 + - name: Verify that action.yml files are in sync + run: | + npm run update-detached-action.yml && + if ! git diff --exit-code \*action.yml + then + echo '::error::action.yml files are not in sync, maybe run `npm run update-detached-action.yml`?' + exit 1 + fi - name: Install dependencies run: npm ci - name: Run tests @@ -24,7 +32,8 @@ jobs: - name: Verify that the project is built run: | if [[ -n $(git status -s) ]]; then - echo "ERROR: generated dist/ differs from the current sources" + echo "ERROR: generated lib/ differs from the current sources" + git status -s git diff exit 1 fi diff --git a/.github/workflows/manual-detached-test.yml b/.github/workflows/manual-detached-test.yml index a623483f..f3efbf90 100644 --- a/.github/workflows/manual-detached-test.yml +++ b/.github/workflows/manual-detached-test.yml @@ -5,11 +5,10 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: ./ + - uses: actions/checkout@v4 + - uses: ./detached with: - limit-access-to-actor: true - detached: true + connect-timeout-seconds: 60 - run: | echo "A busy loop" for value in $(seq 10) diff --git a/.github/workflows/manual-test.yml b/.github/workflows/manual-test.yml index bf1b27bd..a68f773f 100644 --- a/.github/workflows/manual-test.yml +++ b/.github/workflows/manual-test.yml @@ -1,43 +1,73 @@ -name: Manual test matrix -on: workflow_dispatch +name: Manual test +on: + workflow_dispatch: + inputs: + runs-on: + type: choice + description: 'The runner pool to run the job on' + required: true + default: ubuntu-24.04 + options: + - ubuntu-24.04 + - ubuntu-22.04 + - macos-15-large + - macos-15 + - macos-14-large + - macos-14 + - macos-13 + - macos-13-xlarge + - windows-2025 + - windows-2022 + - windows-2019 + - windows-11-arm + container-runs-on: + type: choice + description: 'The Docker container to run the job on (this overrides the `runs-on` input)' + required: false + default: '(none)' + options: + - '(none)' + - fedora:latest + - archlinux:latest + - ubuntu:latest + limit-access-to-actor: + type: choice + description: 'Whether to limit access to the actor only' + required: true + default: 'auto' + options: + - auto + - 'true' + - 'false' jobs: test: - strategy: - fail-fast: false - matrix: - runs-on: - - ubuntu-20.04 - - ubuntu-22.04 - - macos-11.0 - - macos-12.0 - - windows-2019 - - windows-2022 - limit-access-to-actor: - - true - - false - runs-on: ${{ matrix.runs-on }} + if: ${{ inputs.container-runs-on == '(none)' }} + runs-on: ${{ inputs.runs-on }} steps: - - uses: actions/checkout@v3 + - uses: msys2/setup-msys2@v2 + # The public preview of GitHub-hosted Windows/ARM64 runners lacks + # a working MSYS2 installation, so we need to set it up ourselves. + if: ${{ inputs.runs-on == 'windows-11-arm' }} + with: + msystem: 'CLANGARM64' + # We cannot use `C:\` because `msys2/setup-msys2` erroneously + # believes that an MSYS2 exists at `C:\msys64`, but it doesn't, + # which is the entire reason why we need to set it up in this + # here step... However, by using `C:\.\` we can fool that + # overzealous check. + location: C:\.\ + - uses: actions/checkout@v4 - uses: ./ with: - limit-access-to-actor: ${{ matrix.limit-access-to-actor }} + limit-access-to-actor: ${{ inputs.limit-access-to-actor }} test-container: - strategy: - fail-fast: false - matrix: - container-runs-on: - - fedora:latest - - archlinux:latest - - ubuntu:latest - limit-access-to-actor: - - true - - false + if: ${{ inputs.container-runs-on != '(none)' }} runs-on: ubuntu-latest container: - image: ${{ matrix.container-runs-on }} + image: ${{ inputs.container-runs-on }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: ./ with: - limit-access-to-actor: ${{ matrix.limit-access-to-actor }} + limit-access-to-actor: ${{ inputs.limit-access-to-actor }} diff --git a/.github/workflows/tmate.yaml b/.github/workflows/tmate.yaml new file mode 100644 index 00000000..8091f6db --- /dev/null +++ b/.github/workflows/tmate.yaml @@ -0,0 +1,13 @@ +name: Canonical action-tmate test + +on: + push: + +jobs: + build: + runs-on: [self-hosted, linux, X64, jammy, large] + steps: + - name: Setup tmate session + uses: canonical/action-tmate@main + with: + detached: true diff --git a/.github/workflows/update-manual-test.js b/.github/workflows/update-manual-test.js new file mode 100644 index 00000000..5f173370 --- /dev/null +++ b/.github/workflows/update-manual-test.js @@ -0,0 +1,62 @@ +#!/usr/bin/env node + +// Update the `runs-on` options of the `manual-test.yml` workflow file with the +// latest available images from the GitHub Actions runner images README file. + +(async () => { + const fs = require('fs') + + const readme = await (await fetch("https://github.com/actions/runner-images/raw/HEAD/README.md")).text() + + // This will be the first `ubuntu` one. + let defaultOption = '' + + const choices = readme + // Get the "Available Images" section + .split(/\n## Available Images\n/)[1] + .split(/##\s*[^#]/)[0] + // Split by lines + .split('\n') + .map(line => { + // The relevant lines are table rows; The first column is the image name, + // the second one contains a relatively free-form list of the `runs-on` + // options that we are interested in. Those `runs-on` options are + // surrounded by backticks. + const match = line.match(/^\|\s*([^|]+)\s*\|([^|]*)`([^`|]+)`\s*\|/) + if (!match) return false // Skip e.g. the table header and empty lines + let runsOn = match[3] // default to the last `runs-on` option + const alternatives = match[2] + .split(/`([^`]*)`/) // split by backticks + .filter((_, i) => (i % 2)) // keep only the text between backticks + .sort((a, b) => a.length - b.length) // order by length + if (alternatives.length > 0 && alternatives[0].length < runsOn.length) runsOn = alternatives[0] + if (!defaultOption && match[3].startsWith('ubuntu-')) defaultOption = runsOn + return runsOn + }) + .filter(runsOn => runsOn) + + // The Windows/ARM64 runners are in public preview (and for the time being, + // not listed in the `runner-images` README file), so we need to add this + // manually. + if (!choices.includes('windows-11-arm')) choices.push('windows-11-arm') + + // Now edit the `manual-test` workflow definition + const ymlPath = `${__dirname}/manual-test.yml` + const yml = fs.readFileSync(ymlPath, 'utf8') + + // We want to replace the `runs-on` options and the `default` value. This + // would be easy if there was a built-in YAML parser and renderer in Node.js, + // but there is none. Therefore, we use a regular expression to find certain + // "needles" near the beginning of the file: first `workflow_dispatch:`, + // after that `runs-on:` and then `default:` and `options:`. Then we replace + // the `default` value and the `options` values with the new ones. + const [, beforeDefault, beforeOptions, optionsIndent, afterOptions] = + yml.match(/^([^]*?workflow_dispatch:[^]*?runs-on:[^]*?default:)(?:.*)([^]*?options:)(\n +- )(?:.*)(?:\3.*)*([^]*)/) || [] + if (!beforeDefault) throw new Error(`The 'manual-test.yml' file does not match the expected format!`) + const newYML = + `${beforeDefault} ${defaultOption}${[beforeOptions, ...choices].join(optionsIndent)}${afterOptions}` + fs.writeFileSync(ymlPath, newYML) +})().catch(e => { + console.error(e) + process.exitCode = 1 +}) \ No newline at end of file diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 00000000..e9fadb72 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1 @@ +* @canonical/platform-engineering diff --git a/README.md b/README.md index af43bc6e..7b4039c5 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ This is a forked version of [action-tmate](https://github.com/mxschmitt/action-t be used with [GitHub Runner Operator](https://github.com/canonical/github-runner-operator/) to provide automatic SSH debug access within the Canonical VPN. +You must have your SSH Key [registered on GitHub](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/adding-a-new-ssh-key-to-your-github-account) to be able to connect. + [![GitHub Actions](https://github.com/canonical/action-tmate/workflows/Node.js%20CI/badge.svg)](https://github.com/canonical/action-tmate/actions) [![GitHub Marketplace](https://img.shields.io/badge/GitHub-Marketplace-green)](https://github.com/marketplace/actions/debugging-with-tmate) @@ -31,9 +33,9 @@ jobs: build: runs-on: self-hosted steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup tmate session - uses: canonical/action-tmate@master + uses: canonical/action-tmate@main ``` To get the connection string, just open the `Checks` tab in your Pull Request and scroll to the bottom. There you can connect either directly per SSH or via a web based terminal. @@ -69,7 +71,7 @@ jobs: steps: # Enable tmate debugging of manually-triggered workflows if the input option was provided - name: Setup tmate session - uses: canonical/action-tmate@mxschmitt + uses: canonical/action-tmate@main if: ${{ github.event_name == 'workflow_dispatch' && inputs.debug_enabled }} ``` * (any, kinda silly) -// ~2, ~2.x, ~2.x.x, ~>2, ~>2.x ~>2.x.x --> >=2.0.0 <3.0.0 -// ~2.0, ~2.0.x, ~>2.0, ~>2.0.x --> >=2.0.0 <2.1.0 -// ~1.2, ~1.2.x, ~>1.2, ~>1.2.x --> >=1.2.0 <1.3.0 -// ~1.2.3, ~>1.2.3 --> >=1.2.3 <1.3.0 -// ~1.2.0, ~>1.2.0 --> >=1.2.0 <1.3.0 -function replaceTildes (comp, options) { - return comp.trim().split(/\s+/).map(function (comp) { - return replaceTilde(comp, options) - }).join(' ') -} - -function replaceTilde (comp, options) { - var r = options.loose ? safeRe[t.TILDELOOSE] : safeRe[t.TILDE] - return comp.replace(r, function (_, M, m, p, pr) { - debug('tilde', comp, _, M, m, p, pr) - var ret - - if (isX(M)) { - ret = '' - } else if (isX(m)) { - ret = '>=' + M + '.0.0 <' + (+M + 1) + '.0.0' - } else if (isX(p)) { - // ~1.2 == >=1.2.0 <1.3.0 - ret = '>=' + M + '.' + m + '.0 <' + M + '.' + (+m + 1) + '.0' - } else if (pr) { - debug('replaceTilde pr', pr) - ret = '>=' + M + '.' + m + '.' + p + '-' + pr + - ' <' + M + '.' + (+m + 1) + '.0' - } else { - // ~1.2.3 == >=1.2.3 <1.3.0 - ret = '>=' + M + '.' + m + '.' + p + - ' <' + M + '.' + (+m + 1) + '.0' - } + if (kind === "error") { + hook = function (method, options) { + return Promise.resolve() + .then(method.bind(null, options)) + .catch(function (error) { + return orig(error, options); + }); + }; + } - debug('tilde return', ret) - return ret - }) + state.registry[name].push({ + hook: hook, + orig: orig, + }); } -// ^ --> * (any, kinda silly) -// ^2, ^2.x, ^2.x.x --> >=2.0.0 <3.0.0 -// ^2.0, ^2.0.x --> >=2.0.0 <3.0.0 -// ^1.2, ^1.2.x --> >=1.2.0 <2.0.0 -// ^1.2.3 --> >=1.2.3 <2.0.0 -// ^1.2.0 --> >=1.2.0 <2.0.0 -function replaceCarets (comp, options) { - return comp.trim().split(/\s+/).map(function (comp) { - return replaceCaret(comp, options) - }).join(' ') -} -function replaceCaret (comp, options) { - debug('caret', comp, options) - var r = options.loose ? safeRe[t.CARETLOOSE] : safeRe[t.CARET] - return comp.replace(r, function (_, M, m, p, pr) { - debug('caret', comp, _, M, m, p, pr) - var ret - - if (isX(M)) { - ret = '' - } else if (isX(m)) { - ret = '>=' + M + '.0.0 <' + (+M + 1) + '.0.0' - } else if (isX(p)) { - if (M === '0') { - ret = '>=' + M + '.' + m + '.0 <' + M + '.' + (+m + 1) + '.0' - } else { - ret = '>=' + M + '.' + m + '.0 <' + (+M + 1) + '.0.0' - } - } else if (pr) { - debug('replaceCaret pr', pr) - if (M === '0') { - if (m === '0') { - ret = '>=' + M + '.' + m + '.' + p + '-' + pr + - ' <' + M + '.' + m + '.' + (+p + 1) - } else { - ret = '>=' + M + '.' + m + '.' + p + '-' + pr + - ' <' + M + '.' + (+m + 1) + '.0' - } - } else { - ret = '>=' + M + '.' + m + '.' + p + '-' + pr + - ' <' + (+M + 1) + '.0.0' - } - } else { - debug('no pr') - if (M === '0') { - if (m === '0') { - ret = '>=' + M + '.' + m + '.' + p + - ' <' + M + '.' + m + '.' + (+p + 1) - } else { - ret = '>=' + M + '.' + m + '.' + p + - ' <' + M + '.' + (+m + 1) + '.0' - } - } else { - ret = '>=' + M + '.' + m + '.' + p + - ' <' + (+M + 1) + '.0.0' - } - } +/***/ }), - debug('caret return', ret) - return ret - }) -} +/***/ 4670: +/***/ ((module) => { -function replaceXRanges (comp, options) { - debug('replaceXRanges', comp, options) - return comp.split(/\s+/).map(function (comp) { - return replaceXRange(comp, options) - }).join(' ') -} +module.exports = register; -function replaceXRange (comp, options) { - comp = comp.trim() - var r = options.loose ? safeRe[t.XRANGELOOSE] : safeRe[t.XRANGE] - return comp.replace(r, function (ret, gtlt, M, m, p, pr) { - debug('xRange', comp, ret, gtlt, M, m, p, pr) - var xM = isX(M) - var xm = xM || isX(m) - var xp = xm || isX(p) - var anyX = xp - - if (gtlt === '=' && anyX) { - gtlt = '' - } +function register(state, name, method, options) { + if (typeof method !== "function") { + throw new Error("method for before hook must be a function"); + } - // if we're including prereleases in the match, then we need - // to fix this to -0, the lowest possible prerelease value - pr = options.includePrerelease ? '-0' : '' + if (!options) { + options = {}; + } - if (xM) { - if (gtlt === '>' || gtlt === '<') { - // nothing is allowed - ret = '<0.0.0-0' - } else { - // nothing is forbidden - ret = '*' - } - } else if (gtlt && anyX) { - // we know patch is an x, because we have any x at all. - // replace X with 0 - if (xm) { - m = 0 - } - p = 0 - - if (gtlt === '>') { - // >1 => >=2.0.0 - // >1.2 => >=1.3.0 - // >1.2.3 => >= 1.2.4 - gtlt = '>=' - if (xm) { - M = +M + 1 - m = 0 - p = 0 - } else { - m = +m + 1 - p = 0 - } - } else if (gtlt === '<=') { - // <=0.7.x is actually <0.8.0, since any 0.7.x should - // pass. Similarly, <=7.x is actually <8.0.0, etc. - gtlt = '<' - if (xm) { - M = +M + 1 - } else { - m = +m + 1 - } - } + if (Array.isArray(name)) { + return name.reverse().reduce(function (callback, name) { + return register.bind(null, state, name, callback, options); + }, method)(); + } - ret = gtlt + M + '.' + m + '.' + p + pr - } else if (xm) { - ret = '>=' + M + '.0.0' + pr + ' <' + (+M + 1) + '.0.0' + pr - } else if (xp) { - ret = '>=' + M + '.' + m + '.0' + pr + - ' <' + M + '.' + (+m + 1) + '.0' + pr + return Promise.resolve().then(function () { + if (!state.registry[name]) { + return method(options); } - debug('xRange return', ret) - - return ret - }) + return state.registry[name].reduce(function (method, registered) { + return registered.hook.bind(null, method, options); + }, method)(); + }); } -// Because * is AND-ed with everything else in the comparator, -// and '' means "any version", just remove the *s entirely. -function replaceStars (comp, options) { - debug('replaceStars', comp, options) - // Looseness is ignored here. star is always as loose as it gets! - return comp.trim().replace(safeRe[t.STAR], '') -} -// This function is passed to string.replace(re[t.HYPHENRANGE]) -// M, m, patch, prerelease, build -// 1.2 - 3.4.5 => >=1.2.0 <=3.4.5 -// 1.2.3 - 3.4 => >=1.2.0 <3.5.0 Any 3.4.x will do -// 1.2 - 3.4 => >=1.2.0 <3.5.0 -function hyphenReplace ($0, - from, fM, fm, fp, fpr, fb, - to, tM, tm, tp, tpr, tb) { - if (isX(fM)) { - from = '' - } else if (isX(fm)) { - from = '>=' + fM + '.0.0' - } else if (isX(fp)) { - from = '>=' + fM + '.' + fm + '.0' - } else { - from = '>=' + from - } +/***/ }), - if (isX(tM)) { - to = '' - } else if (isX(tm)) { - to = '<' + (+tM + 1) + '.0.0' - } else if (isX(tp)) { - to = '<' + tM + '.' + (+tm + 1) + '.0' - } else if (tpr) { - to = '<=' + tM + '.' + tm + '.' + tp + '-' + tpr - } else { - to = '<=' + to - } +/***/ 6819: +/***/ ((module) => { - return (from + ' ' + to).trim() -} +module.exports = removeHook; -// if ANY of the sets match ALL of its comparators, then pass -Range.prototype.test = function (version) { - if (!version) { - return false +function removeHook(state, name, method) { + if (!state.registry[name]) { + return; } - if (typeof version === 'string') { - try { - version = new SemVer(version, this.options) - } catch (er) { - return false - } - } + var index = state.registry[name] + .map(function (registered) { + return registered.orig; + }) + .indexOf(method); - for (var i = 0; i < this.set.length; i++) { - if (testSet(this.set[i], version, this.options)) { - return true - } + if (index === -1) { + return; } - return false + + state.registry[name].splice(index, 1); } -function testSet (set, version, options) { - for (var i = 0; i < set.length; i++) { - if (!set[i].test(version)) { - return false - } - } - if (version.prerelease.length && !options.includePrerelease) { - // Find the set of versions that are allowed to have prereleases - // For example, ^1.2.3-pr.1 desugars to >=1.2.3-pr.1 <2.0.0 - // That should allow `1.2.3-pr.2` to pass. - // However, `1.2.4-alpha.notready` should NOT be allowed, - // even though it's within the range set by the comparators. - for (i = 0; i < set.length; i++) { - debug(set[i].semver) - if (set[i].semver === ANY) { - continue - } +/***/ }), - if (set[i].semver.prerelease.length > 0) { - var allowed = set[i].semver - if (allowed.major === version.major && - allowed.minor === version.minor && - allowed.patch === version.patch) { - return true - } - } - } +/***/ 8932: +/***/ ((__unused_webpack_module, exports) => { - // Version has a -pre, but it's not one of the ones we like. - return false - } +"use strict"; - return true -} -exports.satisfies = satisfies -function satisfies (version, range, options) { - try { - range = new Range(range, options) - } catch (er) { - return false - } - return range.test(version) -} +Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.maxSatisfying = maxSatisfying -function maxSatisfying (versions, range, options) { - var max = null - var maxSV = null - try { - var rangeObj = new Range(range, options) - } catch (er) { - return null - } - versions.forEach(function (v) { - if (rangeObj.test(v)) { - // satisfies(v, range, options) - if (!max || maxSV.compare(v) === -1) { - // compare(max, v, true) - max = v - maxSV = new SemVer(max, options) - } - } - }) - return max -} +class Deprecation extends Error { + constructor(message) { + super(message); // Maintains proper stack trace (only available on V8) -exports.minSatisfying = minSatisfying -function minSatisfying (versions, range, options) { - var min = null - var minSV = null - try { - var rangeObj = new Range(range, options) - } catch (er) { - return null - } - versions.forEach(function (v) { - if (rangeObj.test(v)) { - // satisfies(v, range, options) - if (!min || minSV.compare(v) === 1) { - // compare(min, v, true) - min = v - minSV = new SemVer(min, options) - } - } - }) - return min -} + /* istanbul ignore next */ -exports.minVersion = minVersion -function minVersion (range, loose) { - range = new Range(range, loose) + if (Error.captureStackTrace) { + Error.captureStackTrace(this, this.constructor); + } - var minver = new SemVer('0.0.0') - if (range.test(minver)) { - return minver + this.name = 'Deprecation'; } - minver = new SemVer('0.0.0-0') - if (range.test(minver)) { - return minver - } +} - minver = null - for (var i = 0; i < range.set.length; ++i) { - var comparators = range.set[i] - - comparators.forEach(function (comparator) { - // Clone to avoid manipulating the comparator's semver object. - var compver = new SemVer(comparator.semver.version) - switch (comparator.operator) { - case '>': - if (compver.prerelease.length === 0) { - compver.patch++ - } else { - compver.prerelease.push(0) - } - compver.raw = compver.format() - /* fallthrough */ - case '': - case '>=': - if (!minver || gt(minver, compver)) { - minver = compver - } - break - case '<': - case '<=': - /* Ignore maximum versions */ - break - /* istanbul ignore next */ - default: - throw new Error('Unexpected operation: ' + comparator.operator) - } - }) - } +exports.Deprecation = Deprecation; - if (minver && range.test(minver)) { - return minver - } - return null -} +/***/ }), -exports.validRange = validRange -function validRange (range, options) { - try { - // Return '*' instead of '' so that truthiness works. - // This will throw if it's invalid anyway - return new Range(range, options).range || '*' - } catch (er) { - return null - } -} +/***/ 3287: +/***/ ((__unused_webpack_module, exports) => { -// Determine if version is less than all the versions possible in the range -exports.ltr = ltr -function ltr (version, range, options) { - return outside(version, range, '<', options) -} +"use strict"; -// Determine if version is greater than all the versions possible in the range. -exports.gtr = gtr -function gtr (version, range, options) { - return outside(version, range, '>', options) -} -exports.outside = outside -function outside (version, range, hilo, options) { - version = new SemVer(version, options) - range = new Range(range, options) - - var gtfn, ltefn, ltfn, comp, ecomp - switch (hilo) { - case '>': - gtfn = gt - ltefn = lte - ltfn = lt - comp = '>' - ecomp = '>=' - break - case '<': - gtfn = lt - ltefn = gte - ltfn = gt - comp = '<' - ecomp = '<=' - break - default: - throw new TypeError('Must provide a hilo val of "<" or ">"') - } +Object.defineProperty(exports, "__esModule", ({ value: true })); - // If it satisifes the range it is not outside - if (satisfies(version, range, options)) { - return false - } +/*! + * is-plain-object + * + * Copyright (c) 2014-2017, Jon Schlinkert. + * Released under the MIT License. + */ - // From now on, variable terms are as if we're in "gtr" mode. - // but note that everything is flipped for the "ltr" function. +function isObject(o) { + return Object.prototype.toString.call(o) === '[object Object]'; +} - for (var i = 0; i < range.set.length; ++i) { - var comparators = range.set[i] +function isPlainObject(o) { + var ctor,prot; - var high = null - var low = null + if (isObject(o) === false) return false; - comparators.forEach(function (comparator) { - if (comparator.semver === ANY) { - comparator = new Comparator('>=0.0.0') - } - high = high || comparator - low = low || comparator - if (gtfn(comparator.semver, high.semver, options)) { - high = comparator - } else if (ltfn(comparator.semver, low.semver, options)) { - low = comparator - } - }) + // If has modified constructor + ctor = o.constructor; + if (ctor === undefined) return true; - // If the edge version comparator has a operator then our version - // isn't outside it - if (high.operator === comp || high.operator === ecomp) { - return false - } + // If has modified prototype + prot = ctor.prototype; + if (isObject(prot) === false) return false; - // If the lowest version comparator has an operator and our version - // is less than it then it isn't higher than the range - if ((!low.operator || low.operator === comp) && - ltefn(version, low.semver)) { - return false - } else if (low.operator === ecomp && ltfn(version, low.semver)) { - return false - } + // If constructor does not have an Object-specific method + if (prot.hasOwnProperty('isPrototypeOf') === false) { + return false; } - return true -} -exports.prerelease = prerelease -function prerelease (version, options) { - var parsed = parse(version, options) - return (parsed && parsed.prerelease.length) ? parsed.prerelease : null + // Most likely a plain Object + return true; } -exports.intersects = intersects -function intersects (r1, r2, options) { - r1 = new Range(r1, options) - r2 = new Range(r2, options) - return r1.intersects(r2) -} +exports.isPlainObject = isPlainObject; -exports.coerce = coerce -function coerce (version, options) { - if (version instanceof SemVer) { - return version - } - if (typeof version === 'number') { - version = String(version) - } +/***/ }), - if (typeof version !== 'string') { - return null - } +/***/ 1223: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { - options = options || {} +var wrappy = __nccwpck_require__(2940) +module.exports = wrappy(once) +module.exports.strict = wrappy(onceStrict) - var match = null - if (!options.rtl) { - match = version.match(safeRe[t.COERCE]) - } else { - // Find the right-most coercible string that does not share - // a terminus with a more left-ward coercible string. - // Eg, '1.2.3.4' wants to coerce '2.3.4', not '3.4' or '4' - // - // Walk through the string checking with a /g regexp - // Manually set the index so as to pick up overlapping matches. - // Stop when we get a match that ends at the string end, since no - // coercible string can be more right-ward without the same terminus. - var next - while ((next = safeRe[t.COERCERTL].exec(version)) && - (!match || match.index + match[0].length !== version.length) - ) { - if (!match || - next.index + next[0].length !== match.index + match[0].length) { - match = next - } - safeRe[t.COERCERTL].lastIndex = next.index + next[1].length + next[2].length - } - // leave it in a clean state - safeRe[t.COERCERTL].lastIndex = -1 - } +once.proto = once(function () { + Object.defineProperty(Function.prototype, 'once', { + value: function () { + return once(this) + }, + configurable: true + }) + + Object.defineProperty(Function.prototype, 'onceStrict', { + value: function () { + return onceStrict(this) + }, + configurable: true + }) +}) - if (match === null) { - return null +function once (fn) { + var f = function () { + if (f.called) return f.value + f.called = true + return f.value = fn.apply(this, arguments) } + f.called = false + return f +} - return parse(match[2] + - '.' + (match[3] || '0') + - '.' + (match[4] || '0'), options) +function onceStrict (fn) { + var f = function () { + if (f.called) + throw new Error(f.onceError) + f.called = true + return f.value = fn.apply(this, arguments) + } + var name = fn.name || 'Function wrapped with `once`' + f.onceError = name + " shouldn't be called more than once" + f.called = false + return f } @@ -17066,14 +13245,6 @@ module.exports = require("assert"); /***/ }), -/***/ 2081: -/***/ ((module) => { - -"use strict"; -module.exports = require("child_process"); - -/***/ }), - /***/ 6113: /***/ ((module) => { @@ -17279,28 +13450,26 @@ var __webpack_exports__ = {}; // ESM COMPAT FLAG __nccwpck_require__.r(__webpack_exports__); -// EXTERNAL MODULE: external "os" -var external_os_ = __nccwpck_require__(2037); -var external_os_default = /*#__PURE__*/__nccwpck_require__.n(external_os_); -// EXTERNAL MODULE: external "fs" -var external_fs_ = __nccwpck_require__(7147); -var external_fs_default = /*#__PURE__*/__nccwpck_require__.n(external_fs_); -// EXTERNAL MODULE: external "path" -var external_path_ = __nccwpck_require__(1017); -var external_path_default = /*#__PURE__*/__nccwpck_require__.n(external_path_); // EXTERNAL MODULE: ./node_modules/@actions/core/lib/core.js var core = __nccwpck_require__(2186); // EXTERNAL MODULE: ./node_modules/@actions/github/lib/github.js var github = __nccwpck_require__(5438); -// EXTERNAL MODULE: ./node_modules/@actions/tool-cache/lib/tool-cache.js -var tool_cache = __nccwpck_require__(7784); // EXTERNAL MODULE: ./node_modules/@octokit/rest/dist-node/index.js var dist_node = __nccwpck_require__(5375); +// EXTERNAL MODULE: external "fs" +var external_fs_ = __nccwpck_require__(7147); +var external_fs_default = /*#__PURE__*/__nccwpck_require__.n(external_fs_); +// EXTERNAL MODULE: external "os" +var external_os_ = __nccwpck_require__(2037); +var external_os_default = /*#__PURE__*/__nccwpck_require__.n(external_os_); +// EXTERNAL MODULE: external "path" +var external_path_ = __nccwpck_require__(1017); +var external_path_default = /*#__PURE__*/__nccwpck_require__.n(external_path_); ;// CONCATENATED MODULE: external "process" const external_process_namespaceObject = require("process"); var external_process_default = /*#__PURE__*/__nccwpck_require__.n(external_process_namespaceObject); -// EXTERNAL MODULE: external "child_process" -var external_child_process_ = __nccwpck_require__(2081); +;// CONCATENATED MODULE: external "child_process" +const external_child_process_namespaceObject = require("child_process"); ;// CONCATENATED MODULE: ./src/helpers.js // @ts-check @@ -17326,14 +13495,14 @@ const execShellCommand = (cmd, options) => { core.debug(`Executing shell command: [${cmd}]`) return new Promise((resolve, reject) => { const proc = (external_process_default()).platform !== "win32" ? - (0,external_child_process_.spawn)(cmd, [], { + (0,external_child_process_namespaceObject.spawn)(cmd, [], { shell: true, env: { ...(external_process_default()).env, HOMEBREW_GITHUB_API_TOKEN: core.getInput('github-token') || undefined } }) : - (0,external_child_process_.spawn)("C:\\msys64\\usr\\bin\\bash.exe", ["-lc", cmd], { + (0,external_child_process_namespaceObject.spawn)(`${core.getInput("msys2-location") || "C:\\msys64"}\\usr\\bin\\bash.exe`, ["-lc", cmd], { env: { ...(external_process_default()).env, "MSYS2_PATH_TYPE": "inherit", /* Inherit previous path */ @@ -17367,9 +13536,10 @@ const execShellCommand = (cmd, options) => { * @return {string|undefined} {undefined} or throws an error if input doesn't match regex */ const getValidatedEnvVars = (key, re) => { - const value = (external_process_default()).env[key] || "" + const envVarKey = key.toUpperCase().replace(/-/gi, "_") + const value = (external_process_default()).env[envVarKey] || "" if (value !== undefined && !re.test(value)) { - throw new Error(`Invalid value for '${key}': '${value}'`); + throw new Error(`Invalid value for '${key}(${envVarKey})': '${value}'`); } return value; } @@ -17380,7 +13550,7 @@ const getValidatedEnvVars = (key, re) => { */ const getLinuxDistro = async () => { try { - const osRelease = await external_fs_default().promises.readFile("/etc/os-release") + const osRelease = await fs.promises.readFile("/etc/os-release") const match = osRelease.toString().match(/^ID=(.*)$/m) return match ? match[1] : "(unknown)" } catch (e) { @@ -17400,17 +13570,20 @@ const getLinuxDistro = async () => { - -const TMATE_LINUX_VERSION = "2.4.0" - // Map os.arch() values to the architectures in tmate release binary filenames. // Possible os.arch() values documented here: // https://nodejs.org/api/os.html#os_os_arch // Available tmate binaries listed here: -// https://github.com/tmate-io/tmate/releases/ +// https://packages.ubuntu.com/jammy/tmate +// For different Ubuntu releases, change the release (i.e. jammy) to the +// appropriate release. const TMATE_ARCH_MAP = { arm64: 'arm64v8', + armhf: 'armhf', x64: 'amd64', + ppc64: 'ppc64', + riscv64: 'riscv64', + s390x: 's390x' }; /** @param {number} ms */ @@ -17441,12 +13614,17 @@ async function run() { && '0' !== await execShellCommand(`${tmate} display -p '#{tmate_num_clients}'`, { quiet: true }) } })() - for (let seconds = 10 * 60; seconds > 0; ) { - console.log(`${ - await hasAnyoneConnectedYet() + + let connectTimeoutSeconds = parseInt(core.getInput("connect-timeout-seconds")) + if (Number.isNaN(connectTimeoutSeconds) || connectTimeoutSeconds <= 0) { + connectTimeoutSeconds = 10 * 60 + } + + for (let seconds = connectTimeoutSeconds; seconds > 0;) { + console.log(`${await hasAnyoneConnectedYet() ? 'Waiting for session to end' : `Waiting for client to connect (at most ${seconds} more second(s))` - }\n${message}`) + }\n${message}`) if (continueFileExists()) { core.info("Exiting debugging session because the continue file was created") @@ -17468,43 +13646,31 @@ async function run() { let tmateExecutable = "tmate" if (core.getInput("install-dependencies") !== "false") { core.debug("Installing dependencies") - if ((external_process_default()).platform === "darwin") { - await execShellCommand('brew install tmate'); - } else if ((external_process_default()).platform === "win32") { - await execShellCommand('pacman -S --noconfirm tmate'); - } else { - const optionalSudoPrefix = useSudoPrefix() ? "sudo " : ""; - const distro = await getLinuxDistro(); - core.debug("linux distro: [" + distro + "]"); - if (distro === "alpine") { - // for set -e workaround, we need to install bash because alpine doesn't have it - await execShellCommand(optionalSudoPrefix + 'apk add openssh-client xz bash'); - } else if (distro === "arch") { - // partial upgrades are not supported so also upgrade everything - await execShellCommand(optionalSudoPrefix + 'pacman -Syu --noconfirm xz openssh'); - } else if (distro === "fedora") { - await execShellCommand(optionalSudoPrefix + 'dnf install -y xz openssh'); - } else { - await execShellCommand(optionalSudoPrefix + 'apt-get update'); - await execShellCommand(optionalSudoPrefix + 'apt-get install -y openssh-client xz-utils'); - } + const optionalSudoPrefix = useSudoPrefix() ? "sudo " : ""; + await execShellCommand(optionalSudoPrefix + 'apt-get update'); + await execShellCommand(optionalSudoPrefix + 'apt-get install -y openssh-client xz-utils'); + await execShellCommand(optionalSudoPrefix + 'apt-get install -y tmate'); + + const tmateArch = TMATE_ARCH_MAP[external_os_default().arch()]; + if (!tmateArch) { + throw new Error(`Unsupported architecture: ${external_os_default().arch()}`) + } + // We change from downloading tmate from source built tar from GitHub to the + // Ubuntu packages tmate binary. Hence we've removed support for non Ubuntu/Linux + // platforms/distributions. + // This decision is to support different architectures. + tmateExecutable = external_path_default().join("/usr/bin/", "tmate") - const tmateArch = TMATE_ARCH_MAP[external_os_default().arch()]; - if (!tmateArch) { - throw new Error(`Unsupported architecture: ${external_os_default().arch()}`) - } - const tmateReleaseTar = await tool_cache.downloadTool(`https://github.com/tmate-io/tmate/releases/download/${TMATE_LINUX_VERSION}/tmate-${TMATE_LINUX_VERSION}-static-linux-${tmateArch}.tar.xz`); - const tmateDir = external_path_default().join(external_os_default().tmpdir(), "tmate") - tmateExecutable = external_path_default().join(tmateDir, "tmate") - - if (external_fs_default().existsSync(tmateExecutable)) - external_fs_default().unlinkSync(tmateExecutable) - external_fs_default().mkdirSync(tmateDir, { recursive: true }) - await execShellCommand(`tar x -C ${tmateDir} -f ${tmateReleaseTar} --strip-components=1`) - external_fs_default().unlinkSync(tmateReleaseTar) + // Optionally start the proxy service. + try { + await execShellCommand(optionalSudoPrefix + 'systemctl enable tmate-proxy --now'); + } catch (error) { + core.info(`tmate-proxy not enabled`); + core.debug(`tmate-proxy error: ${error.message || error}`); + if (error.stderr) core.debug(`stderr: ${error.stderr}`); } - core.debug("Installed dependencies successfully"); } + core.debug("Installed dependencies successfully"); if ((external_process_default()).platform === "win32") { tmateExecutable = 'CHERE_INVOKING=1 tmate' @@ -17524,7 +13690,7 @@ async function run() { if (limitAccessToActor === "true" || limitAccessToActor === "auto") { const { actor, apiUrl } = github.context const auth = core.getInput('github-token') - const octokit = new dist_node.Octokit({ auth, baseUrl: apiUrl, request: { fetch }}); + const octokit = new dist_node.Octokit({ auth, baseUrl: apiUrl, request: { fetch } }); const keys = await octokit.users.listPublicKeysForUser({ username: actor @@ -17536,7 +13702,7 @@ async function run() { const sshPath = external_path_default().join(external_os_default().homedir(), ".ssh") await external_fs_default().promises.mkdir(sshPath, { recursive: true }) const authorizedKeysPath = external_path_default().join(sshPath, "authorized_keys") - await external_fs_default().promises.writeFile(authorizedKeysPath, keys.data.map(e => e.key).join('\n')) + await external_fs_default().promises.appendFile(authorizedKeysPath, keys.data.map(e => e.key).join('\n')) newSessionExtra = `-a "${authorizedKeysPath}"` tmateSSHDashI = "ssh -i " } @@ -17552,10 +13718,10 @@ async function run() { // values that are not, strictly speaking, valid, but should be good // enough for detecting obvious errors, which is all we want here. const options = { - "TMATE_SERVER_HOST": /^[a-z\d\-]+(\.[a-z\d\-]+)*$/i, - "TMATE_SERVER_PORT": /^\d{1,5}$/, - "TMATE_SERVER_RSA_FINGERPRINT": /./, - "TMATE_SERVER_ED25519_FINGERPRINT": /./, + "tmate-server-host": /^[a-z\d\-]+(\.[a-z\d\-]+)*$/i, + "tmate-server-port": /^\d{1,5}$/, + "tmate-server-rsa-fingerprint": /./, + "tmate-server-ed25519-fingerprint": /./, } let host = ""; @@ -17564,10 +13730,10 @@ async function run() { const value = getValidatedEnvVars(key, option); if (value !== undefined) { setDefaultCommand = `${setDefaultCommand} set-option -g ${key} "${value}" \\;`; - if (key === "TMATE_SERVER_HOST") { + if (key === "tmate-server-host") { host = value; } - if (key === "TMATE_SERVER_PORT") { + if (key === "tmate-server-port") { port = value; } } @@ -17580,8 +13746,8 @@ async function run() { core.debug("Fetching connection strings") const tmateSSH = await execShellCommand(`${tmate} display -p '#{tmate_ssh}'`); - const [ , ,tokenHost] = tmateSSH.split(" "); - const [token, ] = tokenHost.split("@") + const [, , tokenHost] = tmateSSH.split(" "); + const [token,] = tokenHost.split("@") const tmateWeb = await execShellCommand(`${tmate} display -p '#{tmate_web}'`); /* @@ -17610,6 +13776,15 @@ async function run() { } core.saveState('message', message) core.saveState('tmate', tmate) + + // Set the SSH command as an output so other jobs can use it + core.setOutput('ssh-command', tmateSSH) + // Extract and set the raw SSH address (without the "ssh" prefix) + core.setOutput('ssh-address', tmateSSH.replace(/^ssh /, '')) + if (tmateWeb) { + core.setOutput('web-url', tmateWeb) + } + console.log(message) return } @@ -17622,7 +13797,7 @@ async function run() { if (tmateWeb) { core.info(`Web shell: ${tmateWeb}`); } - core.info(`SSH: ssh -p ${port} ${token}@${host}`); + core.info(`SSH: ${tmateSSH}`); if (tmateSSHDashI) { core.info(`or: ${tmateSSH.replace(/^ssh/, tmateSSHDashI)}`) } @@ -17646,12 +13821,12 @@ async function run() { } function didTmateQuit() { - const tmateSocketPath = (external_process_default()).platform === "win32" ? "C:/msys64/tmp/tmate.sock" : "/tmp/tmate.sock" + const tmateSocketPath = (external_process_default()).platform === "win32" ? `${core.getInput("msys2-location") || "C:\\msys64"}/tmp/tmate.sock` : "/tmp/tmate.sock" return !external_fs_default().existsSync(tmateSocketPath) } function continueFileExists() { - const continuePath = (external_process_default()).platform === "win32" ? "C:/msys64/continue" : "/continue" + const continuePath = (external_process_default()).platform === "win32" ? `${core.getInput("msys2-location") || "C:\\msys64"}/continue` : "/continue" return external_fs_default().existsSync(continuePath) || external_fs_default().existsSync(external_path_default().join((external_process_default()).env.GITHUB_WORKSPACE, "continue")) } diff --git a/package-lock.json b/package-lock.json index 02aa63e9..063debbe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3220,12 +3220,12 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -3434,9 +3434,9 @@ } }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", + "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", "dev": true, "dependencies": { "path-key": "^3.1.0", @@ -3647,9 +3647,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "dependencies": { "to-regex-range": "^5.0.1" @@ -5751,12 +5751,12 @@ "dev": true }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { @@ -9079,12 +9079,12 @@ } }, "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "requires": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" } }, "browserslist": { @@ -9224,9 +9224,9 @@ } }, "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", + "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", "dev": true, "requires": { "path-key": "^3.1.0", @@ -9376,9 +9376,9 @@ } }, "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "requires": { "to-regex-range": "^5.0.1" @@ -10936,12 +10936,12 @@ "dev": true }, "micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "requires": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" } }, diff --git a/package.json b/package.json index 7aa60e75..8fe1d52d 100644 --- a/package.json +++ b/package.json @@ -7,11 +7,12 @@ "scripts": { "start": "node src/index.js", "build": "ncc build src/main.js -o lib", + "update-detached-action.yml": "sed '/^runs:$/{N;N;N;s/lib\\//..\\/&/g;};/^ detached:/{N;N;N;s/\\(default: .\\)false/\\1true/g;}' action.yml >detached/action.yml", "test": "GITHUB_EVENT_PATH= jest" }, "repository": { "type": "git", - "url": "https://github.com/mxschmitt/action-tmate.git" + "url": "https://github.com/canonical/action-tmate.git" }, "keywords": [ "actions", diff --git a/src/helpers.js b/src/helpers.js index 9318170d..4490a65d 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -29,7 +29,7 @@ export const execShellCommand = (cmd, options) => { HOMEBREW_GITHUB_API_TOKEN: core.getInput('github-token') || undefined } }) : - spawn("C:\\msys64\\usr\\bin\\bash.exe", ["-lc", cmd], { + spawn(`${core.getInput("msys2-location") || "C:\\msys64"}\\usr\\bin\\bash.exe`, ["-lc", cmd], { env: { ...process.env, "MSYS2_PATH_TYPE": "inherit", /* Inherit previous path */ @@ -63,9 +63,10 @@ export const execShellCommand = (cmd, options) => { * @return {string|undefined} {undefined} or throws an error if input doesn't match regex */ export const getValidatedEnvVars = (key, re) => { - const value = process.env[key] || "" + const envVarKey = key.toUpperCase().replace(/-/gi, "_") + const value = process.env[envVarKey] || "" if (value !== undefined && !re.test(value)) { - throw new Error(`Invalid value for '${key}': '${value}'`); + throw new Error(`Invalid value for '${key}(${envVarKey})': '${value}'`); } return value; } diff --git a/src/index.js b/src/index.js index 76f672c0..6397bfb6 100644 --- a/src/index.js +++ b/src/index.js @@ -1,25 +1,28 @@ // @ts-check -import os from "os" -import fs from "fs" -import path from "path" import * as core from "@actions/core" import * as github from "@actions/github" -import * as tc from "@actions/tool-cache" import { Octokit } from "@octokit/rest" +import fs from "fs" +import os from "os" +import path from "path" import process from "process" -import { execShellCommand, getValidatedEnvVars, getLinuxDistro, useSudoPrefix } from "./helpers" - -const TMATE_LINUX_VERSION = "2.4.0" +import { execShellCommand, getValidatedEnvVars, useSudoPrefix } from "./helpers" // Map os.arch() values to the architectures in tmate release binary filenames. // Possible os.arch() values documented here: // https://nodejs.org/api/os.html#os_os_arch // Available tmate binaries listed here: -// https://github.com/tmate-io/tmate/releases/ +// https://packages.ubuntu.com/jammy/tmate +// For different Ubuntu releases, change the release (i.e. jammy) to the +// appropriate release. const TMATE_ARCH_MAP = { arm64: 'arm64v8', + armhf: 'armhf', x64: 'amd64', + ppc64: 'ppc64', + riscv64: 'riscv64', + s390x: 's390x' }; /** @param {number} ms */ @@ -50,12 +53,17 @@ export async function run() { && '0' !== await execShellCommand(`${tmate} display -p '#{tmate_num_clients}'`, { quiet: true }) } })() - for (let seconds = 10 * 60; seconds > 0; ) { - console.log(`${ - await hasAnyoneConnectedYet() + + let connectTimeoutSeconds = parseInt(core.getInput("connect-timeout-seconds")) + if (Number.isNaN(connectTimeoutSeconds) || connectTimeoutSeconds <= 0) { + connectTimeoutSeconds = 10 * 60 + } + + for (let seconds = connectTimeoutSeconds; seconds > 0;) { + console.log(`${await hasAnyoneConnectedYet() ? 'Waiting for session to end' : `Waiting for client to connect (at most ${seconds} more second(s))` - }\n${message}`) + }\n${message}`) if (continueFileExists()) { core.info("Exiting debugging session because the continue file was created") @@ -77,43 +85,31 @@ export async function run() { let tmateExecutable = "tmate" if (core.getInput("install-dependencies") !== "false") { core.debug("Installing dependencies") - if (process.platform === "darwin") { - await execShellCommand('brew install tmate'); - } else if (process.platform === "win32") { - await execShellCommand('pacman -S --noconfirm tmate'); - } else { - const optionalSudoPrefix = useSudoPrefix() ? "sudo " : ""; - const distro = await getLinuxDistro(); - core.debug("linux distro: [" + distro + "]"); - if (distro === "alpine") { - // for set -e workaround, we need to install bash because alpine doesn't have it - await execShellCommand(optionalSudoPrefix + 'apk add openssh-client xz bash'); - } else if (distro === "arch") { - // partial upgrades are not supported so also upgrade everything - await execShellCommand(optionalSudoPrefix + 'pacman -Syu --noconfirm xz openssh'); - } else if (distro === "fedora") { - await execShellCommand(optionalSudoPrefix + 'dnf install -y xz openssh'); - } else { - await execShellCommand(optionalSudoPrefix + 'apt-get update'); - await execShellCommand(optionalSudoPrefix + 'apt-get install -y openssh-client xz-utils'); - } + const optionalSudoPrefix = useSudoPrefix() ? "sudo " : ""; + await execShellCommand(optionalSudoPrefix + 'apt-get update'); + await execShellCommand(optionalSudoPrefix + 'apt-get install -y openssh-client xz-utils'); + await execShellCommand(optionalSudoPrefix + 'apt-get install -y tmate'); - const tmateArch = TMATE_ARCH_MAP[os.arch()]; - if (!tmateArch) { - throw new Error(`Unsupported architecture: ${os.arch()}`) - } - const tmateReleaseTar = await tc.downloadTool(`https://github.com/tmate-io/tmate/releases/download/${TMATE_LINUX_VERSION}/tmate-${TMATE_LINUX_VERSION}-static-linux-${tmateArch}.tar.xz`); - const tmateDir = path.join(os.tmpdir(), "tmate") - tmateExecutable = path.join(tmateDir, "tmate") - - if (fs.existsSync(tmateExecutable)) - fs.unlinkSync(tmateExecutable) - fs.mkdirSync(tmateDir, { recursive: true }) - await execShellCommand(`tar x -C ${tmateDir} -f ${tmateReleaseTar} --strip-components=1`) - fs.unlinkSync(tmateReleaseTar) + const tmateArch = TMATE_ARCH_MAP[os.arch()]; + if (!tmateArch) { + throw new Error(`Unsupported architecture: ${os.arch()}`) + } + // We change from downloading tmate from source built tar from GitHub to the + // Ubuntu packages tmate binary. Hence we've removed support for non Ubuntu/Linux + // platforms/distributions. + // This decision is to support different architectures. + tmateExecutable = path.join("/usr/bin/", "tmate") + + // Optionally start the proxy service. + try { + await execShellCommand(optionalSudoPrefix + 'systemctl enable tmate-proxy --now'); + } catch (error) { + core.info(`tmate-proxy not enabled`); + core.debug(`tmate-proxy error: ${error.message || error}`); + if (error.stderr) core.debug(`stderr: ${error.stderr}`); } - core.debug("Installed dependencies successfully"); } + core.debug("Installed dependencies successfully"); if (process.platform === "win32") { tmateExecutable = 'CHERE_INVOKING=1 tmate' @@ -133,7 +129,7 @@ export async function run() { if (limitAccessToActor === "true" || limitAccessToActor === "auto") { const { actor, apiUrl } = github.context const auth = core.getInput('github-token') - const octokit = new Octokit({ auth, baseUrl: apiUrl, request: { fetch }}); + const octokit = new Octokit({ auth, baseUrl: apiUrl, request: { fetch } }); const keys = await octokit.users.listPublicKeysForUser({ username: actor @@ -145,7 +141,7 @@ export async function run() { const sshPath = path.join(os.homedir(), ".ssh") await fs.promises.mkdir(sshPath, { recursive: true }) const authorizedKeysPath = path.join(sshPath, "authorized_keys") - await fs.promises.writeFile(authorizedKeysPath, keys.data.map(e => e.key).join('\n')) + await fs.promises.appendFile(authorizedKeysPath, keys.data.map(e => e.key).join('\n')) newSessionExtra = `-a "${authorizedKeysPath}"` tmateSSHDashI = "ssh -i " } @@ -161,10 +157,10 @@ export async function run() { // values that are not, strictly speaking, valid, but should be good // enough for detecting obvious errors, which is all we want here. const options = { - "TMATE_SERVER_HOST": /^[a-z\d\-]+(\.[a-z\d\-]+)*$/i, - "TMATE_SERVER_PORT": /^\d{1,5}$/, - "TMATE_SERVER_RSA_FINGERPRINT": /./, - "TMATE_SERVER_ED25519_FINGERPRINT": /./, + "tmate-server-host": /^[a-z\d\-]+(\.[a-z\d\-]+)*$/i, + "tmate-server-port": /^\d{1,5}$/, + "tmate-server-rsa-fingerprint": /./, + "tmate-server-ed25519-fingerprint": /./, } let host = ""; @@ -173,10 +169,10 @@ export async function run() { const value = getValidatedEnvVars(key, option); if (value !== undefined) { setDefaultCommand = `${setDefaultCommand} set-option -g ${key} "${value}" \\;`; - if (key === "TMATE_SERVER_HOST") { + if (key === "tmate-server-host") { host = value; } - if (key === "TMATE_SERVER_PORT") { + if (key === "tmate-server-port") { port = value; } } @@ -189,8 +185,8 @@ export async function run() { core.debug("Fetching connection strings") const tmateSSH = await execShellCommand(`${tmate} display -p '#{tmate_ssh}'`); - const [ , ,tokenHost] = tmateSSH.split(" "); - const [token, ] = tokenHost.split("@") + const [, , tokenHost] = tmateSSH.split(" "); + const [token,] = tokenHost.split("@") const tmateWeb = await execShellCommand(`${tmate} display -p '#{tmate_web}'`); /* @@ -219,6 +215,15 @@ export async function run() { } core.saveState('message', message) core.saveState('tmate', tmate) + + // Set the SSH command as an output so other jobs can use it + core.setOutput('ssh-command', tmateSSH) + // Extract and set the raw SSH address (without the "ssh" prefix) + core.setOutput('ssh-address', tmateSSH.replace(/^ssh /, '')) + if (tmateWeb) { + core.setOutput('web-url', tmateWeb) + } + console.log(message) return } @@ -231,7 +236,7 @@ export async function run() { if (tmateWeb) { core.info(`Web shell: ${tmateWeb}`); } - core.info(`SSH: ssh -p ${port} ${token}@${host}`); + core.info(`SSH: ${tmateSSH}`); if (tmateSSHDashI) { core.info(`or: ${tmateSSH.replace(/^ssh/, tmateSSHDashI)}`) } @@ -255,11 +260,11 @@ export async function run() { } function didTmateQuit() { - const tmateSocketPath = process.platform === "win32" ? "C:/msys64/tmp/tmate.sock" : "/tmp/tmate.sock" + const tmateSocketPath = process.platform === "win32" ? `${core.getInput("msys2-location") || "C:\\msys64"}/tmp/tmate.sock` : "/tmp/tmate.sock" return !fs.existsSync(tmateSocketPath) } function continueFileExists() { - const continuePath = process.platform === "win32" ? "C:/msys64/continue" : "/continue" + const continuePath = process.platform === "win32" ? `${core.getInput("msys2-location") || "C:\\msys64"}/continue` : "/continue" return fs.existsSync(continuePath) || fs.existsSync(path.join(process.env.GITHUB_WORKSPACE, "continue")) }