From a3d5500a6ac4b8cb507cd49325acf1add971e809 Mon Sep 17 00:00:00 2001 From: Shivam Mathur Date: Tue, 7 Jul 2020 16:36:32 +0530 Subject: [PATCH] Improve composer setup --- .github/workflows/main-workflow.yml | 1 + __tests__/tools.test.ts | 53 +- dist/index.js | 949 +++++++++++++++++++++++++++- package-lock.json | 37 +- package.json | 1 + src/scripts/darwin.sh | 27 +- src/scripts/linux.sh | 34 +- src/scripts/win32.ps1 | 34 +- src/tools.ts | 33 +- 9 files changed, 1096 insertions(+), 73 deletions(-) diff --git a/.github/workflows/main-workflow.yml b/.github/workflows/main-workflow.yml index e864a1a7..5f0abb20 100644 --- a/.github/workflows/main-workflow.yml +++ b/.github/workflows/main-workflow.yml @@ -1,5 +1,6 @@ name: Main workflow on: + workflow_dispatch: pull_request: branches: - releases/v1 diff --git a/__tests__/tools.test.ts b/__tests__/tools.test.ts index 9649ea5a..954f760c 100644 --- a/__tests__/tools.test.ts +++ b/__tests__/tools.test.ts @@ -1,5 +1,21 @@ +import * as httpm from '@actions/http-client'; import * as tools from '../src/tools'; +httpm.HttpClient.prototype.get = jest.fn().mockImplementation(() => { + return { + message: null, + readBody: jest.fn().mockImplementation(() => { + return JSON.stringify({ + stable: [{path: '/composer-stable.phar'}], + preview: [{path: '/composer-preview.phar'}], + snapshot: [{path: '/composer.phar'}], + '1': [{path: '/composer-1.phar'}], + '2': [{path: '/composer-2.phar'}] + }); + }) + }; +}); + describe('Tools tests', () => { it('checking getCommand', async () => { expect(await tools.getCommand('linux', 'tool')).toBe('add_tool '); @@ -263,20 +279,24 @@ describe('Tools tests', () => { ).toStrictEqual(['composer:2', 'a', 'b', 'c']); }); - it('checking updateComposer', async () => { - expect(await tools.updateComposer('latest', 'linux')).toContain(''); - expect(await tools.updateComposer('stable', 'win32')).toContain(''); - expect(await tools.updateComposer('snapshot', 'darwin')).toContain( - '\ncomposer self-update --snapshot' + it('checking getComposerUrl', async () => { + expect(await tools.getComposerUrl('latest')).toContain( + 'https://getcomposer.org/composer-stable.phar' ); - expect(await tools.updateComposer('preview', 'linux')).toContain( - '\ncomposer self-update --preview' + expect(await tools.getComposerUrl('stable')).toContain( + 'https://getcomposer.org/composer-stable.phar' ); - expect(await tools.updateComposer('1', 'win32')).toContain( - '\ncomposer self-update --1' + expect(await tools.getComposerUrl('snapshot')).toContain( + 'https://getcomposer.org/composer.phar' ); - expect(await tools.updateComposer('2', 'darwin')).toContain( - '\ncomposer self-update --2' + expect(await tools.getComposerUrl('preview')).toContain( + 'https://getcomposer.org/composer-preview.phar' + ); + expect(await tools.getComposerUrl('1')).toContain( + 'https://getcomposer.org/composer-1.phar' + ); + expect(await tools.getComposerUrl('2')).toContain( + 'https://getcomposer.org/composer-2.phar' ); }); @@ -527,17 +547,20 @@ describe('Tools tests', () => { 'linux' ); expect(script).toContain( - 'add_tool https://getcomposer.org/composer-stable.phar composer' + 'add_tool https://getcomposer.org/composer-1.phar composer' ); - expect(script).toContain('composer self-update --1'); script = await tools.addTools('composer:preview', '7.4', 'linux'); - expect(script).toContain('composer self-update --preview'); + expect(script).toContain( + 'add_tool https://getcomposer.org/composer-preview.phar composer' + ); script = await tools.addTools( 'composer:v1, composer:preview, composer:snapshot', '7.4', 'linux' ); - expect(script).toContain('composer self-update --snapshot'); + expect(script).toContain( + 'add_tool https://getcomposer.org/composer.phar composer' + ); }); }); diff --git a/dist/index.js b/dist/index.js index b1f4bd0c..8e43d14e 100644 --- a/dist/index.js +++ b/dist/index.js @@ -953,6 +953,13 @@ class ExecState extends events.EventEmitter { /***/ }), +/***/ 16: +/***/ (function(module) { + +module.exports = require("tls"); + +/***/ }), + /***/ 86: /***/ (function(__unusedmodule, exports, __webpack_require__) { @@ -1007,6 +1014,278 @@ module.exports = require("os"); module.exports = require("child_process"); +/***/ }), + +/***/ 141: +/***/ (function(__unusedmodule, exports, __webpack_require__) { + +"use strict"; + + +var net = __webpack_require__(631); +var tls = __webpack_require__(16); +var http = __webpack_require__(605); +var https = __webpack_require__(211); +var events = __webpack_require__(614); +var assert = __webpack_require__(357); +var util = __webpack_require__(669); + + +exports.httpOverHttp = httpOverHttp; +exports.httpsOverHttp = httpsOverHttp; +exports.httpOverHttps = httpOverHttps; +exports.httpsOverHttps = httpsOverHttps; + + +function httpOverHttp(options) { + var agent = new TunnelingAgent(options); + agent.request = http.request; + return agent; +} + +function httpsOverHttp(options) { + var agent = new TunnelingAgent(options); + agent.request = http.request; + agent.createSocket = createSecureSocket; + agent.defaultPort = 443; + return agent; +} + +function httpOverHttps(options) { + var agent = new TunnelingAgent(options); + agent.request = https.request; + return agent; +} + +function httpsOverHttps(options) { + var agent = new TunnelingAgent(options); + agent.request = https.request; + agent.createSocket = createSecureSocket; + agent.defaultPort = 443; + return agent; +} + + +function TunnelingAgent(options) { + var self = this; + self.options = options || {}; + self.proxyOptions = self.options.proxy || {}; + self.maxSockets = self.options.maxSockets || http.Agent.defaultMaxSockets; + self.requests = []; + self.sockets = []; + + self.on('free', function onFree(socket, host, port, localAddress) { + var options = toOptions(host, port, localAddress); + for (var i = 0, len = self.requests.length; i < len; ++i) { + var pending = self.requests[i]; + if (pending.host === options.host && pending.port === options.port) { + // Detect the request to connect same origin server, + // reuse the connection. + self.requests.splice(i, 1); + pending.request.onSocket(socket); + return; + } + } + socket.destroy(); + self.removeSocket(socket); + }); +} +util.inherits(TunnelingAgent, events.EventEmitter); + +TunnelingAgent.prototype.addRequest = function addRequest(req, host, port, localAddress) { + var self = this; + var options = mergeOptions({request: req}, self.options, toOptions(host, port, localAddress)); + + if (self.sockets.length >= this.maxSockets) { + // We are over limit so we'll add it to the queue. + self.requests.push(options); + return; + } + + // If we are under maxSockets create a new one. + self.createSocket(options, function(socket) { + socket.on('free', onFree); + socket.on('close', onCloseOrRemove); + socket.on('agentRemove', onCloseOrRemove); + req.onSocket(socket); + + function onFree() { + self.emit('free', socket, options); + } + + function onCloseOrRemove(err) { + self.removeSocket(socket); + socket.removeListener('free', onFree); + socket.removeListener('close', onCloseOrRemove); + socket.removeListener('agentRemove', onCloseOrRemove); + } + }); +}; + +TunnelingAgent.prototype.createSocket = function createSocket(options, cb) { + var self = this; + var placeholder = {}; + self.sockets.push(placeholder); + + var connectOptions = mergeOptions({}, self.proxyOptions, { + method: 'CONNECT', + path: options.host + ':' + options.port, + agent: false, + headers: { + host: options.host + ':' + options.port + } + }); + if (options.localAddress) { + connectOptions.localAddress = options.localAddress; + } + if (connectOptions.proxyAuth) { + connectOptions.headers = connectOptions.headers || {}; + connectOptions.headers['Proxy-Authorization'] = 'Basic ' + + new Buffer(connectOptions.proxyAuth).toString('base64'); + } + + debug('making CONNECT request'); + var connectReq = self.request(connectOptions); + connectReq.useChunkedEncodingByDefault = false; // for v0.6 + connectReq.once('response', onResponse); // for v0.6 + connectReq.once('upgrade', onUpgrade); // for v0.6 + connectReq.once('connect', onConnect); // for v0.7 or later + connectReq.once('error', onError); + connectReq.end(); + + function onResponse(res) { + // Very hacky. This is necessary to avoid http-parser leaks. + res.upgrade = true; + } + + function onUpgrade(res, socket, head) { + // Hacky. + process.nextTick(function() { + onConnect(res, socket, head); + }); + } + + function onConnect(res, socket, head) { + connectReq.removeAllListeners(); + socket.removeAllListeners(); + + if (res.statusCode !== 200) { + debug('tunneling socket could not be established, statusCode=%d', + res.statusCode); + socket.destroy(); + var error = new Error('tunneling socket could not be established, ' + + 'statusCode=' + res.statusCode); + error.code = 'ECONNRESET'; + options.request.emit('error', error); + self.removeSocket(placeholder); + return; + } + if (head.length > 0) { + debug('got illegal response body from proxy'); + socket.destroy(); + var error = new Error('got illegal response body from proxy'); + error.code = 'ECONNRESET'; + options.request.emit('error', error); + self.removeSocket(placeholder); + return; + } + debug('tunneling connection has established'); + self.sockets[self.sockets.indexOf(placeholder)] = socket; + return cb(socket); + } + + function onError(cause) { + connectReq.removeAllListeners(); + + debug('tunneling socket could not be established, cause=%s\n', + cause.message, cause.stack); + var error = new Error('tunneling socket could not be established, ' + + 'cause=' + cause.message); + error.code = 'ECONNRESET'; + options.request.emit('error', error); + self.removeSocket(placeholder); + } +}; + +TunnelingAgent.prototype.removeSocket = function removeSocket(socket) { + var pos = this.sockets.indexOf(socket) + if (pos === -1) { + return; + } + this.sockets.splice(pos, 1); + + var pending = this.requests.shift(); + if (pending) { + // If we have pending requests and a socket gets closed a new one + // needs to be created to take over in the pool for the one that closed. + this.createSocket(pending, function(socket) { + pending.request.onSocket(socket); + }); + } +}; + +function createSecureSocket(options, cb) { + var self = this; + TunnelingAgent.prototype.createSocket.call(self, options, function(socket) { + var hostHeader = options.request.getHeader('host'); + var tlsOptions = mergeOptions({}, self.options, { + socket: socket, + servername: hostHeader ? hostHeader.replace(/:.*$/, '') : options.host + }); + + // 0 is dummy port for v0.6 + var secureSocket = tls.connect(0, tlsOptions); + self.sockets[self.sockets.indexOf(socket)] = secureSocket; + cb(secureSocket); + }); +} + + +function toOptions(host, port, localAddress) { + if (typeof host === 'string') { // since v0.10 + return { + host: host, + port: port, + localAddress: localAddress + }; + } + return host; // for v0.11 or later +} + +function mergeOptions(target) { + for (var i = 1, len = arguments.length; i < len; ++i) { + var overrides = arguments[i]; + if (typeof overrides === 'object') { + var keys = Object.keys(overrides); + for (var j = 0, keyLen = keys.length; j < keyLen; ++j) { + var k = keys[j]; + if (overrides[k] !== undefined) { + target[k] = overrides[k]; + } + } + } + } + return target; +} + + +var debug; +if (process.env.NODE_DEBUG && /\btunnel\b/.test(process.env.NODE_DEBUG)) { + debug = function() { + var args = Array.prototype.slice.call(arguments); + if (typeof args[0] === 'string') { + args[0] = 'TUNNEL: ' + args[0]; + } else { + args.unshift('TUNNEL:'); + } + console.error.apply(console, args); + } +} else { + debug = function() {}; +} +exports.debug = debug; // for test + + /***/ }), /***/ 163: @@ -1247,6 +1526,13 @@ async function suppressOutput(os_version) { exports.suppressOutput = suppressOutput; +/***/ }), + +/***/ 211: +/***/ (function(module) { + +module.exports = require("https"); + /***/ }), /***/ 357: @@ -1254,6 +1540,14 @@ exports.suppressOutput = suppressOutput; module.exports = require("assert"); +/***/ }), + +/***/ 413: +/***/ (function(module, __unusedexports, __webpack_require__) { + +module.exports = __webpack_require__(141); + + /***/ }), /***/ 431: @@ -1609,8 +1903,9 @@ var __importStar = (this && this.__importStar) || function (mod) { return result; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.addTools = exports.addPackage = exports.addDevTools = exports.addArchive = exports.getCleanedToolsList = exports.updateComposer = exports.addComposer = exports.getSymfonyUri = exports.getDeployerUrl = exports.getPharUrl = exports.addPhive = exports.getCodeceptionUri = exports.getCodeceptionUriBuilder = exports.getUri = exports.parseTool = exports.getToolVersion = exports.getCommand = void 0; +exports.addTools = exports.addPackage = exports.addDevTools = exports.addArchive = exports.getCleanedToolsList = exports.getComposerUrl = exports.addComposer = exports.getSymfonyUri = exports.getDeployerUrl = exports.getPharUrl = exports.addPhive = exports.getCodeceptionUri = exports.getCodeceptionUriBuilder = exports.getUri = exports.parseTool = exports.getToolVersion = exports.getCommand = void 0; const utils = __importStar(__webpack_require__(163)); +const httpm = __importStar(__webpack_require__(539)); /** * Function to get command to setup tools * @@ -1868,25 +2163,29 @@ async function addComposer(tools_list) { } exports.addComposer = addComposer; /** - * Function to get script to update composer + * Function to get composer URL for a given version * * @param version - * @param os_version */ -async function updateComposer(version, os_version) { +async function getComposerUrl(version) { + const getComposerUrlHelper = async function (version) { + const client = new httpm.HttpClient('setup-php'); + const response = await client.get('https://getcomposer.org/versions'); + const data = JSON.parse(await response.readBody()); + return 'https://getcomposer.org' + data[version][0]['path']; + }; switch (version) { case 'snapshot': + return 'https://getcomposer.org/composer.phar'; case 'preview': case '1': case '2': - return ('\ncomposer self-update --' + - version + - (await utils.suppressOutput(os_version))); + return await getComposerUrlHelper(version); default: - return ''; + return 'https://getcomposer.org/composer-stable.phar'; } } -exports.updateComposer = updateComposer; +exports.getComposerUrl = getComposerUrl; /** * Function to get Tools list after cleanup * @@ -2001,10 +2300,8 @@ async function addTools(tools_csv, php_version, os_version) { script += await addArchive(tool, version, url, os_version); break; case 'composer': - url = 'https://getcomposer.org/composer-stable.phar'; - script += - (await addArchive('composer', version, url, os_version)) + - (await updateComposer(version, os_version)); + url = await getComposerUrl(version); + script += await addArchive('composer', version, url, os_version); break; case 'codeception': url = @@ -2053,6 +2350,552 @@ async function addTools(tools_csv, php_version, os_version) { exports.addTools = addTools; +/***/ }), + +/***/ 539: +/***/ (function(__unusedmodule, exports, __webpack_require__) { + +"use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +const url = __webpack_require__(835); +const http = __webpack_require__(605); +const https = __webpack_require__(211); +const pm = __webpack_require__(950); +let tunnel; +var HttpCodes; +(function (HttpCodes) { + HttpCodes[HttpCodes["OK"] = 200] = "OK"; + HttpCodes[HttpCodes["MultipleChoices"] = 300] = "MultipleChoices"; + HttpCodes[HttpCodes["MovedPermanently"] = 301] = "MovedPermanently"; + HttpCodes[HttpCodes["ResourceMoved"] = 302] = "ResourceMoved"; + HttpCodes[HttpCodes["SeeOther"] = 303] = "SeeOther"; + HttpCodes[HttpCodes["NotModified"] = 304] = "NotModified"; + HttpCodes[HttpCodes["UseProxy"] = 305] = "UseProxy"; + HttpCodes[HttpCodes["SwitchProxy"] = 306] = "SwitchProxy"; + HttpCodes[HttpCodes["TemporaryRedirect"] = 307] = "TemporaryRedirect"; + HttpCodes[HttpCodes["PermanentRedirect"] = 308] = "PermanentRedirect"; + HttpCodes[HttpCodes["BadRequest"] = 400] = "BadRequest"; + HttpCodes[HttpCodes["Unauthorized"] = 401] = "Unauthorized"; + HttpCodes[HttpCodes["PaymentRequired"] = 402] = "PaymentRequired"; + HttpCodes[HttpCodes["Forbidden"] = 403] = "Forbidden"; + HttpCodes[HttpCodes["NotFound"] = 404] = "NotFound"; + HttpCodes[HttpCodes["MethodNotAllowed"] = 405] = "MethodNotAllowed"; + HttpCodes[HttpCodes["NotAcceptable"] = 406] = "NotAcceptable"; + HttpCodes[HttpCodes["ProxyAuthenticationRequired"] = 407] = "ProxyAuthenticationRequired"; + HttpCodes[HttpCodes["RequestTimeout"] = 408] = "RequestTimeout"; + HttpCodes[HttpCodes["Conflict"] = 409] = "Conflict"; + HttpCodes[HttpCodes["Gone"] = 410] = "Gone"; + HttpCodes[HttpCodes["TooManyRequests"] = 429] = "TooManyRequests"; + HttpCodes[HttpCodes["InternalServerError"] = 500] = "InternalServerError"; + HttpCodes[HttpCodes["NotImplemented"] = 501] = "NotImplemented"; + HttpCodes[HttpCodes["BadGateway"] = 502] = "BadGateway"; + HttpCodes[HttpCodes["ServiceUnavailable"] = 503] = "ServiceUnavailable"; + HttpCodes[HttpCodes["GatewayTimeout"] = 504] = "GatewayTimeout"; +})(HttpCodes = exports.HttpCodes || (exports.HttpCodes = {})); +var Headers; +(function (Headers) { + Headers["Accept"] = "accept"; + Headers["ContentType"] = "content-type"; +})(Headers = exports.Headers || (exports.Headers = {})); +var MediaTypes; +(function (MediaTypes) { + MediaTypes["ApplicationJson"] = "application/json"; +})(MediaTypes = exports.MediaTypes || (exports.MediaTypes = {})); +/** + * Returns the proxy URL, depending upon the supplied url and proxy environment variables. + * @param serverUrl The server URL where the request will be sent. For example, https://api.github.com + */ +function getProxyUrl(serverUrl) { + let proxyUrl = pm.getProxyUrl(url.parse(serverUrl)); + return proxyUrl ? proxyUrl.href : ''; +} +exports.getProxyUrl = getProxyUrl; +const HttpRedirectCodes = [ + HttpCodes.MovedPermanently, + HttpCodes.ResourceMoved, + HttpCodes.SeeOther, + HttpCodes.TemporaryRedirect, + HttpCodes.PermanentRedirect +]; +const HttpResponseRetryCodes = [ + HttpCodes.BadGateway, + HttpCodes.ServiceUnavailable, + HttpCodes.GatewayTimeout +]; +const RetryableHttpVerbs = ['OPTIONS', 'GET', 'DELETE', 'HEAD']; +const ExponentialBackoffCeiling = 10; +const ExponentialBackoffTimeSlice = 5; +class HttpClientResponse { + constructor(message) { + this.message = message; + } + readBody() { + return new Promise(async (resolve, reject) => { + let output = Buffer.alloc(0); + this.message.on('data', (chunk) => { + output = Buffer.concat([output, chunk]); + }); + this.message.on('end', () => { + resolve(output.toString()); + }); + }); + } +} +exports.HttpClientResponse = HttpClientResponse; +function isHttps(requestUrl) { + let parsedUrl = url.parse(requestUrl); + return parsedUrl.protocol === 'https:'; +} +exports.isHttps = isHttps; +class HttpClient { + constructor(userAgent, handlers, requestOptions) { + this._ignoreSslError = false; + this._allowRedirects = true; + this._allowRedirectDowngrade = false; + this._maxRedirects = 50; + this._allowRetries = false; + this._maxRetries = 1; + this._keepAlive = false; + this._disposed = false; + this.userAgent = userAgent; + this.handlers = handlers || []; + this.requestOptions = requestOptions; + if (requestOptions) { + if (requestOptions.ignoreSslError != null) { + this._ignoreSslError = requestOptions.ignoreSslError; + } + this._socketTimeout = requestOptions.socketTimeout; + if (requestOptions.allowRedirects != null) { + this._allowRedirects = requestOptions.allowRedirects; + } + if (requestOptions.allowRedirectDowngrade != null) { + this._allowRedirectDowngrade = requestOptions.allowRedirectDowngrade; + } + if (requestOptions.maxRedirects != null) { + this._maxRedirects = Math.max(requestOptions.maxRedirects, 0); + } + if (requestOptions.keepAlive != null) { + this._keepAlive = requestOptions.keepAlive; + } + if (requestOptions.allowRetries != null) { + this._allowRetries = requestOptions.allowRetries; + } + if (requestOptions.maxRetries != null) { + this._maxRetries = requestOptions.maxRetries; + } + } + } + options(requestUrl, additionalHeaders) { + return this.request('OPTIONS', requestUrl, null, additionalHeaders || {}); + } + get(requestUrl, additionalHeaders) { + return this.request('GET', requestUrl, null, additionalHeaders || {}); + } + del(requestUrl, additionalHeaders) { + return this.request('DELETE', requestUrl, null, additionalHeaders || {}); + } + post(requestUrl, data, additionalHeaders) { + return this.request('POST', requestUrl, data, additionalHeaders || {}); + } + patch(requestUrl, data, additionalHeaders) { + return this.request('PATCH', requestUrl, data, additionalHeaders || {}); + } + put(requestUrl, data, additionalHeaders) { + return this.request('PUT', requestUrl, data, additionalHeaders || {}); + } + head(requestUrl, additionalHeaders) { + return this.request('HEAD', requestUrl, null, additionalHeaders || {}); + } + sendStream(verb, requestUrl, stream, additionalHeaders) { + return this.request(verb, requestUrl, stream, additionalHeaders); + } + /** + * Gets a typed object from an endpoint + * Be aware that not found returns a null. Other errors (4xx, 5xx) reject the promise + */ + async getJson(requestUrl, additionalHeaders = {}) { + additionalHeaders[Headers.Accept] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.Accept, MediaTypes.ApplicationJson); + let res = await this.get(requestUrl, additionalHeaders); + return this._processResponse(res, this.requestOptions); + } + async postJson(requestUrl, obj, additionalHeaders = {}) { + let data = JSON.stringify(obj, null, 2); + additionalHeaders[Headers.Accept] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.Accept, MediaTypes.ApplicationJson); + additionalHeaders[Headers.ContentType] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.ContentType, MediaTypes.ApplicationJson); + let res = await this.post(requestUrl, data, additionalHeaders); + return this._processResponse(res, this.requestOptions); + } + async putJson(requestUrl, obj, additionalHeaders = {}) { + let data = JSON.stringify(obj, null, 2); + additionalHeaders[Headers.Accept] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.Accept, MediaTypes.ApplicationJson); + additionalHeaders[Headers.ContentType] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.ContentType, MediaTypes.ApplicationJson); + let res = await this.put(requestUrl, data, additionalHeaders); + return this._processResponse(res, this.requestOptions); + } + async patchJson(requestUrl, obj, additionalHeaders = {}) { + let data = JSON.stringify(obj, null, 2); + additionalHeaders[Headers.Accept] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.Accept, MediaTypes.ApplicationJson); + additionalHeaders[Headers.ContentType] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.ContentType, MediaTypes.ApplicationJson); + let res = await this.patch(requestUrl, data, additionalHeaders); + return this._processResponse(res, this.requestOptions); + } + /** + * Makes a raw http request. + * All other methods such as get, post, patch, and request ultimately call this. + * Prefer get, del, post and patch + */ + async request(verb, requestUrl, data, headers) { + if (this._disposed) { + throw new Error('Client has already been disposed.'); + } + let parsedUrl = url.parse(requestUrl); + let info = this._prepareRequest(verb, parsedUrl, headers); + // Only perform retries on reads since writes may not be idempotent. + let maxTries = this._allowRetries && RetryableHttpVerbs.indexOf(verb) != -1 + ? this._maxRetries + 1 + : 1; + let numTries = 0; + let response; + while (numTries < maxTries) { + response = await this.requestRaw(info, data); + // Check if it's an authentication challenge + if (response && + response.message && + response.message.statusCode === HttpCodes.Unauthorized) { + let authenticationHandler; + for (let i = 0; i < this.handlers.length; i++) { + if (this.handlers[i].canHandleAuthentication(response)) { + authenticationHandler = this.handlers[i]; + break; + } + } + if (authenticationHandler) { + return authenticationHandler.handleAuthentication(this, info, data); + } + else { + // We have received an unauthorized response but have no handlers to handle it. + // Let the response return to the caller. + return response; + } + } + let redirectsRemaining = this._maxRedirects; + while (HttpRedirectCodes.indexOf(response.message.statusCode) != -1 && + this._allowRedirects && + redirectsRemaining > 0) { + const redirectUrl = response.message.headers['location']; + if (!redirectUrl) { + // if there's no location to redirect to, we won't + break; + } + let parsedRedirectUrl = url.parse(redirectUrl); + if (parsedUrl.protocol == 'https:' && + parsedUrl.protocol != parsedRedirectUrl.protocol && + !this._allowRedirectDowngrade) { + throw new Error('Redirect from HTTPS to HTTP protocol. This downgrade is not allowed for security reasons. If you want to allow this behavior, set the allowRedirectDowngrade option to true.'); + } + // we need to finish reading the response before reassigning response + // which will leak the open socket. + await response.readBody(); + // strip authorization header if redirected to a different hostname + if (parsedRedirectUrl.hostname !== parsedUrl.hostname) { + for (let header in headers) { + // header names are case insensitive + if (header.toLowerCase() === 'authorization') { + delete headers[header]; + } + } + } + // let's make the request with the new redirectUrl + info = this._prepareRequest(verb, parsedRedirectUrl, headers); + response = await this.requestRaw(info, data); + redirectsRemaining--; + } + if (HttpResponseRetryCodes.indexOf(response.message.statusCode) == -1) { + // If not a retry code, return immediately instead of retrying + return response; + } + numTries += 1; + if (numTries < maxTries) { + await response.readBody(); + await this._performExponentialBackoff(numTries); + } + } + return response; + } + /** + * Needs to be called if keepAlive is set to true in request options. + */ + dispose() { + if (this._agent) { + this._agent.destroy(); + } + this._disposed = true; + } + /** + * Raw request. + * @param info + * @param data + */ + requestRaw(info, data) { + return new Promise((resolve, reject) => { + let callbackForResult = function (err, res) { + if (err) { + reject(err); + } + resolve(res); + }; + this.requestRawWithCallback(info, data, callbackForResult); + }); + } + /** + * Raw request with callback. + * @param info + * @param data + * @param onResult + */ + requestRawWithCallback(info, data, onResult) { + let socket; + if (typeof data === 'string') { + info.options.headers['Content-Length'] = Buffer.byteLength(data, 'utf8'); + } + let callbackCalled = false; + let handleResult = (err, res) => { + if (!callbackCalled) { + callbackCalled = true; + onResult(err, res); + } + }; + let req = info.httpModule.request(info.options, (msg) => { + let res = new HttpClientResponse(msg); + handleResult(null, res); + }); + req.on('socket', sock => { + socket = sock; + }); + // If we ever get disconnected, we want the socket to timeout eventually + req.setTimeout(this._socketTimeout || 3 * 60000, () => { + if (socket) { + socket.end(); + } + handleResult(new Error('Request timeout: ' + info.options.path), null); + }); + req.on('error', function (err) { + // err has statusCode property + // res should have headers + handleResult(err, null); + }); + if (data && typeof data === 'string') { + req.write(data, 'utf8'); + } + if (data && typeof data !== 'string') { + data.on('close', function () { + req.end(); + }); + data.pipe(req); + } + else { + req.end(); + } + } + /** + * Gets an http agent. This function is useful when you need an http agent that handles + * routing through a proxy server - depending upon the url and proxy environment variables. + * @param serverUrl The server URL where the request will be sent. For example, https://api.github.com + */ + getAgent(serverUrl) { + let parsedUrl = url.parse(serverUrl); + return this._getAgent(parsedUrl); + } + _prepareRequest(method, requestUrl, headers) { + const info = {}; + info.parsedUrl = requestUrl; + const usingSsl = info.parsedUrl.protocol === 'https:'; + info.httpModule = usingSsl ? https : http; + const defaultPort = usingSsl ? 443 : 80; + info.options = {}; + info.options.host = info.parsedUrl.hostname; + info.options.port = info.parsedUrl.port + ? parseInt(info.parsedUrl.port) + : defaultPort; + info.options.path = + (info.parsedUrl.pathname || '') + (info.parsedUrl.search || ''); + info.options.method = method; + info.options.headers = this._mergeHeaders(headers); + if (this.userAgent != null) { + info.options.headers['user-agent'] = this.userAgent; + } + info.options.agent = this._getAgent(info.parsedUrl); + // gives handlers an opportunity to participate + if (this.handlers) { + this.handlers.forEach(handler => { + handler.prepareRequest(info.options); + }); + } + return info; + } + _mergeHeaders(headers) { + const lowercaseKeys = obj => Object.keys(obj).reduce((c, k) => ((c[k.toLowerCase()] = obj[k]), c), {}); + if (this.requestOptions && this.requestOptions.headers) { + return Object.assign({}, lowercaseKeys(this.requestOptions.headers), lowercaseKeys(headers)); + } + return lowercaseKeys(headers || {}); + } + _getExistingOrDefaultHeader(additionalHeaders, header, _default) { + const lowercaseKeys = obj => Object.keys(obj).reduce((c, k) => ((c[k.toLowerCase()] = obj[k]), c), {}); + let clientHeader; + if (this.requestOptions && this.requestOptions.headers) { + clientHeader = lowercaseKeys(this.requestOptions.headers)[header]; + } + return additionalHeaders[header] || clientHeader || _default; + } + _getAgent(parsedUrl) { + let agent; + let proxyUrl = pm.getProxyUrl(parsedUrl); + let useProxy = proxyUrl && proxyUrl.hostname; + if (this._keepAlive && useProxy) { + agent = this._proxyAgent; + } + if (this._keepAlive && !useProxy) { + agent = this._agent; + } + // if agent is already assigned use that agent. + if (!!agent) { + return agent; + } + const usingSsl = parsedUrl.protocol === 'https:'; + let maxSockets = 100; + if (!!this.requestOptions) { + maxSockets = this.requestOptions.maxSockets || http.globalAgent.maxSockets; + } + if (useProxy) { + // If using proxy, need tunnel + if (!tunnel) { + tunnel = __webpack_require__(413); + } + const agentOptions = { + maxSockets: maxSockets, + keepAlive: this._keepAlive, + proxy: { + proxyAuth: proxyUrl.auth, + host: proxyUrl.hostname, + port: proxyUrl.port + } + }; + let tunnelAgent; + const overHttps = proxyUrl.protocol === 'https:'; + if (usingSsl) { + tunnelAgent = overHttps ? tunnel.httpsOverHttps : tunnel.httpsOverHttp; + } + else { + tunnelAgent = overHttps ? tunnel.httpOverHttps : tunnel.httpOverHttp; + } + agent = tunnelAgent(agentOptions); + this._proxyAgent = agent; + } + // if reusing agent across request and tunneling agent isn't assigned create a new agent + if (this._keepAlive && !agent) { + const options = { keepAlive: this._keepAlive, maxSockets: maxSockets }; + agent = usingSsl ? new https.Agent(options) : new http.Agent(options); + this._agent = agent; + } + // if not using private agent and tunnel agent isn't setup then use global agent + if (!agent) { + agent = usingSsl ? https.globalAgent : http.globalAgent; + } + if (usingSsl && this._ignoreSslError) { + // we don't want to set NODE_TLS_REJECT_UNAUTHORIZED=0 since that will affect request for entire process + // http.RequestOptions doesn't expose a way to modify RequestOptions.agent.options + // we have to cast it to any and change it directly + agent.options = Object.assign(agent.options || {}, { + rejectUnauthorized: false + }); + } + return agent; + } + _performExponentialBackoff(retryNumber) { + retryNumber = Math.min(ExponentialBackoffCeiling, retryNumber); + const ms = ExponentialBackoffTimeSlice * Math.pow(2, retryNumber); + return new Promise(resolve => setTimeout(() => resolve(), ms)); + } + static dateTimeDeserializer(key, value) { + if (typeof value === 'string') { + let a = new Date(value); + if (!isNaN(a.valueOf())) { + return a; + } + } + return value; + } + async _processResponse(res, options) { + return new Promise(async (resolve, reject) => { + const statusCode = res.message.statusCode; + const response = { + statusCode: statusCode, + result: null, + headers: {} + }; + // not found leads to null obj returned + if (statusCode == HttpCodes.NotFound) { + resolve(response); + } + let obj; + let contents; + // get the result from the body + try { + contents = await res.readBody(); + if (contents && contents.length > 0) { + if (options && options.deserializeDates) { + obj = JSON.parse(contents, HttpClient.dateTimeDeserializer); + } + else { + obj = JSON.parse(contents); + } + response.result = obj; + } + response.headers = res.message.headers; + } + catch (err) { + // Invalid resource (contents not json); leaving result obj null + } + // note that 3xx redirects are handled by the http layer. + if (statusCode > 299) { + let msg; + // if exception/error in body, attempt to get better error + if (obj && obj.message) { + msg = obj.message; + } + else if (contents && contents.length > 0) { + // it may be the case that the exception is in the body message as string + msg = contents; + } + else { + msg = 'Failed request: (' + statusCode + ')'; + } + let err = new Error(msg); + // attach statusCode and body obj (if available) to the error object + err['statusCode'] = statusCode; + if (response.result) { + err['result'] = response.result; + } + reject(err); + } + else { + resolve(response); + } + }); + } +} +exports.HttpClient = HttpClient; + + +/***/ }), + +/***/ 605: +/***/ (function(module) { + +module.exports = require("http"); + /***/ }), /***/ 614: @@ -2069,6 +2912,13 @@ module.exports = require("path"); /***/ }), +/***/ 631: +/***/ (function(module) { + +module.exports = require("net"); + +/***/ }), + /***/ 635: /***/ (function(__unusedmodule, exports, __webpack_require__) { @@ -2623,6 +3473,13 @@ module.exports = require("fs"); /***/ }), +/***/ 835: +/***/ (function(module) { + +module.exports = require("url"); + +/***/ }), + /***/ 911: /***/ (function(__unusedmodule, exports, __webpack_require__) { @@ -2908,6 +3765,72 @@ async function addExtension(extension_csv, version, os_version, no_step = false) exports.addExtension = addExtension; +/***/ }), + +/***/ 950: +/***/ (function(__unusedmodule, exports, __webpack_require__) { + +"use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +const url = __webpack_require__(835); +function getProxyUrl(reqUrl) { + let usingSsl = reqUrl.protocol === 'https:'; + let proxyUrl; + if (checkBypass(reqUrl)) { + return proxyUrl; + } + let proxyVar; + if (usingSsl) { + proxyVar = process.env['https_proxy'] || process.env['HTTPS_PROXY']; + } + else { + proxyVar = process.env['http_proxy'] || process.env['HTTP_PROXY']; + } + if (proxyVar) { + proxyUrl = url.parse(proxyVar); + } + return proxyUrl; +} +exports.getProxyUrl = getProxyUrl; +function checkBypass(reqUrl) { + if (!reqUrl.hostname) { + return false; + } + let noProxy = process.env['no_proxy'] || process.env['NO_PROXY'] || ''; + if (!noProxy) { + return false; + } + // Determine the request port + let reqPort; + if (reqUrl.port) { + reqPort = Number(reqUrl.port); + } + else if (reqUrl.protocol === 'http:') { + reqPort = 80; + } + else if (reqUrl.protocol === 'https:') { + reqPort = 443; + } + // Format the request hostname and hostname with port + let upperReqHosts = [reqUrl.hostname.toUpperCase()]; + if (typeof reqPort === 'number') { + upperReqHosts.push(`${upperReqHosts[0]}:${reqPort}`); + } + // Compare request host against noproxy + for (let upperNoProxyItem of noProxy + .split(',') + .map(x => x.trim().toUpperCase()) + .filter(x => x)) { + if (upperReqHosts.some(x => x === upperNoProxyItem)) { + return true; + } + } + return false; +} +exports.checkBypass = checkBypass; + + /***/ }), /***/ 986: diff --git a/package-lock.json b/package-lock.json index 9cfa74e7..05a652c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,14 @@ "@actions/io": "^1.0.1" } }, + "@actions/http-client": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.8.tgz", + "integrity": "sha512-G4JjJ6f9Hb3Zvejj+ewLLKLf99ZC+9v+yCxoYf9vSyH+WkzPLB2LuUtRMGNkooMqdugGBFStIKXOuvH1W+EctA==", + "requires": { + "tunnel": "0.0.6" + } + }, "@actions/io": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.0.2.tgz", @@ -970,9 +978,9 @@ } }, "@types/babel__traverse": { - "version": "7.0.12", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.0.12.tgz", - "integrity": "sha512-t4CoEokHTfcyfb4hUaF9oOHu9RmmNWnm1CP0YmMqOOfClKascOmvlEM736vlqeScuGvBDsHkf8R2INd4DWreQA==", + "version": "7.0.13", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.0.13.tgz", + "integrity": "sha512-i+zS7t6/s9cdQvbqKDARrcbrPvtJGlbYsMkazo03nTAK3RX9FNrLllXys22uiTGJapPOTZTQ35nHh4ISph4SLQ==", "dev": true, "requires": { "@babel/types": "^7.3.0" @@ -1025,9 +1033,9 @@ } }, "@types/jest": { - "version": "26.0.3", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.3.tgz", - "integrity": "sha512-v89ga1clpVL/Y1+YI0eIu1VMW+KU7Xl8PhylVtDKVWaSUHBHYPLXMQGBdrpHewaKoTvlXkksbYqPgz8b4cmRZg==", + "version": "26.0.4", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.4.tgz", + "integrity": "sha512-4fQNItvelbNA9+sFgU+fhJo8ZFF+AS4Egk3GWwCW2jFtViukXbnztccafAdLhzE/0EiCogljtQQXP8aQ9J7sFg==", "dev": true, "requires": { "jest-diff": "^25.2.1", @@ -1047,9 +1055,9 @@ "dev": true }, "@types/node": { - "version": "14.0.14", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.14.tgz", - "integrity": "sha512-syUgf67ZQpaJj01/tRTknkMNoBBLWJOBODF0Zm4NrXmiSuxjymFrxnTu1QVYRubhVkRcZLYZG8STTwJRdVm/WQ==", + "version": "14.0.18", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.18.tgz", + "integrity": "sha512-0Z3nS5acM0cIV4JPzrj9g/GH0Et5vmADWtip3YOXOp1NpOLU8V3KoZDc8ny9c1pe/YSYYzQkAWob6dyV/EWg4g==", "dev": true }, "@types/normalize-package-data": { @@ -1065,9 +1073,9 @@ "dev": true }, "@types/prettier": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.0.1.tgz", - "integrity": "sha512-boy4xPNEtiw6N3abRhBi/e7hNvy3Tt8E9ZRAQrwAGzoCGZS/1wjo9KY7JHhnfnEsG5wSjDbymCozUM9a3ea7OQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.0.2.tgz", + "integrity": "sha512-IkVfat549ggtkZUthUzEX49562eGikhSYeVGX97SkMFn+sTZrgRewXjQ4tPKFPCykZHkX1Zfd9OoELGqKU2jJA==", "dev": true }, "@types/stack-utils": { @@ -6713,6 +6721,11 @@ "tslib": "^1.8.1" } }, + "tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==" + }, "tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", diff --git a/package.json b/package.json index b6984d65..aac6a63d 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "dependencies": { "@actions/core": "^1.2.4", "@actions/exec": "^1.0.4", + "@actions/http-client": "^1.0.8", "@actions/io": "^1.0.2", "fs": "0.0.1-security" }, diff --git a/src/scripts/darwin.sh b/src/scripts/darwin.sh index d91dedfd..064f67d7 100644 --- a/src/scripts/darwin.sh +++ b/src/scripts/darwin.sh @@ -105,7 +105,23 @@ add_unstable_extension() { add_pecl_extension "$extension" "$pecl_version" "$prefix" } -# Function to setup a remote tool +# Function to configure composer +configure_composer() { + tool_path=$1 + sudo ln -sf "$tool_path" "$tool_path.phar" + php -r "try {\$p=new Phar('$tool_path.phar', 0);exit(0);} catch(Exception \$e) {exit(1);}" + if [ $? -eq 1 ]; then + add_log "$cross" "composer" "Could not download composer" + exit 1; + fi + composer -q global config process-timeout 0 + echo "::add-path::/Users/$USER/.composer/vendor/bin" + if [ -n "$COMPOSER_TOKEN" ]; then + composer -q global config github-oauth.github.com "$COMPOSER_TOKEN" + fi +} + +# Function to setup a remote tool. add_tool() { url=$1 tool=$2 @@ -118,11 +134,10 @@ add_tool() { if [ "$status_code" = "200" ]; then sudo chmod a+x "$tool_path" if [ "$tool" = "composer" ]; then - composer -q global config process-timeout 0 - echo "::add-path::/Users/$USER/.composer/vendor/bin" - if [ -n "$COMPOSER_TOKEN" ]; then - composer -q global config github-oauth.github.com "$COMPOSER_TOKEN" - fi + configure_composer "$tool_path" + elif [ "$tool" = "phan" ]; then + add_extension fileinfo "sudo pecl install -f fileinfo" extension >/dev/null 2>&1 + add_extension ast "sudo pecl install -f ast" extension >/dev/null 2>&1 elif [ "$tool" = "phive" ]; then add_extension curl "sudo pecl install -f curl" extension >/dev/null 2>&1 add_extension mbstring "sudo pecl install -f mbstring" extension >/dev/null 2>&1 diff --git a/src/scripts/linux.sh b/src/scripts/linux.sh index 514c93a3..e5791922 100644 --- a/src/scripts/linux.sh +++ b/src/scripts/linux.sh @@ -16,6 +16,16 @@ add_log() { fi } +# Function to backup and cleanup package lists. +cleanup_lists() { + if [ ! -e /etc/apt/sources.list.d.save ]; then + sudo mv /etc/apt/sources.list.d /etc/apt/sources.list.d.save + sudo mkdir /etc/apt/sources.list.d + sudo mv /etc/apt/sources.list.d.save/*ondrej*.list /etc/apt/sources.list.d/ + trap "sudo mv /etc/apt/sources.list.d.save/*.list /etc/apt/sources.list.d/" exit + fi +} + # Function to update php ppa update_lists() { if [ "$lists_updated" = "false" ]; then @@ -140,7 +150,23 @@ update_extension() { fi } -# Function to setup a remote tool +# Function to configure composer +configure_composer() { + tool_path=$1 + sudo ln -sf "$tool_path" "$tool_path.phar" + php -r "try {\$p=new Phar('$tool_path.phar', 0);exit(0);} catch(Exception \$e) {exit(1);}" + if [ $? -eq 1 ]; then + add_log "$cross" "composer" "Could not download composer" + exit 1; + fi + composer -q global config process-timeout 0 + echo "::add-path::/home/$USER/.composer/vendor/bin" + if [ -n "$COMPOSER_TOKEN" ]; then + composer -q global config github-oauth.github.com "$COMPOSER_TOKEN" + fi +} + +# Function to setup a remote tool. add_tool() { url=$1 tool=$2 @@ -152,11 +178,7 @@ add_tool() { if [ "$status_code" = "200" ]; then sudo chmod a+x "$tool_path" if [ "$tool" = "composer" ]; then - composer -q global config process-timeout 0 - echo "::add-path::/home/$USER/.composer/vendor/bin" - if [ -n "$COMPOSER_TOKEN" ]; then - composer -q global config github-oauth.github.com "$COMPOSER_TOKEN" - fi + configure_composer "$tool_path" elif [ "$tool" = "cs2pr" ]; then sudo sed -i 's/\r$//; s/exit(9)/exit(0)/' "$tool_path" elif [ "$tool" = "phive" ]; then diff --git a/src/scripts/win32.ps1 b/src/scripts/win32.ps1 index 091eda8a..bae86502 100644 --- a/src/scripts/win32.ps1 +++ b/src/scripts/win32.ps1 @@ -109,6 +109,32 @@ Function Remove-Extension() { } } + +Function Edit-ComposerConfig() { + Param( + [Parameter(Position = 0, Mandatory = $true)] + [ValidateNotNull()] + [ValidateLength(1, [int]::MaxValue)] + [string] + $tool_path + ) + Copy-Item $tool_path -Destination "$tool_path.phar" + php -r "try {`$p=new Phar('$tool_path.phar', 0);exit(0);} catch(Exception `$e) {exit(1);}" + if ($? -eq $False) { + Add-Log "$cross" "composer" "Could not download composer" + exit 1; + } + composer -q global config process-timeout 0 + Write-Output "::add-path::$env:APPDATA\Composer\vendor\bin" + if (Test-Path env:COMPOSER_TOKEN) { + composer -q global config github-oauth.github.com $env:COMPOSER_TOKEN + } + # TODO: Remove after composer 2.0 update, fixes peer fingerprint error + if ($version -lt 5.6) { + composer -q global config repos.packagist composer https://repo-ca-bhs-1.packagist.org + } +} + Function Add-Tool() { Param ( [Parameter(Position = 0, Mandatory = $true)] @@ -147,11 +173,9 @@ Function Add-Tool() { } elseif($tool -eq "cs2pr") { (Get-Content $php_dir/cs2pr).replace('exit(9)', 'exit(0)') | Set-Content $php_dir/cs2pr } elseif($tool -eq "composer") { - composer -q global config process-timeout 0 - Write-Output "::add-path::$env:APPDATA\Composer\vendor\bin" - if (Test-Path env:COMPOSER_TOKEN) { - composer -q global config github-oauth.github.com $env:COMPOSER_TOKEN - } + Edit-ComposerConfig $php_dir\$tool + } elseif($tool -eq "wp-cli") { + Copy-Item $php_dir\wp-cli.bat -Destination $php_dir\wp.bat } if (((Get-ChildItem -Path $php_dir/* | Where-Object Name -Match "^$tool(.exe|.phar)*$").Count -gt 0)) { diff --git a/src/tools.ts b/src/tools.ts index 298eb02b..2847ce78 100644 --- a/src/tools.ts +++ b/src/tools.ts @@ -1,4 +1,6 @@ import * as utils from './utils'; +import * as httpm from '@actions/http-client'; +import {IHttpClientResponse as hcr} from '@actions/http-client/interfaces'; /** * Function to get command to setup tools @@ -298,27 +300,28 @@ export async function addComposer(tools_list: string[]): Promise { } /** - * Function to get script to update composer + * Function to get composer URL for a given version * * @param version - * @param os_version */ -export async function updateComposer( - version: string, - os_version: string -): Promise { +export async function getComposerUrl(version: string): Promise { + const getComposerUrlHelper = async function ( + version: string + ): Promise { + const client: httpm.HttpClient = new httpm.HttpClient('setup-php'); + const response: hcr = await client.get('https://getcomposer.org/versions'); + const data = JSON.parse(await response.readBody()); + return 'https://getcomposer.org' + data[version][0]['path']; + }; switch (version) { case 'snapshot': + return 'https://getcomposer.org/composer.phar'; case 'preview': case '1': case '2': - return ( - '\ncomposer self-update --' + - version + - (await utils.suppressOutput(os_version)) - ); + return await getComposerUrlHelper(version); default: - return ''; + return 'https://getcomposer.org/composer-stable.phar'; } } @@ -473,10 +476,8 @@ export async function addTools( script += await addArchive(tool, version, url, os_version); break; case 'composer': - url = 'https://getcomposer.org/composer-stable.phar'; - script += - (await addArchive('composer', version, url, os_version)) + - (await updateComposer(version, os_version)); + url = await getComposerUrl(version); + script += await addArchive('composer', version, url, os_version); break; case 'codeception': url =