diff --git a/__tests__/packagist.test.ts b/__tests__/packagist.test.ts new file mode 100644 index 000000000..f3af7da4f --- /dev/null +++ b/__tests__/packagist.test.ts @@ -0,0 +1,50 @@ +import * as packagist from '../src/packagist'; +import nock = require('nock'); + +describe('search function', () => { + const mockResponse = { + packages: { + 'test-package': [ + { + require: { + php: '8.0.0' + }, + version: '1.0.0' + }, + { + version: '2.0.0' + } + ] + } + }; + + test('should return the version if matching php version is found', async () => { + nock('https://repo.packagist.org') + .get('/p2/test-package.json') + .reply(200, mockResponse); + const result = await packagist.search('test-package', '8.0'); + expect(result).toBe('1.0.0'); + }); + + test('should return null if no matching php version is found', async () => { + nock('https://repo.packagist.org') + .get('/p2/test-package.json') + .reply(200, mockResponse); + const result = await packagist.search('test-package', '5.6'); + expect(result).toBeNull(); + }); + + test('should return null if fetch fails', async () => { + nock('https://repo.packagist.org').get('/p2/test-package.json').reply(404); + const result = await packagist.search('test-package', '8.0'); + expect(result).toBeNull(); + }); + + test('should return null if the response is empty', async () => { + nock('https://repo.packagist.org') + .get('/p2/test-package.json') + .reply(200, {error: true, data: '[]'}); + const result = await packagist.search('test-package', '8.0'); + expect(result).toBeNull(); + }); +}); diff --git a/__tests__/tools.test.ts b/__tests__/tools.test.ts index 54c17b050..33ef7b1eb 100644 --- a/__tests__/tools.test.ts +++ b/__tests__/tools.test.ts @@ -70,6 +70,22 @@ jest.mock('../src/fetch', () => ({ ) })); +jest.mock('../src/packagist', () => ({ + search: jest + .fn() + .mockImplementation( + async ( + package_name: string, + php_version: string + ): Promise => { + if (package_name === 'phpunit/phpunit') { + return php_version + '.0'; + } + return null; + } + ) +})); + describe('Tools tests', () => { it.each` token | version @@ -387,7 +403,7 @@ describe('Tools tests', () => { 'add_tool https://github.com/phpDocumentor/phpDocumentor/releases/latest/download/phpDocumentor.phar phpDocumentor "--version"', 'add_composer_tool phplint phplint overtrue/', 'add_tool https://github.com/phpstan/phpstan/releases/latest/download/phpstan.phar phpstan "-V"', - 'add_tool https://phar.phpunit.de/phpunit.phar phpunit "--version"', + 'add_tool https://phar.phpunit.de/phpunit-7.4.0.phar phpunit "--version"', 'add_pecl', 'add_tool https://www.phing.info/get/phing-latest.phar phing "-v"', 'add_composer_tool phinx phinx robmorgan/ scoped', @@ -513,7 +529,7 @@ describe('Tools tests', () => { it.each` tools_csv | script ${'none'} | ${''} - ${'none, phpunit'} | ${'\nstep_log "Setup Tools"\nadd_tool https://github.com/shivammathur/composer-cache/releases/latest/download/composer-7.4-stable.phar,https://dl.cloudsmith.io/public/shivammathur/composer-cache/raw/files/composer-7.4-stable.phar,https://getcomposer.org/composer-stable.phar composer latest\n\nadd_tool https://phar.phpunit.de/phpunit.phar phpunit "--version"'} + ${'none, phpunit'} | ${'\nstep_log "Setup Tools"\nadd_tool https://github.com/shivammathur/composer-cache/releases/latest/download/composer-7.4-stable.phar,https://dl.cloudsmith.io/public/shivammathur/composer-cache/raw/files/composer-7.4-stable.phar,https://getcomposer.org/composer-stable.phar composer latest\n\nadd_tool https://phar.phpunit.de/phpunit-7.4.0.phar phpunit "--version"'} ${'composer:preview'} | ${'add_tool https://github.com/shivammathur/composer-cache/releases/latest/download/composer-7.4-preview.phar,https://dl.cloudsmith.io/public/shivammathur/composer-cache/raw/files/composer-7.4-preview.phar,https://getcomposer.org/composer-preview.phar composer preview'} ${'composer, composer:v1'} | ${'add_tool https://github.com/shivammathur/composer-cache/releases/latest/download/composer-7.4-1.phar,https://dl.cloudsmith.io/public/shivammathur/composer-cache/raw/files/composer-7.4-1.phar,https://getcomposer.org/composer-1.phar composer'} ${'composer:v1, composer:preview, composer:snapshot'} | ${'add_tool https://github.com/shivammathur/composer-cache/releases/latest/download/composer-7.4-snapshot.phar,https://dl.cloudsmith.io/public/shivammathur/composer-cache/raw/files/composer-7.4-snapshot.phar,https://getcomposer.org/composer.phar composer snapshot'} @@ -543,13 +559,13 @@ describe('Tools tests', () => { it.each` tools_csv | php_version | resolved - ${'phpunit'} | ${'8.2'} | ${'/phpunit.phar'} - ${'phpunit'} | ${'8.1'} | ${'/phpunit.phar'} - ${'phpunit'} | ${'8.0'} | ${'/phpunit-9.6.8.phar'} - ${'phpunit'} | ${'7.3'} | ${'/phpunit-9.6.8.phar'} - ${'phpunit'} | ${'7.2'} | ${'/phpunit-8.5.33.phar'} - ${'phpunit'} | ${'7.1'} | ${'/phpunit-7.5.20.phar'} - ${'phpunit'} | ${'7.0'} | ${'/phpunit-6.5.14.phar'} + ${'phpunit'} | ${'8.2'} | ${'/phpunit-8.2.0.phar'} + ${'phpunit'} | ${'8.1'} | ${'/phpunit-8.1.0.phar'} + ${'phpunit'} | ${'8.0'} | ${'/phpunit-8.0.0.phar'} + ${'phpunit'} | ${'7.3'} | ${'/phpunit-7.3.0.phar'} + ${'phpunit'} | ${'7.2'} | ${'/phpunit-7.2.0.phar'} + ${'phpunit'} | ${'7.1'} | ${'/phpunit-7.1.0.phar'} + ${'phpunit'} | ${'7.0'} | ${'/phpunit-7.0.0.phar'} `( 'checking error: $tools_csv', async ({tools_csv, php_version, resolved}) => { diff --git a/dist/index.js b/dist/index.js index 8ee2d0688..114b08a57 100644 --- a/dist/index.js +++ b/dist/index.js @@ -592,6 +592,64 @@ exports.run = run; /***/ }), +/***/ 5151: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.search = void 0; +const cv = __importStar(__nccwpck_require__(4773)); +const fetch = __importStar(__nccwpck_require__(2387)); +async function search(package_name, php_version) { + const response = await fetch.fetch(`https://repo.packagist.org/p2/${package_name}.json`); + if (response.error || response.data === '[]') { + return null; + } + const data = JSON.parse(response['data']); + if (data && data.packages) { + const versions = data.packages[package_name]; + versions.sort((a, b) => cv.compareVersions(b.version, a.version)); + const result = versions.find((versionData) => { + if (versionData?.require?.php) { + return versionData?.require?.php + .split('|') + .some(require => require && cv.satisfies(php_version + '.0', require)); + } + return false; + }); + return result ? result.version : null; + } + return null; +} +exports.search = search; +//# sourceMappingURL=packagist.js.map + +/***/ }), + /***/ 7740: /***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { @@ -628,6 +686,7 @@ exports.addTools = exports.functionRecord = exports.getData = exports.addWPCLI = const path_1 = __importDefault(__nccwpck_require__(1017)); const fs_1 = __importDefault(__nccwpck_require__(7147)); const fetch = __importStar(__nccwpck_require__(2387)); +const packagist = __importStar(__nccwpck_require__(5151)); const utils = __importStar(__nccwpck_require__(918)); async function getSemverVersion(data) { const search = data['version_prefix'] + data['version']; @@ -885,16 +944,10 @@ async function addPhive(data) { } exports.addPhive = addPhive; async function addPHPUnitTools(data) { - if (data['version'] == 'latest') { - if (/7\.3|8\.0/.test(data['php_version'])) { - data['version'] = '9.6.8'; - } else if (/7\.[2-3]/.test(data['php_version'])) { - data['version'] = '8.5.33'; - } else if (/7\.[1-3]/.test(data['php_version'])) { - data['version'] = '7.5.20'; - } else if (/7\.[0-2]/.test(data['php_version'])) { - data['version'] = '6.5.14'; - } + if (data['version'] === 'latest') { + data['version'] = + (await packagist.search(data['repository'], data['php_version'])) ?? + 'latest'; } data['url'] = await getPharUrl(data); return await addArchive(data); @@ -4314,6 +4367,201 @@ function copyFile(srcFile, destFile, force) { } //# sourceMappingURL=io.js.map +/***/ }), + +/***/ 4773: +/***/ (function(__unused_webpack_module, exports) { + +(function (global, factory) { + true ? factory(exports) : + 0; +})(this, (function (exports) { 'use strict'; + + /** + * Compare [semver](https://semver.org/) version strings to find greater, equal or lesser. + * This library supports the full semver specification, including comparing versions with different number of digits like `1.0.0`, `1.0`, `1`, and pre-release versions like `1.0.0-alpha`. + * @param v1 - First version to compare + * @param v2 - Second version to compare + * @returns Numeric value compatible with the [Array.sort(fn) interface](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#Parameters). + */ + const compareVersions = (v1, v2) => { + // validate input and split into segments + const n1 = validateAndParse(v1); + const n2 = validateAndParse(v2); + // pop off the patch + const p1 = n1.pop(); + const p2 = n2.pop(); + // validate numbers + const r = compareSegments(n1, n2); + if (r !== 0) + return r; + // validate pre-release + if (p1 && p2) { + return compareSegments(p1.split('.'), p2.split('.')); + } + else if (p1 || p2) { + return p1 ? -1 : 1; + } + return 0; + }; + /** + * Validate [semver](https://semver.org/) version strings. + * + * @param version Version number to validate + * @returns `true` if the version number is a valid semver version number, `false` otherwise. + * + * @example + * ``` + * validate('1.0.0-rc.1'); // return true + * validate('1.0-rc.1'); // return false + * validate('foo'); // return false + * ``` + */ + const validate = (version) => typeof version === 'string' && /^[v\d]/.test(version) && semver.test(version); + /** + * Compare [semver](https://semver.org/) version strings using the specified operator. + * + * @param v1 First version to compare + * @param v2 Second version to compare + * @param operator Allowed arithmetic operator to use + * @returns `true` if the comparison between the firstVersion and the secondVersion satisfies the operator, `false` otherwise. + * + * @example + * ``` + * compare('10.1.8', '10.0.4', '>'); // return true + * compare('10.0.1', '10.0.1', '='); // return true + * compare('10.1.1', '10.2.2', '<'); // return true + * compare('10.1.1', '10.2.2', '<='); // return true + * compare('10.1.1', '10.2.2', '>='); // return false + * ``` + */ + const compare = (v1, v2, operator) => { + // validate input operator + assertValidOperator(operator); + // since result of compareVersions can only be -1 or 0 or 1 + // a simple map can be used to replace switch + const res = compareVersions(v1, v2); + return operatorResMap[operator].includes(res); + }; + /** + * Match [npm semver](https://docs.npmjs.com/cli/v6/using-npm/semver) version range. + * + * @param version Version number to match + * @param range Range pattern for version + * @returns `true` if the version number is within the range, `false` otherwise. + * + * @example + * ``` + * satisfies('1.1.0', '^1.0.0'); // return true + * satisfies('1.1.0', '~1.0.0'); // return false + * ``` + */ + const satisfies = (version, range) => { + // handle multiple comparators + if (range.includes('||')) { + return range.split('||').some((r) => satisfies(version, r)); + } + else if (range.includes(' ')) { + return range + .trim() + .replace(/\s{2,}/g, ' ') + .split(' ') + .every((r) => satisfies(version, r)); + } + // if no range operator then "=" + const m = range.match(/^([<>=~^]+)/); + const op = m ? m[1] : '='; + // if gt/lt/eq then operator compare + if (op !== '^' && op !== '~') + return compare(version, range, op); + // else range of either "~" or "^" is assumed + const [v1, v2, v3, , vp] = validateAndParse(version); + const [r1, r2, r3, , rp] = validateAndParse(range); + const v = [v1, v2, v3]; + const r = [r1, r2 !== null && r2 !== void 0 ? r2 : 'x', r3 !== null && r3 !== void 0 ? r3 : 'x']; + // validate pre-release + if (rp) { + if (!vp) + return false; + if (compareSegments(v, r) !== 0) + return false; + if (compareSegments(vp.split('.'), rp.split('.')) === -1) + return false; + } + // first non-zero number + const nonZero = r.findIndex((v) => v !== '0') + 1; + // pointer to where segments can be >= + const i = op === '~' ? 2 : nonZero > 1 ? nonZero : 1; + // before pointer must be equal + if (compareSegments(v.slice(0, i), r.slice(0, i)) !== 0) + return false; + // after pointer must be >= + if (compareSegments(v.slice(i), r.slice(i)) === -1) + return false; + return true; + }; + const semver = /^[v^~<>=]*?(\d+)(?:\.([x*]|\d+)(?:\.([x*]|\d+)(?:\.([x*]|\d+))?(?:-([\da-z\-]+(?:\.[\da-z\-]+)*))?(?:\+[\da-z\-]+(?:\.[\da-z\-]+)*)?)?)?$/i; + const validateAndParse = (version) => { + if (typeof version !== 'string') { + throw new TypeError('Invalid argument expected string'); + } + const match = version.match(semver); + if (!match) { + throw new Error(`Invalid argument not valid semver ('${version}' received)`); + } + match.shift(); + return match; + }; + const isWildcard = (s) => s === '*' || s === 'x' || s === 'X'; + const tryParse = (v) => { + const n = parseInt(v, 10); + return isNaN(n) ? v : n; + }; + const forceType = (a, b) => typeof a !== typeof b ? [String(a), String(b)] : [a, b]; + const compareStrings = (a, b) => { + if (isWildcard(a) || isWildcard(b)) + return 0; + const [ap, bp] = forceType(tryParse(a), tryParse(b)); + if (ap > bp) + return 1; + if (ap < bp) + return -1; + return 0; + }; + const compareSegments = (a, b) => { + for (let i = 0; i < Math.max(a.length, b.length); i++) { + const r = compareStrings(a[i] || '0', b[i] || '0'); + if (r !== 0) + return r; + } + return 0; + }; + const operatorResMap = { + '>': [1], + '>=': [0, 1], + '=': [0], + '<=': [-1, 0], + '<': [-1], + }; + const allowedOperators = Object.keys(operatorResMap); + const assertValidOperator = (op) => { + if (typeof op !== 'string') { + throw new TypeError(`Invalid operator type, expected string but got ${typeof op}`); + } + if (allowedOperators.indexOf(op) === -1) { + throw new Error(`Invalid operator, expected one of ${allowedOperators.join('|')}`); + } + }; + + exports.compare = compare; + exports.compareVersions = compareVersions; + exports.satisfies = satisfies; + exports.validate = validate; + +})); +//# sourceMappingURL=index.js.map + + /***/ }), /***/ 4294: diff --git a/package-lock.json b/package-lock.json index edeeed3c9..9f9223198 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,8 @@ "dependencies": { "@actions/core": "^1.10.0", "@actions/exec": "^1.1.1", - "@actions/io": "^1.1.3" + "@actions/io": "^1.1.3", + "compare-versions": "^6.0.0-rc.1" }, "devDependencies": { "@types/jest": "^29.5.1", @@ -2024,6 +2025,11 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/compare-versions": { + "version": "6.0.0-rc.1", + "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-6.0.0-rc.1.tgz", + "integrity": "sha512-cFhkjbGY1jLFWIV7KegECbfuyYPxSGvgGkdkfM+ibboQDoPwg2FRHm5BSNTOApiauRBzJIQH7qvOJs2sW5ueKQ==" + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -7229,6 +7235,11 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "compare-versions": { + "version": "6.0.0-rc.1", + "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-6.0.0-rc.1.tgz", + "integrity": "sha512-cFhkjbGY1jLFWIV7KegECbfuyYPxSGvgGkdkfM+ibboQDoPwg2FRHm5BSNTOApiauRBzJIQH7qvOJs2sW5ueKQ==" + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", diff --git a/package.json b/package.json index 7f00d0660..d47805e7a 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,8 @@ "dependencies": { "@actions/core": "^1.10.0", "@actions/exec": "^1.1.1", - "@actions/io": "^1.1.3" + "@actions/io": "^1.1.3", + "compare-versions": "^6.0.0-rc.1" }, "devDependencies": { "@types/jest": "^29.5.1", diff --git a/src/configs/tools.json b/src/configs/tools.json index 5cefede22..b3969a7d7 100644 --- a/src/configs/tools.json +++ b/src/configs/tools.json @@ -267,7 +267,7 @@ }, "phpunit": { "type": "custom-function", - "repository": "sebastianbergmann/phpunit", + "repository": "phpunit/phpunit", "domain": "https://phar.phpunit.de", "function": "phpunit", "version_prefix": "", diff --git a/src/packagist.ts b/src/packagist.ts new file mode 100644 index 000000000..46d6a3d16 --- /dev/null +++ b/src/packagist.ts @@ -0,0 +1,37 @@ +import * as cv from 'compare-versions'; +import * as fetch from './fetch'; + +type RS = Record; +type RSRS = Record; + +export async function search( + package_name: string, + php_version: string +): Promise { + const response = await fetch.fetch( + `https://repo.packagist.org/p2/${package_name}.json` + ); + if (response.error || response.data === '[]') { + return null; + } + + const data = JSON.parse(response['data']); + if (data && data.packages) { + const versions = data.packages[package_name]; + versions.sort((a: RS, b: RS) => cv.compareVersions(b.version, a.version)); + + const result = versions.find((versionData: RSRS) => { + if (versionData?.require?.php) { + return versionData?.require?.php + .split('|') + .some( + require => require && cv.satisfies(php_version + '.0', require) + ); + } + return false; + }); + + return result ? result.version : null; + } + return null; +} diff --git a/src/tools.ts b/src/tools.ts index 50972754f..d9d1295a4 100644 --- a/src/tools.ts +++ b/src/tools.ts @@ -1,6 +1,7 @@ import path from 'path'; import fs from 'fs'; import * as fetch from './fetch'; +import * as packagist from './packagist'; import * as utils from './utils'; type RS = Record; @@ -392,16 +393,10 @@ export async function addPhive(data: RS): Promise { * @param data */ export async function addPHPUnitTools(data: RS): Promise { - if (data['version'] == 'latest') { - if (/7\.3|8\.0/.test(data['php_version'])) { - data['version'] = '9.6.8'; - } else if (/7\.[2-3]/.test(data['php_version'])) { - data['version'] = '8.5.33'; - } else if (/7\.[1-3]/.test(data['php_version'])) { - data['version'] = '7.5.20'; - } else if (/7\.[0-2]/.test(data['php_version'])) { - data['version'] = '6.5.14'; - } + if (data['version'] === 'latest') { + data['version'] = + (await packagist.search(data['repository'], data['php_version'])) ?? + 'latest'; } data['url'] = await getPharUrl(data); return await addArchive(data);