Compare commits

..

1 Commits

Author SHA1 Message Date
Shivam Mathur
85c019fd02 Release v2-verbose 2025-11-26 21:52:34 +05:30
35 changed files with 839 additions and 1232 deletions

View File

@@ -88,7 +88,7 @@ jobs:
Remove-Item "$env:file.all" -Force Remove-Item "$env:file.all" -Force
Remove-Item "$env:file.builtin" -Force Remove-Item "$env:file.builtin" -Force
- name: Upload artifacts - name: Upload artifacts
uses: actions/upload-artifact@v6 uses: actions/upload-artifact@v5
with: with:
name: lists-php${{ matrix.php-versions }}-${{ matrix.operating-system }}.md name: lists-php${{ matrix.php-versions }}-${{ matrix.operating-system }}.md
path: php${{ matrix.php-versions }}-${{ matrix.operating-system }}.md path: php${{ matrix.php-versions }}-${{ matrix.operating-system }}.md
@@ -105,7 +105,7 @@ jobs:
with: with:
repository: ${{ github.repository }}.wiki repository: ${{ github.repository }}.wiki
- name: Download artifacts - name: Download artifacts
uses: actions/download-artifact@v7 uses: actions/download-artifact@v6
with: with:
path: ${{ github.workspace }}/lists path: ${{ github.workspace }}/lists
pattern: lists-* pattern: lists-*

View File

@@ -33,10 +33,10 @@ jobs:
with: with:
fetch-depth: 2 fetch-depth: 2
- name: Setup Node.js 24.x - name: Setup Node.js 20.x
uses: actions/setup-node@v6 uses: actions/setup-node@v6
with: with:
node-version: 24.x node-version: 20.x
- name: Install dependencies - name: Install dependencies
run: npm install run: npm install

View File

@@ -50,7 +50,7 @@ jobs:
key: ${{ env.key }} key: ${{ env.key }}
- name: Cache extensions - name: Cache extensions
uses: actions/cache@v5 uses: actions/cache@v4
with: with:
path: ${{ steps.cache-env.outputs.dir }} path: ${{ steps.cache-env.outputs.dir }}
key: ${{ steps.cache-env.outputs.key }} key: ${{ steps.cache-env.outputs.key }}

View File

@@ -74,12 +74,13 @@ Both `GitHub-hosted` and `self-hosted` runners are supported by `setup-php` on t
| Ubuntu 22.04 | x86_64 | `ubuntu-22.04` | `PHP 8.1` | | Ubuntu 22.04 | x86_64 | `ubuntu-22.04` | `PHP 8.1` |
| Ubuntu 24.04 | aarch64 | `ubuntu-24.04-arm` | `PHP 8.3` | | Ubuntu 24.04 | aarch64 | `ubuntu-24.04-arm` | `PHP 8.3` |
| Ubuntu 22.04 | aarch64 | `ubuntu-22.04-arm` | `PHP 8.1` | | Ubuntu 22.04 | aarch64 | `ubuntu-22.04-arm` | `PHP 8.1` |
| Windows Server 2025 | x64 | `windows-2025` | `PHP 8.5` | | Windows Server 2025 | x64 | `windows-2025` | `PHP 8.3` |
| Windows Server 2022 | x64 | `windows-latest` or `windows-2022` | `PHP 8.5` | | Windows Server 2022 | x64 | `windows-latest` or `windows-2022` | `PHP 8.3` |
| macOS Tahoe 26.x | arm64 | `macos-26` | - | | macOS Tahoe 26.x | arm64 | `macos-26` | - |
| macOS Sequoia 15.x | arm64 | `macos-latest` or `macos-15` | - | | macOS Sequoia 15.x | arm64 | `macos-latest` or `macos-15` | - |
| macOS Sonoma 14.x | arm64 | `macos-14` | - | | macOS Sonoma 14.x | arm64 | `macos-14` | - |
| macOS Sequoia 15.x | x86_64 | `macos-15-intel` | `PHP 8.5` | | macOS Sequoia 15.x | x86_64 | `macos-15-intel` | `PHP 8.3` |
| macOS Ventura 13.x | x86_64 | `macos-13` | `PHP 8.3` |
### Self-Hosted Runners ### Self-Hosted Runners
@@ -120,9 +121,9 @@ On all supported OS/Platforms, the following PHP versions can be set up as per t
| `7.3` | `Stable` | `End of life` | `GitHub-hosted`, `self-hosted` | | `7.3` | `Stable` | `End of life` | `GitHub-hosted`, `self-hosted` |
| `7.4` | `Stable` | `End of life` | `GitHub-hosted`, `self-hosted` | | `7.4` | `Stable` | `End of life` | `GitHub-hosted`, `self-hosted` |
| `8.0` | `Stable` | `End of life` | `GitHub-hosted`, `self-hosted` | | `8.0` | `Stable` | `End of life` | `GitHub-hosted`, `self-hosted` |
| `8.1` | `Stable` | `End of life` | `GitHub-hosted`, `self-hosted` | | `8.1` | `Stable` | `Security fixes only` | `GitHub-hosted`, `self-hosted` |
| `8.2` | `Stable` | `Security fixes only` | `GitHub-hosted`, `self-hosted` | | `8.2` | `Stable` | `Security fixes only` | `GitHub-hosted`, `self-hosted` |
| `8.3` | `Stable` | `Security fixes only` | `GitHub-hosted`, `self-hosted` | | `8.3` | `Stable` | `Active` | `GitHub-hosted`, `self-hosted` |
| `8.4` | `Stable` | `Active` | `GitHub-hosted`, `self-hosted` | | `8.4` | `Stable` | `Active` | `GitHub-hosted`, `self-hosted` |
| `8.5` | `Stable` | `Active` | `GitHub-hosted`, `self-hosted` | | `8.5` | `Stable` | `Active` | `GitHub-hosted`, `self-hosted` |
| `8.6` | `Nightly` | `In development` | `GitHub-hosted`, `self-hosted` | | `8.6` | `Nightly` | `In development` | `GitHub-hosted`, `self-hosted` |

View File

@@ -1,106 +0,0 @@
import * as core from '../src/core';
describe('Core tests', () => {
const originalEnv = process.env;
const originalExitCode = process.exitCode;
let stdoutOutput: string;
const originalWrite = process.stdout.write;
beforeEach(() => {
process.env = {...originalEnv};
process.exitCode = undefined;
stdoutOutput = '';
process.stdout.write = jest.fn((chunk: string | Uint8Array): boolean => {
stdoutOutput += chunk.toString();
return true;
}) as unknown as typeof process.stdout.write;
});
afterEach(() => {
process.env = originalEnv;
process.exitCode = originalExitCode;
process.stdout.write = originalWrite;
});
it('checking issueCommand with no properties', () => {
core.issueCommand('warning', {}, 'test message');
expect(stdoutOutput).toContain('::warning::test message');
});
it('checking issueCommand with properties', () => {
core.issueCommand('error', {file: 'test.ts', line: '10'}, 'error message');
expect(stdoutOutput).toContain(
'::error file=test.ts,line=10::error message'
);
});
it('checking issueCommand escapes special characters in message', () => {
core.issueCommand('warning', {}, 'line1\nline2\rline3%percent');
expect(stdoutOutput).toContain(
'::warning::line1%0Aline2%0Dline3%25percent'
);
});
it('checking issueCommand escapes special characters in properties', () => {
core.issueCommand('error', {file: 'path:to,file'}, 'message');
expect(stdoutOutput).toContain('::error file=path%3Ato%2Cfile::message');
});
it('checking issueCommand with Error object', () => {
const error = new Error('test error');
core.issueCommand('error', {}, error);
expect(stdoutOutput).toContain('::error::Error: test error');
});
it('checking issueCommand filters empty properties', () => {
core.issueCommand('warning', {file: 'test.ts', line: ''}, 'message');
expect(stdoutOutput).toContain('::warning file=test.ts::message');
});
it('checking error', () => {
core.error('error message');
expect(stdoutOutput).toContain('::error::error message');
});
it('checking error with Error object', () => {
core.error(new Error('error instance'));
expect(stdoutOutput).toContain('::error::Error: error instance');
});
it('checking setFailed', () => {
core.setFailed('failure message');
expect(process.exitCode).toBe(1);
expect(stdoutOutput).toContain('::error::failure message');
});
it('checking setFailed with Error object', () => {
core.setFailed(new Error('failure error'));
expect(process.exitCode).toBe(1);
expect(stdoutOutput).toContain('::error::Error: failure error');
});
it('checking getInput returns value', () => {
process.env['INPUT_TEST-INPUT'] = 'test value';
expect(core.getInput('test-input')).toBe('test value');
});
it('checking getInput trims value', () => {
process.env['INPUT_TEST-INPUT'] = ' trimmed ';
expect(core.getInput('test-input')).toBe('trimmed');
});
it('checking getInput returns empty string for missing input', () => {
expect(core.getInput('missing-input')).toBe('');
});
it('checking getInput throws for required missing input', () => {
expect(() => core.getInput('missing-input', true)).toThrow(
'Input required and not supplied: missing-input'
);
});
it('checking getInput handles spaces in name', () => {
process.env['INPUT_INPUT_WITH_SPACES'] = 'spaced value';
expect(core.getInput('input with spaces')).toBe('spaced value');
});
});

View File

@@ -131,11 +131,7 @@ describe('Extension tests', () => {
) )
? `add_${ext_name}` ? `add_${ext_name}`
: `add_brew_extension ${formula} ${prefix}`; : `add_brew_extension ${formula} ${prefix}`;
return [ return [formula, formula === 'phalcon3' ? '7.3' : '7.4', output];
formula,
formula.match(/phalcon3|lua|propro/) ? '7.3' : '8.1',
output
];
}); });
it.each(data)( it.each(data)(

View File

@@ -19,9 +19,7 @@ it('checking fetch', async () => {
Location: host_url + '/pong' Location: host_url + '/pong'
}) })
.get('/pong') .get('/pong')
.reply(200, 'pong') .reply(200, 'pong');
.get('/error')
.replyWithError('Network failure');
let response: Record<string, string> = await fetch.fetch(manifest_url); let response: Record<string, string> = await fetch.fetch(manifest_url);
expect(response.error).toBe(undefined); expect(response.error).toBe(undefined);
@@ -38,8 +36,4 @@ it('checking fetch', async () => {
response = await fetch.fetch(manifest_url, 'invalid_token'); response = await fetch.fetch(manifest_url, 'invalid_token');
expect(response.error).not.toBe(undefined); expect(response.error).not.toBe(undefined);
expect(response.data).toBe(undefined); expect(response.data).toBe(undefined);
response = await fetch.fetch(host_url + '/error');
expect(response.error).toContain('Fetch error:');
expect(response.data).toBe(undefined);
}); });

View File

@@ -1,33 +1,40 @@
import * as fs from 'fs'; import * as fs from 'fs';
import * as tools from '../src/tools'; import * as tools from '../src/tools';
import {ToolData, ToolInput} from '../src/tools';
function getData(data: Partial<ToolData>): ToolData { interface IData {
const tool = data.tool || 'tool'; tool: string;
const version = data.version || ''; version?: string;
domain?: string;
extension?: string;
os?: string;
php_version?: string;
release?: string;
repository?: string;
scope?: string;
type?: string;
fetch_latest?: string;
version_parameter?: string;
version_prefix?: string;
}
function getData(data: IData): Record<string, string> {
return { return {
tool, tool: data.tool,
version, version: data.version || '',
url: data.url || '',
domain: data.domain || 'https://example.com', domain: data.domain || 'https://example.com',
extension: data.extension || '.phar', extension: data.extension || '.phar',
os: data.os || 'linux', os: data.os || 'linux',
php_version: data.php_version || '7.4', php_version: data.php_version || '7.4',
release: data.release || [tool, version].join(':'), release: data.release || [data.tool, data.version].join(':'),
repository: data.repository || '', repository: data.repository || '',
scope: data.scope || 'global', scope: data.scope || 'global',
type: data.type || 'phar', type: data.type || 'phar',
fetch_latest: data.fetch_latest || 'false', fetch_latest: data.fetch_latest || 'false',
version_parameter: data.version_parameter || '-V', version_parameter: data.version_parameter || '-V',
version_prefix: data.version_prefix || '', version_prefix: data.version_prefix || '',
github: data.github || 'https://github.com', github: 'https://github.com',
prefix: data.prefix || 'releases', prefix: 'releases',
verb: data.verb || 'download', verb: 'download'
packagist: data.packagist || data.repository || '',
function: data.function,
alias: data.alias,
uri: data.uri,
error: data.error
}; };
} }
@@ -154,18 +161,6 @@ describe('Tools tests', () => {
} }
); );
it('checking getLatestVersion with fetch_latest=true but no repository', async () => {
expect(
await tools.getLatestVersion(
getData({
tool: 'tool',
repository: '',
fetch_latest: 'true'
})
)
).toBe('latest');
});
it.each` it.each`
version | tool | type | expected version | tool | type | expected
${'latest'} | ${'tool'} | ${'phar'} | ${'latest'} ${'latest'} | ${'tool'} | ${'phar'} | ${'latest'}
@@ -250,15 +245,13 @@ describe('Tools tests', () => {
); );
it('checking getUrl handles undefined version without double slash', async () => { it('checking getUrl handles undefined version without double slash', async () => {
const data: ToolInput = { const data = getData({
...getData({
tool: 'cs2pr', tool: 'cs2pr',
repository: 'staabm/annotate-pull-request-from-checkstyle', repository: 'staabm/annotate-pull-request-from-checkstyle',
domain: 'https://github.com' domain: 'https://github.com'
}), });
version: undefined data['extension'] = '';
}; delete data['version'];
data.extension = '';
expect(await tools.getUrl(data)).toBe( expect(await tools.getUrl(data)).toBe(
'https://github.com/staabm/annotate-pull-request-from-checkstyle/releases/download/cs2pr' 'https://github.com/staabm/annotate-pull-request-from-checkstyle/releases/download/cs2pr'
); );
@@ -291,9 +284,9 @@ describe('Tools tests', () => {
tool: 'tool', tool: 'tool',
version: 'latest', version: 'latest',
version_parameter: JSON.stringify('-v'), version_parameter: JSON.stringify('-v'),
os: os, os: os
url: 'https://example.com/tool.phar'
}); });
data['url'] = 'https://example.com/tool.phar';
expect(await tools.addArchive(data)).toContain(script); expect(await tools.addArchive(data)).toContain(script);
}); });
@@ -656,43 +649,14 @@ describe('Tools tests', () => {
expect(await tools.addTools(tools_csv, '7.4', 'linux')).toContain(script); expect(await tools.addTools(tools_csv, '7.4', 'linux')).toContain(script);
}); });
it('checking error when custom-function tool is missing function field', async () => { it.each`
const brokenToolsJson = JSON.stringify({ tools_csv | token | script
composer: { ${'cs2pr:1.2'} | ${'invalid_token'} | ${'add_log "$cross" "cs2pr" "Invalid token"'}
type: 'custom-function', ${'phpunit:1.2'} | ${'invalid_token'} | ${'add_log "$cross" "phpunit" "Invalid token"'}
domain: 'https://getcomposer.org', ${'phpunit:0.1'} | ${'no_data'} | ${'add_log "$cross" "phpunit" "No version found with prefix 0.1."'}
repository: 'composer/composer', `('checking error: $tools_csv', async ({tools_csv, token, script}) => {
function: 'composer' process.env['GITHUB_TOKEN'] = token;
}, expect(await tools.addTools(tools_csv, '7.4', 'linux')).toContain(script);
'broken-tool': {
type: 'custom-function'
}
});
let result: string = '';
await jest.isolateModulesAsync(async () => {
jest.doMock('fs', () => ({
...jest.requireActual('fs'),
readFileSync: (
filePath: fs.PathOrFileDescriptor,
options?: unknown
) => {
if (String(filePath).includes('tools.json')) {
return brokenToolsJson;
}
return (jest.requireActual('fs') as typeof fs).readFileSync(
filePath,
options as fs.ObjectEncodingOptions & {flag?: string}
);
}
}));
const isolatedTools = await import('../src/tools');
result = await isolatedTools.addTools('broken-tool', '7.4', 'linux');
});
expect(result).toContain(
'add_log "$cross" "broken-tool" "broken-tool has no function defined. Please report this issue."'
);
}); });
it.each` it.each`

View File

@@ -3,6 +3,16 @@ import * as path from 'path';
import * as utils from '../src/utils'; import * as utils from '../src/utils';
import * as fetchModule from '../src/fetch'; import * as fetchModule from '../src/fetch';
/**
* Mock @actions/core
*/
jest.mock('@actions/core', () => ({
getInput: jest.fn().mockImplementation(key => {
return ['setup-php'].indexOf(key) !== -1 ? key : '';
}),
info: jest.fn()
}));
describe('Utils tests', () => { describe('Utils tests', () => {
it('checking readEnv', async () => { it('checking readEnv', async () => {
process.env['test'] = 'setup-php'; process.env['test'] = 'setup-php';
@@ -16,14 +26,12 @@ describe('Utils tests', () => {
it('checking getInput', async () => { it('checking getInput', async () => {
process.env['test'] = 'setup-php'; process.env['test'] = 'setup-php';
process.env['INPUT_SETUP-PHP'] = 'setup-php';
expect(await utils.getInput('test', false)).toBe('setup-php'); expect(await utils.getInput('test', false)).toBe('setup-php');
expect(await utils.getInput('setup-php', false)).toBe('setup-php'); expect(await utils.getInput('setup-php', false)).toBe('setup-php');
expect(await utils.getInput('DoesNotExist', false)).toBe(''); expect(await utils.getInput('DoesNotExist', false)).toBe('');
await expect(async () => { await expect(async () => {
await utils.getInput('DoesNotExist', true); await utils.getInput('DoesNotExist', true);
}).rejects.toThrow('Input required and not supplied: DoesNotExist'); }).rejects.toThrow('Input required and not supplied: DoesNotExist');
delete process.env['INPUT_SETUP-PHP'];
}); });
it('checking getManifestURL', async () => { it('checking getManifestURL', async () => {

View File

@@ -34,5 +34,5 @@ outputs:
php-version: php-version:
description: 'PHP version in semver format' description: 'PHP version in semver format'
runs: runs:
using: 'node24' using: 'node20'
main: 'dist/index.js' main: 'dist/index.js'

4
dist/index.js vendored

File diff suppressed because one or more lines are too long

View File

@@ -1,22 +1,39 @@
import {fixupConfigRules, fixupPluginRules} from '@eslint/compat';
// eslint-disable-next-line import/no-unresolved
import typescriptEslint from '@typescript-eslint/eslint-plugin'; import typescriptEslint from '@typescript-eslint/eslint-plugin';
import importPlugin from 'eslint-plugin-import';
import jest from 'eslint-plugin-jest'; import jest from 'eslint-plugin-jest';
import prettierRecommended from 'eslint-plugin-prettier/recommended';
import eslintConfigPrettier from 'eslint-config-prettier';
import globals from 'globals'; import globals from 'globals';
// eslint-disable-next-line import/no-unresolved
import tsParser from '@typescript-eslint/parser'; import tsParser from '@typescript-eslint/parser';
import path from 'node:path';
import {fileURLToPath} from 'node:url';
import js from '@eslint/js'; import js from '@eslint/js';
import {FlatCompat} from '@eslint/eslintrc';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all
});
export default [ export default [
js.configs.recommended, ...fixupConfigRules(
...typescriptEslint.configs['flat/recommended'], compat.extends(
importPlugin.flatConfigs.errors, 'eslint:recommended',
importPlugin.flatConfigs.warnings, 'plugin:@typescript-eslint/eslint-recommended',
importPlugin.flatConfigs.typescript, 'plugin:@typescript-eslint/recommended',
prettierRecommended, 'plugin:import/errors',
eslintConfigPrettier, 'plugin:import/warnings',
'plugin:import/typescript',
'plugin:prettier/recommended',
'prettier'
)
),
{ {
plugins: { plugins: {
'@typescript-eslint': fixupPluginRules(typescriptEslint),
jest jest
}, },
@@ -29,21 +46,6 @@ export default [
parser: tsParser, parser: tsParser,
ecmaVersion: 2021, ecmaVersion: 2021,
sourceType: 'module' sourceType: 'module'
},
settings: {
'import/resolver': {
typescript: {
alwaysTryTypes: true,
project: './tsconfig.json'
},
node: {
extensions: ['.js', '.ts']
}
},
'import/parsers': {
'@typescript-eslint/parser': ['.ts']
}
} }
} }
]; ];

676
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -34,36 +34,36 @@
"author": "shivammathur", "author": "shivammathur",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@actions/exec": "^2.0.0", "@actions/core": "^1.11.1",
"@actions/exec": "^1.1.1",
"@actions/io": "^2.0.0",
"compare-versions": "^6.1.1" "compare-versions": "^6.1.1"
}, },
"devDependencies": { "devDependencies": {
"@eslint/compat": "^2.0.2", "@eslint/compat": "^2.0.0",
"@eslint/js": "^9.39.1", "@eslint/js": "9.39.1",
"@types/jest": "^30.0.0", "@types/jest": "^30.0.0",
"@types/node": "^25.2.2", "@types/node": "^24.10.1",
"@typescript-eslint/eslint-plugin": "^8.54.0", "@typescript-eslint/eslint-plugin": "^8.48.0",
"@typescript-eslint/parser": "^8.54.0", "@typescript-eslint/parser": "^8.48.0",
"@vercel/ncc": "^0.38.4", "@vercel/ncc": "^0.38.4",
"eslint": "^9.39.1", "eslint": "9.39.1",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"eslint-import-resolver-typescript": "^4.4.4",
"eslint-plugin-import": "^2.32.0", "eslint-plugin-import": "^2.32.0",
"eslint-plugin-jest": "^29.12.2", "eslint-plugin-jest": "^29.2.1",
"eslint-plugin-prettier": "^5.5.5", "eslint-plugin-prettier": "^5.5.4",
"globals": "^17.3.0", "globals": "^16.5.0",
"jest": "^30.2.0", "jest": "^30.2.0",
"jest-circus": "^30.2.0", "jest-circus": "^30.2.0",
"nock": "^14.0.10", "nock": "^14.0.10",
"prettier": "^3.8.1", "prettier": "^3.6.2",
"simple-git-hooks": "^2.13.1", "simple-git-hooks": "^2.13.1",
"ts-jest": "^29.4.6", "ts-jest": "^29.4.5",
"typescript": "^5.9.3" "typescript": "^5.9.3"
}, },
"overrides": { "overrides": {
"test-exclude": "^7.0.1", "test-exclude": "^7.0.1",
"glob": "^11.1.0", "glob": "^11.1.0"
"minimatch": "^10.2.1"
}, },
"bugs": { "bugs": {
"url": "https://github.com/shivammathur/setup-php/issues" "url": "https://github.com/shivammathur/setup-php/issues"

View File

@@ -1,40 +1,29 @@
amqp=amqp amqp=amqp
apcu=apcu apcu=apcu
ast=ast ast=ast
brotli=brotli
couchbase=couchbase couchbase=couchbase
ds=ds ds=ds
event=event event=event
excimer=excimer
expect=expect expect=expect
gearman=gearman gearman=gearman
gmagick=gmagick
gnupg=gnupg gnupg=gnupg
grpc=grpc grpc=grpc
igbinary=igbinary igbinary=igbinary
imagick=imagick imagick=imagick
imap=imap imap=imap
interbase=interbase
lua=lua lua=lua
mailparse=mailparse mailparse=mailparse
maxminddb=maxminddb
mcrypt=mcrypt mcrypt=mcrypt
memcache=memcache memcache=memcache
memcached=memcached memcached=memcached
mongodb=mongodb mongodb=mongodb
mongodb1=mongodb1
msgpack=msgpack msgpack=msgpack
newrelic=newrelic
oauth=oauth
opentelemetry=opentelemetry
pcov=pcov pcov=pcov
pdo_firebird=pdo_firebird
pdo_sqlsrv=pdo_sqlsrv pdo_sqlsrv=pdo_sqlsrv
pecl_http=http pecl_http=http
phalcon3=phalcon phalcon3=phalcon
phalcon4=phalcon phalcon4=phalcon
phalcon5=phalcon phalcon5=phalcon
pinba=pinba
propro=propro propro=propro
protobuf=protobuf protobuf=protobuf
psr=psr psr=psr
@@ -42,24 +31,16 @@ raphf=raphf
rdkafka=rdkafka rdkafka=rdkafka
phpredis=redis phpredis=redis
redis=redis redis=redis
seaslog=seaslog
scalar_objects=scalar_objects
snmp=snmp snmp=snmp
sqlsrv=sqlsrv sqlsrv=sqlsrv
spx=spx
ssh2=ssh2 ssh2=ssh2
swoole=swoole swoole=swoole
swow=swow
uopz=uopz
uploadprogress=uploadprogress
uuid=uuid uuid=uuid
v8js=v8js v8js=v8js
vips=vips vips=vips
vld=vld vld=vld
xdebug=xdebug xdebug=xdebug
xdebug2=xdebug xdebug2=xdebug
xhprof=xhprof
xlswriter=xlswriter xlswriter=xlswriter
yaml=yaml yaml=yaml
zmq=zmq zmq=zmq
zstd=zstd

View File

@@ -1,112 +0,0 @@
import {EOL} from 'os';
/**
* Commands
*
* Command Format:
* ::name key=value,key=value::message
*
* @see https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions
*/
interface CommandProperties {
[key: string]: string;
}
/**
* Sanitizes the message for use in a workflow command.
* @param message
*/
function toCommandValue(message: string | Error): string {
if (message instanceof Error) {
return message.toString();
}
return message;
}
/**
* Escapes data for safe use in workflow command messages.
* @param s
*/
function escapeData(s: string | Error): string {
return toCommandValue(s)
.replace(/%/g, '%25')
.replace(/\r/g, '%0D')
.replace(/\n/g, '%0A');
}
/**
* Escapes property values for safe use in workflow command properties.
* @param s
*/
function escapeProperty(s: string): string {
return s
.replace(/%/g, '%25')
.replace(/\r/g, '%0D')
.replace(/\n/g, '%0A')
.replace(/:/g, '%3A')
.replace(/,/g, '%2C');
}
/**
* Issues a command to the GitHub Actions runner.
*
* @param command - The command name to issue
* @param properties - Additional properties for the command (key-value pairs)
* @param message - The message to include with the command
*/
export function issueCommand(
command: string,
properties: CommandProperties,
message: string | Error
): void {
let cmdStr = `::${command}`;
if (properties && Object.keys(properties).length > 0) {
cmdStr += ' ';
const props = Object.entries(properties)
.filter(([, val]) => val)
.map(([key, val]) => `${key}=${escapeProperty(val)}`)
.join(',');
cmdStr += props;
}
cmdStr += `::${escapeData(message)}`;
process.stdout.write(cmdStr + EOL);
}
/**
* Adds an error issue.
* @param message - error issue message
*/
export function error(message: string | Error): void {
issueCommand('error', {}, message);
}
/**
* Sets the action status to failed.
* When the action exits it will be with an exit code of 1.
* @param message - add error issue message
*/
export function setFailed(message: string | Error): void {
process.exitCode = 1;
error(message);
}
/**
* Gets the value of an input.
* The value is trimmed.
* Returns an empty string if the value is not defined.
*
* @param name - name of the input to get
* @param required - whether the input is required
* @returns string
*/
export function getInput(name: string, required = false): string {
const val: string =
process.env[`INPUT_${name.replace(/ /g, '_').toUpperCase()}`] || '';
if (required && !val) {
throw new Error(`Input required and not supplied: ${name}`);
}
return val.trim();
}

View File

@@ -85,14 +85,13 @@ export async function addExtensionDarwin(
add_script += await utils.getUnsupportedLog('pcov', version, 'darwin'); add_script += await utils.getUnsupportedLog('pcov', version, 'darwin');
return; return;
// match brew extensions // match brew extensions
case /(?<!5\.[3-5])(amqp|apcu|brotli|excimer|expect|gmagick|gnupg|grpc|igbinary|imagick|imap|interbase|mailparse|maxminddb|mcrypt|memcache|memcached|mongodb|mongodb1|msgpack|newrelic|oauth|opentelemetry|pdo_firebird|pinba|protobuf|psr|raphf|rdkafka|redis|scalar_objects|seaslog|snmp|spx|ssh2|swoole|uopz|uploadprogress|uuid|vld|xdebug|xdebug2|xhprof|yaml|zmq|zstd)/.test( case /(?<!5\.[3-5])(amqp|apcu|expect|gnupg|grpc|igbinary|imagick|imap|mailparse|mcrypt|memcache|memcached|mongodb|msgpack|protobuf|psr|raphf|rdkafka|redis|snmp|ssh2|swoole|uuid|vld|xdebug|xdebug2|yaml|zmq)/.test(
version_extension version_extension
): ):
case /(?<!5\.[3-6])(ds|v8js)/.test(version_extension): case /(?<!5\.[3-6])(ds|v8js)/.test(version_extension):
case /(5\.6|7\.[0-4])(propro|lua)/.test(version_extension): case /(5\.6|7\.[0-4])(propro|lua)/.test(version_extension):
case /(?<!5\.[3-6]|7\.0)pcov/.test(version_extension): case /(?<!5\.[3-6]|7\.0)pcov/.test(version_extension):
case /(?<!5\.[3-6])(ast|vips|xlswriter)/.test(version_extension): case /(?<!5\.[3-6])(ast|vips|xlswriter)/.test(version_extension):
case /^(8\.[0-5])swow$/.test(version_extension):
add_script += await utils.joins( add_script += await utils.joins(
'\nadd_brew_extension', '\nadd_brew_extension',
ext_name, ext_name,

View File

@@ -1,10 +1,9 @@
/** import {IncomingMessage, OutgoingHttpHeaders} from 'http';
* Redirect status codes set for O(1) lookup import * as https from 'https';
*/ import * as url from 'url';
const REDIRECT_CODES = new Set([301, 302, 303, 307, 308]);
/** /**
* Function to fetch a URL using native fetch API (Node 24+) * Function to fetch a URL
* *
* @param input_url * @param input_url
* @param auth_token * @param auth_token
@@ -15,28 +14,43 @@ export async function fetch(
auth_token?: string, auth_token?: string,
redirect_count = 5 redirect_count = 5
): Promise<Record<string, string>> { ): Promise<Record<string, string>> {
const headers: Record<string, string> = { const fetch_promise: Promise<Record<string, string>> = new Promise(
resolve => {
const url_object: url.UrlObject = new url.URL(input_url);
const headers: OutgoingHttpHeaders = {
'User-Agent': `Mozilla/5.0 (${process.platform} ${process.arch}) setup-php` 'User-Agent': `Mozilla/5.0 (${process.platform} ${process.arch}) setup-php`
}; };
if (auth_token) { if (auth_token) {
headers['Authorization'] = 'Bearer ' + auth_token; headers.authorization = 'Bearer ' + auth_token;
} }
const options: https.RequestOptions = {
try { hostname: url_object.hostname,
const response = await globalThis.fetch(input_url, { path: url_object.pathname,
headers, headers: headers,
redirect: redirect_count > 0 ? 'follow' : 'manual' agent: new https.Agent({keepAlive: false})
}); };
const req = https.get(options, (res: IncomingMessage) => {
if (response.ok) { if (res.statusCode === 200) {
const data = await response.text(); let body = '';
return {data}; res.setEncoding('utf8');
} else if (REDIRECT_CODES.has(response.status) && redirect_count <= 0) { res.on('data', chunk => (body += chunk));
return {error: `${response.status}: Redirect error`}; res.on('end', () => resolve({data: `${body}`}));
} else if (
[301, 302, 303, 307, 308].includes(res.statusCode as number)
) {
if (redirect_count > 0 && res.headers.location) {
fetch(res.headers.location, auth_token, redirect_count--).then(
resolve
);
} else { } else {
return {error: `${response.status}: ${response.statusText}`}; resolve({error: `${res.statusCode}: Redirect error`});
} }
} catch (error) { } else {
return {error: `Fetch error: ${(error as Error).message}`}; resolve({error: `${res.statusCode}: ${res.statusMessage}`});
} }
});
req.end();
}
);
return await fetch_promise;
} }

View File

@@ -1,8 +1,8 @@
import path from 'path'; import path from 'path';
import fs from 'fs'; import fs from 'fs';
import {exec} from '@actions/exec'; import {exec} from '@actions/exec';
import * as core from '@actions/core';
import * as config from './config'; import * as config from './config';
import * as core from './core';
import * as coverage from './coverage'; import * as coverage from './coverage';
import * as extensions from './extensions'; import * as extensions from './extensions';
import * as tools from './tools'; import * as tools from './tools';

View File

@@ -15,7 +15,7 @@ handle_dependency_extensions() {
brew_opts=(-sf) brew_opts=(-sf)
patch_abstract_file patch_abstract_file
for dependency_extension in "${dependency_extensions[@]}"; do for dependency_extension in "${dependency_extensions[@]}"; do
safe_brew install "${brew_opts[@]}" "$ext_tap/$dependency_extension@$version" && copy_brew_extensions "$dependency_extension" brew install "${brew_opts[@]}" "$ext_tap/$dependency_extension@$version" && copy_brew_extensions "$dependency_extension"
done done
fi fi
} }
@@ -83,7 +83,7 @@ add_brew_extension() {
formula="$(get_renamed_formula "$formula")" formula="$(get_renamed_formula "$formula")"
update_dependencies update_dependencies
handle_dependency_extensions "$formula" "$extension" handle_dependency_extensions "$formula" "$extension"
(safe_brew install "${brew_opts[@]}" "$ext_tap/$formula@$version" && copy_brew_extensions "$formula") || pecl_install "$extension" (brew install "${brew_opts[@]}" "$ext_tap/$formula@$version" && copy_brew_extensions "$formula") || pecl_install "$extension"
add_extension_log "$extension" "Installed and enabled" add_extension_log "$extension" "Installed and enabled"
fi fi
} }
@@ -181,14 +181,14 @@ add_php() {
fi fi
if [[ "$existing_version" != "false" && -z "$suffix" ]]; then if [[ "$existing_version" != "false" && -z "$suffix" ]]; then
if [ "$action" = "upgrade" ]; then if [ "$action" = "upgrade" ]; then
safe_brew install --only-dependencies "$php_formula" brew install --only-dependencies "$php_formula"
safe_brew upgrade -f --overwrite "$php_formula" brew upgrade -f --overwrite "$php_formula"
else else
brew unlink "$php_keg" brew unlink "$php_keg"
fi fi
else else
safe_brew install --only-dependencies "$php_formula" brew install --only-dependencies "$php_formula"
safe_brew install -f --overwrite "$php_formula" 2>/dev/null || safe_brew upgrade -f --overwrite "$php_formula" brew install -f --overwrite "$php_formula" 2>/dev/null || brew upgrade -f --overwrite "$php_formula"
fi fi
brew link --force --overwrite "$php_keg" || (sudo chown -R "$(id -un)":"$(id -gn)" "$brew_prefix" && brew link --force --overwrite "$php_keg") brew link --force --overwrite "$php_keg" || (sudo chown -R "$(id -un)":"$(id -gn)" "$brew_prefix" && brew link --force --overwrite "$php_keg")
} }

View File

@@ -1,3 +1,17 @@
add_firebird_client_darwin() {
firebird_tag='v5.0.0'
arch_name='x64'
arch="$(uname -m)"
[[ "$arch" = "arm64" || "$arch" = "aarch64" ]] && arch_name='arm64'
pkg_name=$(get -s -n "" https://api.github.com/repos/FirebirdSQL/firebird/releases/tags/"$firebird_tag" | grep -Eo "Firebird-.*.-$arch_name.pkg" | head -n 1)
[ -z "$pkg_name" ] && pkg_name=$(get -s -n "" https://github.com/FirebirdSQL/firebird/releases/expanded_assets/"$firebird_tag" | grep -Eo "Firebird-.*.-$arch_name.pkg" | head -n 1)
get -q -e "/tmp/firebird.pkg" https://github.com/FirebirdSQL/firebird/releases/download/"$firebird_tag"/"$pkg_name"
sudo installer -pkg /tmp/firebird.pkg -target /
sudo mkdir -p /opt/firebird/include /opt/firebird/lib
sudo cp -a /Library/Frameworks/Firebird.framework/Headers/* /opt/firebird/include/
sudo find /Library/Frameworks/Firebird.framework -name '*.dylib' -exec cp "{}" /opt/firebird/lib \;
}
add_firebird_helper() { add_firebird_helper() {
firebird_dir=$1 firebird_dir=$1
tag="$(php_src_tag)" tag="$(php_src_tag)"
@@ -9,19 +23,22 @@ add_firebird_helper() {
} }
add_firebird() { add_firebird() {
if [ "$(uname -s )" = "Darwin" ]; then
add_firebird_client_darwin
fi
enable_extension pdo_firebird extension enable_extension pdo_firebird extension
if check_extension pdo_firebird; then status="Enabled"
add_log "${tick:?}" pdo_firebird Enabled if ! check_extension pdo_firebird; then
else status="Installed and enabled"
if [ "$(uname -s)" = "Linux" ]; then if [ "$(uname -s)" = "Linux" ]; then
if [[ "${version:?}" =~ 5.3|${php_builder_versions:?} ]]; then if [[ "${version:?}" =~ 5.3|${nightly_versions:?} ]]; then
add_firebird_helper /usr add_firebird_helper /usr
else else
add_pdo_extension firebird add_pdo_extension firebird
fi fi
else else
add_brew_extension pdo_firebird extension add_firebird_helper /opt/firebird
fi fi
add_extension_log pdo_firebird "Installed and enabled"
fi fi
add_extension_log pdo_firebird "$status"
} }

View File

@@ -61,7 +61,7 @@ add_http_helper() {
export HTTP_CONFIGURE_OPTS="$http_configure_opts" export HTTP_CONFIGURE_OPTS="$http_configure_opts"
export HTTP_LINUX_LIBS="zlib1g libbrotli-dev libcurl4-openssl-dev libevent-dev libicu-dev libidn2-dev" export HTTP_LINUX_LIBS="zlib1g libbrotli-dev libcurl4-openssl-dev libevent-dev libicu-dev libidn2-dev"
export HTTP_DARWIN_LIBS="brotli curl icu4c libevent libidn2" export HTTP_DARWIN_LIBS="brotli curl icu4c libevent libidn2"
if ! [[ ${version:?} =~ 5.[3-6]|7.[0-4] ]]; then if [[ "${version:?}" =~ ${nightly_versions:?} ]]; then
add_extension_from_source http https://github.com m6w6 ext-http master extension add_extension_from_source http https://github.com m6w6 ext-http master extension
else else
add_extension_from_source pecl_http https://pecl.php.net http http "${ext##*-}" extension pecl add_extension_from_source pecl_http https://pecl.php.net http http "${ext##*-}" extension pecl

View File

@@ -36,7 +36,7 @@ get_openssl_suffix() {
change_library_paths() { change_library_paths() {
if [ "$os" = "Darwin" ]; then if [ "$os" = "Darwin" ]; then
otool -L "${ext_dir:?}"/relay.so | grep -q 'ssl.1' && openssl_version='1.1' || openssl_version='3' otool -L "${ext_dir:?}"/relay.so | grep -q 'ssl.1' && openssl_version='1.1' || openssl_version='3'
[ -e "${brew_prefix:?}"/opt/openssl@"$openssl_version" ] || safe_brew install openssl@"$openssl_version" [ -e "${brew_prefix:?}"/opt/openssl@"$openssl_version" ] || brew install openssl@"$openssl_version"
dylibs="$(otool -L "${ext_dir:?}"/relay.so | grep -Eo '.*\.dylib' | cut -f1 -d ' ')" dylibs="$(otool -L "${ext_dir:?}"/relay.so | grep -Eo '.*\.dylib' | cut -f1 -d ' ')"
install_name_tool -change "$(echo "${dylibs}" | grep -E "libzstd.*dylib" | xargs)" "$brew_prefix"/opt/zstd/lib/libzstd.dylib "$ext_dir"/relay.so install_name_tool -change "$(echo "${dylibs}" | grep -E "libzstd.*dylib" | xargs)" "$brew_prefix"/opt/zstd/lib/libzstd.dylib "$ext_dir"/relay.so
install_name_tool -change "$(echo "${dylibs}" | grep -E "liblz4.*dylib" | xargs)" "$brew_prefix"/opt/lz4/lib/liblz4.dylib "$ext_dir"/relay.so install_name_tool -change "$(echo "${dylibs}" | grep -E "liblz4.*dylib" | xargs)" "$brew_prefix"/opt/lz4/lib/liblz4.dylib "$ext_dir"/relay.so
@@ -54,7 +54,7 @@ add_relay_dependencies() {
if [ "$os" = "Darwin" ]; then if [ "$os" = "Darwin" ]; then
. "${0%/*}"/tools/brew.sh . "${0%/*}"/tools/brew.sh
configure_brew configure_brew
safe_brew install lz4 hiredis zstd concurrencykit brew install lz4 hiredis zstd concurrencykit
fi fi
} }

View File

@@ -60,7 +60,7 @@ add_linux_libs() {
add_darwin_libs() { add_darwin_libs() {
local lib=$1 local lib=$1
if ! check_lib "$lib"; then if ! check_lib "$lib"; then
safe_brew install "$lib" || true brew install "$lib" || true
if [[ "$lib" = *@* ]]; then if [[ "$lib" = *@* ]]; then
brew link --overwrite --force "$lib" || true brew link --overwrite --force "$lib" || true
fi fi

View File

@@ -187,7 +187,7 @@ update_php() {
# Function to install PHP. # Function to install PHP.
add_php() { add_php() {
if [ "${runner:?}" = "self-hosted" ] || [ "${use_package_cache:-true}" = "false" ]; then if [ "${runner:?}" = "self-hosted" ] || [ "${use_package_cache:-true}" = "false" ]; then
if [[ "$version" =~ ${php_builder_versions:?} || "$ts" = "zts" ]]; then if [[ "$version" =~ ${nightly_versions:?} || "$ts" = "zts" ]]; then
setup_php_builder setup_php_builder
else else
add_packaged_php add_packaged_php

View File

@@ -158,7 +158,12 @@ Function Add-ToolsHelper() {
} elseif($tool -eq "cs2pr") { } elseif($tool -eq "cs2pr") {
(Get-Content $bin_dir/cs2pr).replace('exit(9)', 'exit(0)') | Set-Content $bin_dir/cs2pr (Get-Content $bin_dir/cs2pr).replace('exit(9)', 'exit(0)') | Set-Content $bin_dir/cs2pr
} elseif($tool -eq "deployer") { } elseif($tool -eq "deployer") {
Copy-Item $bin_dir\deployer.bat -Destination $bin_dir\dep.bat if(Test-Path $composer_bin\deployer.phar.bat) {
Copy-Item $composer_bin\deployer.phar.bat -Destination $composer_bin\dep.bat
}
if(Test-Path $composer_bin\dep.bat) {
Copy-Item $composer_bin\dep.bat -Destination $composer_bin\deployer.bat
}
} elseif($tool -eq "phan") { } elseif($tool -eq "phan") {
$extensions += @('fileinfo', 'ast') $extensions += @('fileinfo', 'ast')
} elseif($tool -eq "phinx") { } elseif($tool -eq "phinx") {
@@ -200,21 +205,14 @@ Function Add-Tool() {
[Parameter(Position = 2, Mandatory = $false)] [Parameter(Position = 2, Mandatory = $false)]
$ver_param $ver_param
) )
$urls = $urls -split ',' if (Test-Path $bin_dir\$tool) {
Copy-Item $bin_dir\$tool -Destination $bin_dir\$tool.old -Force
}
$tool_path = "$bin_dir\$tool" $tool_path = "$bin_dir\$tool"
$is_exe = ((($urls[0] | Split-Path -Extension).ToLowerInvariant()) -eq '.exe')
if ($is_exe) { $tool_path = "$tool_path.exe" }
$tool_ext = if ($is_exe) { '.exe' } else { '' }
$url_stream = [System.IO.MemoryStream]::New([System.Text.Encoding]::UTF8.GetBytes($urls[0]))
$cache_key = (Get-FileHash -InputStream $url_stream -Algorithm SHA256).Hash.Substring(0, 16)
$cache_path = "$env:TEMP\$tool-$cache_key$tool_ext"
$status_code = 200
if (Test-Path $cache_path -PathType Leaf) {
Copy-Item $cache_path -Destination $tool_path -Force
} else {
$backup_path = "$tool_path.bak"
if (Test-Path $tool_path) { Copy-Item $tool_path -Destination $backup_path -Force }
foreach ($url in $urls){ foreach ($url in $urls){
if (($url | Split-Path -Extension) -eq ".exe") {
$tool_path = "$tool_path.exe"
}
try { try {
$status_code = (Invoke-WebRequest -Passthru -Uri $url -OutFile $tool_path).StatusCode $status_code = (Invoke-WebRequest -Passthru -Uri $url -OutFile $tool_path).StatusCode
} catch { } catch {
@@ -222,26 +220,15 @@ Function Add-Tool() {
try { try {
$url = $url.replace("releases/latest/download", "releases/download/" + ([regex]::match((Get-File -Url ($url.split('/release')[0] + "/releases")).Content, "([0-9]+\.[0-9]+\.[0-9]+)/" + ($url.Substring($url.LastIndexOf("/") + 1))).Groups[0].Value).split('/')[0]) $url = $url.replace("releases/latest/download", "releases/download/" + ([regex]::match((Get-File -Url ($url.split('/release')[0] + "/releases")).Content, "([0-9]+\.[0-9]+\.[0-9]+)/" + ($url.Substring($url.LastIndexOf("/") + 1))).Groups[0].Value).split('/')[0])
$status_code = (Invoke-WebRequest -Passthru -Uri $url -OutFile $tool_path).StatusCode $status_code = (Invoke-WebRequest -Passthru -Uri $url -OutFile $tool_path).StatusCode
} catch { } catch { }
$status_code = 0
}
} else {
$status_code = 0
} }
} }
if($status_code -eq 200 -and (Test-Path $tool_path)) { if($status_code -eq 200 -and (Test-Path $tool_path)) {
Copy-Item $tool_path -Destination $cache_path -Force
break break
} }
} }
if ($status_code -ne 200 -and (Test-Path $backup_path)) {
Copy-Item $backup_path -Destination $tool_path -Force
}
Remove-Item $backup_path -Force -ErrorAction SilentlyContinue
}
$escaped_tool = [regex]::Escape($tool) if (((Get-ChildItem -Path $bin_dir/* | Where-Object Name -Match "^$tool(.exe|.phar)*$").Count -gt 0)) {
if (((Get-ChildItem -Path $bin_dir/* | Where-Object Name -Match "^$escaped_tool(\.exe|\.phar)?$").Count -gt 0)) {
$bat_content = @() $bat_content = @()
$bat_content += "@ECHO off" $bat_content += "@ECHO off"
$bat_content += "setlocal DISABLEDELAYEDEXPANSION" $bat_content += "setlocal DISABLEDELAYEDEXPANSION"
@@ -255,6 +242,8 @@ Function Add-Tool() {
} else { } else {
if($tool -eq "composer") { if($tool -eq "composer") {
$env:fail_fast = 'true' $env:fail_fast = 'true'
} elseif (Test-Path $bin_dir\$tool.old) {
Copy-Item $bin_dir\$tool.old -Destination $bin_dir\$tool -Force
} }
Add-Log $cross $tool "Could not add $tool" Add-Log $cross $tool "Could not add $tool"
} }

View File

@@ -123,15 +123,19 @@ add_tools_helper() {
extensions+=(iconv mbstring phar sodium) extensions+=(iconv mbstring phar sodium)
elif [ "$tool" = "codeception" ]; then elif [ "$tool" = "codeception" ]; then
extensions+=(json mbstring) extensions+=(json mbstring)
sudo ln -s "$scoped_dir"/vendor/bin/codecept "$scoped_dir"/vendor/bin/codeception 2>/dev/null || true sudo ln -s "$scoped_dir"/vendor/bin/codecept "$scoped_dir"/vendor/bin/codeception
elif [ "$tool" = "composer" ]; then elif [ "$tool" = "composer" ]; then
configure_composer "$tool_path" configure_composer "$tool_path"
elif [ "$tool" = "cs2pr" ]; then elif [ "$tool" = "cs2pr" ]; then
sudo sed -i 's/\r$//; s/exit(9)/exit(0)/' "$tool_path" 2>/dev/null || sudo sed -i 's/\r$//; s/exit(9)/exit(0)/' "$tool_path" 2>/dev/null ||
sudo sed -i '' 's/\r$//; s/exit(9)/exit(0)/' "$tool_path" sudo sed -i '' 's/\r$//; s/exit(9)/exit(0)/' "$tool_path"
elif [ "$tool" = "deployer" ]; then elif [ "$tool" = "deployer" ]; then
sudo ln -s "$tool_path" "$tool_path_dir"/deployer 2>/dev/null || true if [ -e "$composer_bin"/deployer.phar ]; then
sudo ln -s "$tool_path" "$tool_path_dir"/dep 2>/dev/null || true sudo ln -s "$composer_bin"/deployer.phar "$composer_bin"/dep
fi
if [ -e "$composer_bin"/dep ]; then
sudo ln -s "$composer_bin"/dep "$composer_bin"/deployer
fi
elif [ "$tool" = "phan" ]; then elif [ "$tool" = "phan" ]; then
extensions+=(fileinfo ast) extensions+=(fileinfo ast)
elif [ "$tool" = "phinx" ]; then elif [ "$tool" = "phinx" ]; then
@@ -147,7 +151,7 @@ add_tools_helper() {
elif [ "$tool" = "phpDocumentor" ]; then elif [ "$tool" = "phpDocumentor" ]; then
extensions+=(ctype hash json fileinfo iconv mbstring simplexml xml) extensions+=(ctype hash json fileinfo iconv mbstring simplexml xml)
sudo ln -s "$tool_path" "$tool_path_dir"/phpdocumentor 2>/dev/null || true sudo ln -s "$tool_path" "$tool_path_dir"/phpdocumentor 2>/dev/null || true
sudo ln -s "$tool_path" "$tool_path_dir"/phpdoc 2>/dev/null || true sudo ln -s "$tool_path" "$tool_path_dir"/phpdoc
elif [ "$tool" = "phpunit" ]; then elif [ "$tool" = "phpunit" ]; then
extensions+=(dom json libxml mbstring xml xmlwriter) extensions+=(dom json libxml mbstring xml xmlwriter)
elif [ "$tool" = "phpunit-bridge" ]; then elif [ "$tool" = "phpunit-bridge" ]; then
@@ -158,9 +162,9 @@ add_tools_helper() {
fi fi
elif [ "$tool" = "vapor-cli" ]; then elif [ "$tool" = "vapor-cli" ]; then
extensions+=(fileinfo json mbstring zip simplexml) extensions+=(fileinfo json mbstring zip simplexml)
sudo ln -s "$scoped_dir"/vendor/bin/vapor "$scoped_dir"/vendor/bin/vapor-cli 2>/dev/null || true sudo ln -s "$scoped_dir"/vendor/bin/vapor "$scoped_dir"/vendor/bin/vapor-cli
elif [ "$tool" = wp-cli ]; then elif [ "$tool" = wp-cli ]; then
sudo ln -s "$tool_path" "$tool_path_dir"/"${tool%-*}" 2>/dev/null || true sudo ln -s "$tool_path" "$tool_path_dir"/"${tool%-*}"
fi fi
for extension in "${extensions[@]}"; do for extension in "${extensions[@]}"; do
add_extension "$extension" extension add_extension "$extension" extension
@@ -176,39 +180,25 @@ add_tool() {
if ! [ -d "$tool_path_dir" ]; then if ! [ -d "$tool_path_dir" ]; then
sudo mkdir -p "$tool_path_dir" sudo mkdir -p "$tool_path_dir"
fi fi
if ! [ -d "$tool_cache_path_dir" ]; then add_path "$tool_path_dir"
sudo mkdir -p "$tool_cache_path_dir" if [ -e "$tool_path" ]; then
sudo cp -aL "$tool_path" /tmp/"$tool"
fi fi
add_path "$tool_path_dir" verify
add_path "$tool_cache_path_dir"
IFS="," read -r -a url <<<"$url" IFS="," read -r -a url <<<"$url"
cache_key=$(get_sha256 "${url[0]}" | head -c 16)
cache_path="$tool_cache_path_dir/${tool}-${cache_key}"
status_code="200"
if [ -f "$cache_path" ]; then
sudo cp -a "$cache_path" "$tool_path"
else
[ -f "$tool_path" ] && sudo cp -a "$tool_path" "$tool_path.bak"
status_code=$(get -v -e "$tool_path" "${url[@]}") status_code=$(get -v -e "$tool_path" "${url[@]}")
if [ "$status_code" != "200" ] && [[ "${url[0]}" =~ .*github.com.*releases.*latest.* ]]; then if [ "$status_code" != "200" ] && [[ "${url[0]}" =~ .*github.com.*releases.*latest.* ]]; then
url[0]="${url[0]//releases\/latest\/download/releases/download/$(get -s -n "" "$(echo "${url[0]}" | cut -d '/' -f '1-5')/releases" | grep -Eo -m 1 "([0-9]+\.[0-9]+\.[0-9]+)/$(echo "${url[0]}" | sed -e "s/.*\///")" | cut -d '/' -f 1)}" url[0]="${url[0]//releases\/latest\/download/releases/download/$(get -s -n "" "$(echo "${url[0]}" | cut -d '/' -f '1-5')/releases" | grep -Eo -m 1 "([0-9]+\.[0-9]+\.[0-9]+)/$(echo "${url[0]}" | sed -e "s/.*\///")" | cut -d '/' -f 1)}"
status_code=$(get -v -e "$tool_path" "${url[0]}") status_code=$(get -v -e "$tool_path" "${url[0]}")
fi fi
if [ "$status_code" = "200" ]; then
sudo cp -a "$tool_path" "$cache_path"
elif [ -f "$tool_path.bak" ]; then
sudo mv "$tool_path.bak" "$tool_path"
fi
sudo rm -f "$tool_path.bak"
fi
if [ "$status_code" = "200" ]; then if [ "$status_code" = "200" ]; then
add_tools_helper "$tool" add_tools_helper "$tool"
[ -L "$tool_cache_path_dir/$tool" ] || sudo ln -s "$tool_path" "$tool_cache_path_dir/$tool" 2>/dev/null || true
tool_version=$(get_tool_version "$tool" "$ver_param") tool_version=$(get_tool_version "$tool" "$ver_param")
add_log "${tick:?}" "$tool" "Added $tool $tool_version" add_log "${tick:?}" "$tool" "Added $tool $tool_version"
else else
if [ "$tool" = "composer" ]; then if [ "$tool" = "composer" ]; then
export fail_fast=true export fail_fast=true
elif [ -e /tmp/"$tool" ]; then
sudo cp -a /tmp/"$tool" "$tool_path"
fi fi
if [ "$status_code" = "404" ]; then if [ "$status_code" = "404" ]; then
add_log "$cross" "$tool" "Failed to download $tool from ${url[*]}" add_log "$cross" "$tool" "Failed to download $tool from ${url[*]}"

View File

@@ -8,7 +8,7 @@ add_blackfire_linux() {
add_blackfire_darwin() { add_blackfire_darwin() {
sudo mkdir -p /usr/local/var/run sudo mkdir -p /usr/local/var/run
add_brew_tap blackfireio/homebrew-blackfire add_brew_tap blackfireio/homebrew-blackfire
safe_brew install blackfire brew install blackfire
} }
blackfire_config() { blackfire_config() {

View File

@@ -44,118 +44,6 @@ add_brew_bins_to_path() {
add_path "$brew_prefix"/sbin add_path "$brew_prefix"/sbin
} }
# Function to get file modification time.
get_file_mtime() {
local file=$1
if [ "$(uname -s)" = "Darwin" ]; then
stat -f "%m" "$file" 2>/dev/null || echo 0
else
stat -c "%Y" "$file" 2>/dev/null || echo 0
fi
}
# Function to terminate a process and its direct children.
terminate_process_tree() {
local pid=$1
local children child
children=$(pgrep -P "$pid" 2>/dev/null || true)
kill -TERM "$pid" || true
for child in $children; do
terminate_process_tree "$child"
done
sleep 2
kill -KILL "$pid" || true
for child in $children; do
terminate_process_tree "$child"
done
}
# Function to run a command with an inactivity watchdog.
run_with_inactivity_watchdog() {
local timeout_secs="${SETUP_PHP_BREW_INACTIVITY_TIMEOUT:-180}"
local poll_secs="${SETUP_PHP_BREW_WATCHDOG_POLL:-5}"
local tmp_dir fifo log_file timeout_file command_pid reader_pid monitor_pid exit_code
tmp_dir="$(mktemp -d "${TMPDIR:-/tmp}/setup-php-brew.XXXXXX")" || return 1
fifo="$tmp_dir/output.fifo"
log_file="$tmp_dir/output.log"
timeout_file="$tmp_dir/timed_out"
mkfifo "$fifo" || {
rm -rf "$tmp_dir"
return 1
}
: >"$log_file"
("$@" >"$fifo" 2>&1) &
command_pid=$!
(
while IFS= read -r line || [ -n "$line" ]; do
printf '%s\n' "$line"
printf '%s\n' "$line" >>"$log_file"
done <"$fifo"
) &
reader_pid=$!
(
local last_activity current_activity now
last_activity=$(get_file_mtime "$log_file")
while kill -0 "$command_pid" ; do
sleep "$poll_secs"
current_activity=$(get_file_mtime "$log_file")
[ "$current_activity" -gt "$last_activity" ] && last_activity="$current_activity"
now=$(date +%s)
if [ $((now - last_activity)) -ge "$timeout_secs" ]; then
printf "\nsetup-php: brew produced no output for %ss; terminating and retrying...\n" "$timeout_secs" >&2
: >"$timeout_file"
terminate_process_tree "$command_pid"
break
fi
done
) &
monitor_pid=$!
wait "$command_pid"
exit_code=$?
wait "$reader_pid" 2>/dev/null || true
kill "$monitor_pid" || true
wait "$monitor_pid" 2>/dev/null || true
if [ -e "$timeout_file" ]; then
rm -rf "$tmp_dir"
return 124
fi
rm -rf "$tmp_dir"
return "$exit_code"
}
# Function to run brew with retries and an inactivity watchdog.
safe_brew() {
local max_attempts="${SETUP_PHP_BREW_RETRY_ATTEMPTS:-3}"
local attempt=1
local exit_code=0
if [ "${SETUP_PHP_BREW_WATCHDOG:-true}" = "false" ]; then
brew "$@"
return $?
fi
while [ "$attempt" -le "$max_attempts" ]; do
run_with_inactivity_watchdog brew "$@" && return 0
exit_code=$?
if [ "$attempt" -ge "$max_attempts" ]; then
return "$exit_code"
fi
printf "setup-php: retrying brew command (attempt %s/%s, exit %s)\n" "$((attempt + 1))" "$max_attempts" "$exit_code" >&2
sleep "$((attempt * 5))"
attempt=$((attempt + 1))
done
return "$exit_code"
}
# Function to add brew. # Function to add brew.
add_brew() { add_brew() {
brew_prefix="$(get_brew_prefix)" brew_prefix="$(get_brew_prefix)"
@@ -186,7 +74,6 @@ configure_brew() {
export HOMEBREW_NO_ENV_HINTS=1 export HOMEBREW_NO_ENV_HINTS=1
export HOMEBREW_NO_INSTALL_CLEANUP=1 export HOMEBREW_NO_INSTALL_CLEANUP=1
export HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=1 export HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=1
export HOMEBREW_DOWNLOAD_CONCURRENCY="${HOMEBREW_DOWNLOAD_CONCURRENCY:-6}"
export brew_opts export brew_opts
export brew_path export brew_path
export brew_path_dir export brew_path_dir

View File

@@ -4,7 +4,7 @@ add_bazel() {
add_list bazel/apt https://storage.googleapis.com/bazel-apt https://bazel.build/bazel-release.pub.gpg stable jdk1.8 add_list bazel/apt https://storage.googleapis.com/bazel-apt https://bazel.build/bazel-release.pub.gpg stable jdk1.8
install_packages bazel install_packages bazel
else else
safe_brew install bazel brew install bazel
fi fi
fi fi
} }
@@ -25,7 +25,7 @@ add_grpc_php_plugin_brew() {
. "${0%/*}"/tools/brew.sh . "${0%/*}"/tools/brew.sh
configure_brew configure_brew
[ -e /usr/local/bin/protoc ] && sudo mv /usr/local/bin/protoc /tmp/protoc && sudo mv /usr/local/include/google /tmp [ -e /usr/local/bin/protoc ] && sudo mv /usr/local/bin/protoc /tmp/protoc && sudo mv /usr/local/include/google /tmp
safe_brew install grpc brew install grpc
brew link --force --overwrite grpc brew link --force --overwrite grpc
[ -e /tmp/protoc ] && sudo mv /tmp/protoc /usr/local/bin/protoc && sudo mv /tmp/google /usr/local/include/ [ -e /tmp/protoc ] && sudo mv /tmp/protoc /usr/local/bin/protoc && sudo mv /tmp/google /usr/local/include/
grpc_tag="v$(brew info grpc | grep "grpc:" | grep -Eo "[0-9]+\.[0-9]+\.[0-9]+")" grpc_tag="v$(brew info grpc | grep "grpc:" | grep -Eo "[0-9]+\.[0-9]+\.[0-9]+")"

View File

@@ -4,7 +4,7 @@ export cross="✗"
export curl_opts=(-sL) export curl_opts=(-sL)
export old_versions="5.[3-5]" export old_versions="5.[3-5]"
export jit_versions="8.[0-9]" export jit_versions="8.[0-9]"
export php_builder_versions="8.[3-9]" export nightly_versions="8.[3-9]"
export xdebug3_versions="7.[2-4]|8.[0-9]" export xdebug3_versions="7.[2-4]|8.[0-9]"
export latest="releases/latest/download" export latest="releases/latest/download"
export github="https://github.com/shivammathur" export github="https://github.com/shivammathur"
@@ -58,7 +58,6 @@ read_env() {
-n "$ACT" || -n "$CONTAINER" ]] && _runner=self-hosted || _runner=github -n "$ACT" || -n "$CONTAINER" ]] && _runner=self-hosted || _runner=github
runner="${runner:-${RUNNER:-$_runner}}" runner="${runner:-${RUNNER:-$_runner}}"
tool_path_dir="${setup_php_tools_dir:-${SETUP_PHP_TOOLS_DIR:-/usr/local/bin}}" tool_path_dir="${setup_php_tools_dir:-${SETUP_PHP_TOOLS_DIR:-/usr/local/bin}}"
tool_cache_path_dir="${setup_php_tool_cache_dir:-${SETUP_PHP_TOOL_CACHE_DIR:-${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}/setup-php/tools}}"
if [[ "$runner" = "github" && $_runner = "self-hosted" ]]; then if [[ "$runner" = "github" && $_runner = "self-hosted" ]]; then
fail_fast=true fail_fast=true
@@ -80,7 +79,6 @@ read_env() {
export update export update
export ts export ts
export tool_path_dir export tool_path_dir
export tool_cache_path_dir
} }
# Function to create a lock. # Function to create a lock.
@@ -171,15 +169,14 @@ get_shell_profile() {
# Function to add a path to the PATH variable. # Function to add a path to the PATH variable.
add_path() { add_path() {
path_to_add=$1 path_to_add=$1
action=$2 [[ ":$PATH:" == *":$path_to_add:"* ]] && return
[[ "$action" == "verify" && ":$PATH:" == *":$path_to_add:"* ]] && return
if [[ -n "$GITHUB_PATH" ]]; then if [[ -n "$GITHUB_PATH" ]]; then
printf '%s\n%s' "$path_to_add" "$(grep -v "^${path_to_add}$" "$GITHUB_PATH" 2>/dev/null)" > "$GITHUB_PATH" echo "$path_to_add" | tee -a "$GITHUB_PATH"
else else
profile=$(get_shell_profile) profile=$(get_shell_profile)
([ -e "$profile" ] && grep -q ":$path_to_add\"" "$profile" 2>/dev/null) || echo "export PATH=\"\${PATH:+\${PATH}:}\"$path_to_add" | sudo tee -a "$profile" ([ -e "$profile" ] && grep -q ":$path_to_add\"" "$profile" 2>/dev/null) || echo "export PATH=\"\${PATH:+\${PATH}:}\"$path_to_add" | sudo tee -a "$profile"
fi fi
[[ ":$PATH:" == *":$path_to_add:"* ]] || export PATH="${PATH:+${PATH}:}$path_to_add" export PATH="${PATH:+${PATH}:}$path_to_add"
} }
# Function to add environment variables using a PATH. # Function to add environment variables using a PATH.

View File

@@ -81,10 +81,9 @@ Function Get-PathFromRegistry {
# Function to add a location to PATH. # Function to add a location to PATH.
Function Add-Path { Function Add-Path {
param( param(
[string]$PathItem, [string]$PathItem
[switch]$Force
) )
if(-not($Force) -and "$env:PATH;".contains("$PathItem;")) { if("$env:PATH;".contains("$PathItem;")) {
return return
} }
if ($env:GITHUB_PATH) { if ($env:GITHUB_PATH) {
@@ -376,7 +375,6 @@ if(-not($env:ImageOS) -and -not($env:ImageVersion)) {
if(-not(Test-Path -LiteralPath $current_profile)) { if(-not(Test-Path -LiteralPath $current_profile)) {
New-Item -Path $current_profile -ItemType "file" -Force New-Item -Path $current_profile -ItemType "file" -Force
} }
Add-Path -PathItem $bin_dir -Force
} }
$src = Join-Path -Path $PSScriptRoot -ChildPath \.. $src = Join-Path -Path $PSScriptRoot -ChildPath \..

View File

@@ -5,94 +5,14 @@ import * as fetch from './fetch';
import * as packagist from './packagist'; import * as packagist from './packagist';
import * as utils from './utils'; import * as utils from './utils';
/** type RS = Record<string, string>;
* Valid function names for custom tool handlers type RSRS = Record<string, RS>;
*/
type ToolFunction =
| 'castor'
| 'composer'
| 'deployer'
| 'dev_tools'
| 'phive'
| 'blackfire_player'
| 'pecl'
| 'phing'
| 'phpunit'
| 'phpcpd'
| 'wp_cli';
/** interface IRef {
* Tool data interface containing all properties for tool installation
*/
export interface ToolData {
tool: string;
version: string;
os: string;
php_version: string;
github: string;
domain: string;
extension: string;
repository: string;
prefix: string;
verb: string;
fetch_latest: 'true' | 'false';
scope: string;
version_parameter: string;
version_prefix: string;
release: string;
packagist: string;
type?: string;
function?: ToolFunction;
alias?: string;
url: string;
uri?: string;
error?: string;
}
/**
* Input type for functions that may receive partial/unresolved tool data
* Used by getUrl, getLatestVersion etc. before version is fully resolved
*/
export type ToolInput = Omit<ToolData, 'version' | 'url'> & {version?: string};
/**
* Partial tool data from tools.json configuration
*/
interface ToolConfig {
tool?: string;
repository?: string;
type?: string;
function?: ToolFunction;
alias?: string;
domain?: string;
extension?: string;
fetch_latest?: 'true' | 'false';
scope?: string;
version_parameter?: string;
version_prefix?: string;
packagist?: string;
}
/**
* GitHub reference object from API response
*/
interface GitHubRef {
ref: string; ref: string;
node_id: string; node_id: string;
url: string; url: string;
object: { object: RS;
sha: string;
type: string;
url: string;
};
}
/**
* Deployer manifest entry
*/
interface DeployerManifestEntry {
version: string;
url: string;
} }
/** /**
@@ -100,7 +20,7 @@ interface DeployerManifestEntry {
* *
* @param data * @param data
*/ */
export async function getSemverVersion(data: ToolData): Promise<string> { export async function getSemverVersion(data: RS): Promise<string> {
const fixSemver = (t: string): string => { const fixSemver = (t: string): string => {
if (/^\d+\.\d+\.\d+(-|$)/.test(t)) return t; if (/^\d+\.\d+\.\d+(-|$)/.test(t)) return t;
const m = t.match(/^(\d+\.\d+\.\d+)([A-Za-z]+[0-9A-Za-z.]+)$/); const m = t.match(/^(\d+\.\d+\.\d+)([A-Za-z]+[0-9A-Za-z.]+)$/);
@@ -111,16 +31,14 @@ export async function getSemverVersion(data: ToolData): Promise<string> {
const github_token: string = const github_token: string =
(await utils.readEnv('GITHUB_TOKEN')) || (await utils.readEnv('GITHUB_TOKEN')) ||
(await utils.readEnv('COMPOSER_TOKEN')); (await utils.readEnv('COMPOSER_TOKEN'));
const response = await fetch.fetch(url, github_token); const response: RS = await fetch.fetch(url, github_token);
if (response.error || response.data === '[]') { if (response.error || response.data === '[]') {
data.error = response.error ?? `No version found with prefix ${search}.`; data['error'] = response.error ?? `No version found with prefix ${search}.`;
return data.version; return data['version'];
} else { } else {
const refs: GitHubRef[] = JSON.parse(response.data); const refs: IRef[] = JSON.parse(response['data']);
const tags = refs const tags = refs
.map((i: GitHubRef) => .map((i: IRef) => (i.ref?.split('/').pop() ?? '').replace(/^v(?=\d)/, ''))
(i.ref?.split('/').pop() ?? '').replace(/^v(?=\d)/, '')
)
.filter((t: string) => t.length > 0); .filter((t: string) => t.length > 0);
const fixedToOriginal = new Map<string, string>(); const fixedToOriginal = new Map<string, string>();
const fixed = tags.map(t => { const fixed = tags.map(t => {
@@ -128,14 +46,14 @@ export async function getSemverVersion(data: ToolData): Promise<string> {
fixedToOriginal.set(f, t); fixedToOriginal.set(f, t);
return f; return f;
}); });
const sorted = fixed.toSorted((a, b) => { fixed.sort((a, b) => {
try { try {
return cv.compareVersions(b, a); return cv.compareVersions(b, a);
} catch { } catch {
return b.localeCompare(a, 'en', {numeric: true, sensitivity: 'base'}); return b.localeCompare(a, 'en', {numeric: true, sensitivity: 'base'});
} }
}); });
return fixedToOriginal.get(sorted[0]) ?? sorted[0]; return fixedToOriginal.get(fixed[0]) ?? fixed[0];
} }
} }
@@ -144,25 +62,25 @@ export async function getSemverVersion(data: ToolData): Promise<string> {
* *
* @param data * @param data
*/ */
export async function getLatestVersion(data: ToolInput): Promise<string> { export async function getLatestVersion(data: RS): Promise<string> {
if (!data.version && data.fetch_latest === 'false') { if (!data['version'] && data['fetch_latest'] === 'false') {
return 'latest'; return 'latest';
} }
if (data.fetch_latest === 'true' && !data.repository) { const resp: Record<string, string> = await fetch.fetch(
return 'latest'; `${data['github']}/${data['repository']}/releases.atom`
}
const resp = await fetch.fetch(
`${data.github}/${data.repository}/releases.atom`
); );
if (resp.data) { if (resp['data']) {
const releases: string[] = [ const releases: string[] = [
...resp.data.matchAll(/releases\/tag\/([a-zA-Z]*)?(\d+\.\d+\.\d+)"/g) ...resp['data'].matchAll(/releases\/tag\/([a-zA-Z]*)?(\d+.\d+.\d+)"/g)
].map(match => match[2]); ].map(match => match[2]);
const sorted = releases.toSorted((a: string, b: string) => return (
releases
.sort((a: string, b: string) =>
a.localeCompare(b, undefined, {numeric: true}) a.localeCompare(b, undefined, {numeric: true})
)
.pop() || 'latest'
); );
return sorted.at(-1) || 'latest';
} }
return 'latest'; return 'latest';
} }
@@ -173,29 +91,26 @@ export async function getLatestVersion(data: ToolInput): Promise<string> {
* @param version * @param version
* @param data * @param data
*/ */
export async function getVersion( export async function getVersion(version: string, data: RS): Promise<string> {
version: string,
data: ToolData
): Promise<string> {
// semver_regex - https://semver.org/ // semver_regex - https://semver.org/
const semver_regex = const semver_regex =
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/; /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
const composer_regex = /^composer:(stable|preview|snapshot|[12])$/; const composer_regex = /^composer:(stable|preview|snapshot|[1|2])$/;
const constraint_regex = /[><=^~]+.*/; const constraint_regex = /[><=^~]+.*/;
const major_minor_regex = /^\d+(\.\d+)?$/; const major_minor_regex = /^\d+(\.\d+)?$/;
data.version = version.replace(/v?(\d)/, '$1').replace(/\.x/, ''); data['version'] = version.replace(/v?(\d)/, '$1').replace(/\.x/, '');
switch (true) { switch (true) {
case composer_regex.test(data.release): case composer_regex.test(data['release']):
case semver_regex.test(data.version): case semver_regex.test(data['version']):
case constraint_regex.test(data.version) && data.type === 'composer': case constraint_regex.test(data['version']) && data['type'] === 'composer':
return data.version; return data['version'];
case major_minor_regex.test(data.version) && data.type === 'composer': case major_minor_regex.test(data['version']) && data['type'] === 'composer':
data.release = `${data.tool}:${data.version}.*`; data['release'] = `${data['tool']}:${data['version']}.*`;
return `${data.version}.*`; return `${data['version']}.*`;
case !!data.repository && major_minor_regex.test(data.version): case data['repository'] && major_minor_regex.test(data['version']):
return await getSemverVersion(data); return await getSemverVersion(data);
default: default:
return data.version.replace(/[><=^~]*/, ''); return data['version'].replace(/[><=^~]*/, '');
} }
} }
@@ -205,14 +120,11 @@ export async function getVersion(
* @param release * @param release
* @param data * @param data
*/ */
export async function getRelease( export async function getRelease(release: string, data: RS): Promise<string> {
release: string,
data: ToolData
): Promise<string> {
release = release.includes('/') ? release.split('/')[1] : release; release = release.includes('/') ? release.split('/')[1] : release;
return release.includes(':') return release.includes(':')
? [data.tool, release.split(':')[1]].join(':') ? [data['tool'], release.split(':')[1]].join(':')
: data.tool; : data['tool'];
} }
/** /**
@@ -231,7 +143,7 @@ export async function filterList(tools_list: string[]): Promise<string[]> {
case matches[0] == undefined: case matches[0] == undefined:
break; break;
default: default:
composer = matches.at(-1)!.replace(/v(\d\S*)/, '$1'); composer = matches[matches.length - 1].replace(/v(\d\S*)/, '$1');
break; break;
} }
tools_list.unshift(composer); tools_list.unshift(composer);
@@ -243,26 +155,26 @@ export async function filterList(tools_list: string[]): Promise<string[]> {
* *
* @param data * @param data
*/ */
export async function getUrl(data: ToolInput): Promise<string> { export async function getUrl(data: RS): Promise<string> {
if ((data.version ?? 'latest') === 'latest') { if ((data['version'] ?? 'latest') === 'latest') {
return [ return [
data.domain, data['domain'],
data.repository, data['repository'],
data.prefix, data['prefix'],
data.version, data['version'],
data.verb, data['verb'],
data.tool + data.extension data['tool'] + data['extension']
] ]
.filter(Boolean) .filter(Boolean)
.join('/'); .join('/');
} else { } else {
return [ return [
data.domain, data['domain'],
data.repository, data['repository'],
data.prefix, data['prefix'],
data.verb, data['verb'],
data.version_prefix + data.version, data['version_prefix'] + data['version'],
data.tool + data.extension data['tool'] + data['extension']
] ]
.filter(Boolean) .filter(Boolean)
.join('/'); .join('/');
@@ -274,17 +186,17 @@ export async function getUrl(data: ToolInput): Promise<string> {
* *
* @param data * @param data
*/ */
export async function getPharUrl(data: ToolData): Promise<string> { export async function getPharUrl(data: RS): Promise<string> {
if (data.version === 'latest') { if (data['version'] === 'latest') {
return data.domain + '/' + data.tool + '.phar'; return data['domain'] + '/' + data['tool'] + '.phar';
} else { } else {
return ( return (
data.domain + data['domain'] +
'/' + '/' +
data.tool + data['tool'] +
'-' + '-' +
data.version_prefix + data['version_prefix'] +
data.version + data['version'] +
'.phar' '.phar'
); );
} }
@@ -295,10 +207,10 @@ export async function getPharUrl(data: ToolData): Promise<string> {
* *
* @param data * @param data
*/ */
export async function addArchive(data: ToolData): Promise<string> { export async function addArchive(data: RS): Promise<string> {
return ( return (
(await utils.getCommand(data.os, 'tool')) + (await utils.getCommand(data['os'], 'tool')) +
(await utils.joins(data.url, data.tool, data.version_parameter)) (await utils.joins(data['url'], data['tool'], data['version_parameter']))
); );
} }
@@ -307,14 +219,14 @@ export async function addArchive(data: ToolData): Promise<string> {
* *
* @param data * @param data
*/ */
export async function addPackage(data: ToolData): Promise<string> { export async function addPackage(data: RS): Promise<string> {
const command = await utils.getCommand(data.os, 'composer_tool'); const command = await utils.getCommand(data['os'], 'composer_tool');
const parts: string[] = data.repository.split('/'); const parts: string[] = data['repository'].split('/');
const args: string = await utils.joins( const args: string = await utils.joins(
parts[1], parts[1],
data.release, data['release'],
parts[0] + '/', parts[0] + '/',
data.scope data['scope']
); );
return command + args; return command + args;
} }
@@ -324,24 +236,24 @@ export async function addPackage(data: ToolData): Promise<string> {
* *
* @param data * @param data
*/ */
export async function addBlackfirePlayer(data: ToolData): Promise<string> { export async function addBlackfirePlayer(data: RS): Promise<string> {
switch (data.os) { switch (data['os']) {
case 'win32': case 'win32':
return await utils.addLog( return await utils.addLog(
'$cross', '$cross',
data.tool, data['tool'],
data.tool + ' is not a windows tool', data['tool'] + ' is not a windows tool',
'win32' 'win32'
); );
default: default:
if (data.version == 'latest') { if (data['version'] == 'latest') {
if (/5\.[5-6]|7\.0/.test(data.php_version)) { if (/5\.[5-6]|7\.0/.test(data['php_version'])) {
data.version = '1.9.3'; data['version'] = '1.9.3';
} else if (/7\.[1-4]|8\.0/.test(data.php_version)) { } else if (/7\.[1-4]|8\.0/.test(data['php_version'])) {
data.version = '1.22.0'; data['version'] = '1.22.0';
} }
} }
data.url = await getPharUrl(data); data['url'] = await getPharUrl(data);
return addArchive(data); return addArchive(data);
} }
} }
@@ -351,12 +263,12 @@ export async function addBlackfirePlayer(data: ToolData): Promise<string> {
* *
* @param data * @param data
*/ */
export async function addCastor(data: ToolData): Promise<string> { export async function addCastor(data: RS): Promise<string> {
data.tool = 'castor.' + data.os.replace('win32', 'windows') + '-amd64'; data['tool'] = 'castor.' + data['os'].replace('win32', 'windows') + '-amd64';
data.url = await getUrl(data); data['url'] = await getUrl(data);
data.tool = 'castor'; data['tool'] = 'castor';
data.version_parameter = fs.existsSync('castor.php') data['version_parameter'] = fs.existsSync('castor.php')
? data.version_parameter ? data['version_parameter']
: ''; : '';
return await addArchive(data); return await addArchive(data);
} }
@@ -366,18 +278,18 @@ export async function addCastor(data: ToolData): Promise<string> {
* *
* @param data * @param data
*/ */
export async function addComposer(data: ToolData): Promise<string> { export async function addComposer(data: RS): Promise<string> {
const channel = data.version.replace('latest', 'stable'); const channel = data['version'].replace('latest', 'stable');
const github = data.github; const github = data['github'];
const getcomposer = data.domain; const getcomposer = data['domain'];
const cds = 'https://dl.cloudsmith.io'; const cds = 'https://dl.cloudsmith.io';
const spc = 'https://artifacts.setup-php.com'; const spc = 'https://artifacts.setup-php.com';
const filename = `composer-${data.php_version}-${channel}.phar`; const filename = `composer-${data['php_version']}-${channel}.phar`;
const releases_url = `${github}/shivammathur/composer-cache/releases/latest/download/${filename}`; const releases_url = `${github}/shivammathur/composer-cache/releases/latest/download/${filename}`;
const cds_url = `${cds}/public/shivammathur/composer-cache/raw/files/${filename}`; const cds_url = `${cds}/public/shivammathur/composer-cache/raw/files/${filename}`;
const spc_url = `${spc}/composer/${filename}`; const spc_url = `${spc}/composer/${filename}`;
const lts_url = `${getcomposer}/download/latest-2.2.x/composer.phar`; const lts_url = `${getcomposer}/download/latest-2.2.x/composer.phar`;
const is_lts = /^5\.[3-6]$|^7\.[0-1]$/.test(data.php_version); const is_lts = /^5\.[3-6]$|^7\.[0-1]$/.test(data['php_version']);
const channel_source_url = `${getcomposer}/composer-${channel}.phar`; const channel_source_url = `${getcomposer}/composer-${channel}.phar`;
const version_source_url = `${getcomposer}/download/${channel}/composer.phar`; const version_source_url = `${getcomposer}/download/${channel}/composer.phar`;
let cache_url = `${releases_url},${spc_url},${cds_url}`; let cache_url = `${releases_url},${spc_url},${cds_url}`;
@@ -392,16 +304,16 @@ export async function addComposer(data: ToolData): Promise<string> {
case /^1$/.test(channel): case /^1$/.test(channel):
source_url = channel_source_url; source_url = channel_source_url;
break; break;
case /^\d+\.\d+\.\d+[\w-]*$/.test(data.version): case /^\d+\.\d+\.\d+[\w-]*$/.test(data['version']):
cache_url = `${github}/${data.repository}/releases/download/${data.version}/composer.phar`; cache_url = `${github}/${data['repository']}/releases/download/${data['version']}/composer.phar`;
source_url = version_source_url; source_url = version_source_url;
break; break;
default: default:
source_url = is_lts ? lts_url : channel_source_url; source_url = is_lts ? lts_url : channel_source_url;
} }
const use_cache: boolean = (await utils.readEnv('NO_TOOLS_CACHE')) !== 'true'; const use_cache: boolean = (await utils.readEnv('NO_TOOLS_CACHE')) !== 'true';
data.url = use_cache ? `${cache_url},${source_url}` : source_url; data['url'] = use_cache ? `${cache_url},${source_url}` : source_url;
data.version_parameter = data.version; data['version_parameter'] = data['version'];
return await addArchive(data); return await addArchive(data);
} }
@@ -410,27 +322,27 @@ export async function addComposer(data: ToolData): Promise<string> {
* *
* @param data * @param data
*/ */
export async function addDeployer(data: ToolData): Promise<string> { export async function addDeployer(data: RS): Promise<string> {
if (data.version === 'latest') { if (data['version'] === 'latest') {
data.url = data.domain + '/deployer.phar'; data['url'] = data['domain'] + '/deployer.phar';
} else { } else {
const manifest = await fetch.fetch('https://deployer.org/manifest.json'); const manifest: RS = await fetch.fetch(
const version_data: Record<string, DeployerManifestEntry> = JSON.parse( 'https://deployer.org/manifest.json'
manifest.data
); );
const version_data: RSRS = JSON.parse(manifest.data);
const version_key: string | undefined = Object.keys(version_data).find( const version_key: string | undefined = Object.keys(version_data).find(
(key: string) => { (key: string) => {
return version_data[key].version === data.version; return version_data[key]['version'] === data['version'];
} }
); );
if (version_key) { if (version_key) {
data.url = version_data[version_key].url; data['url'] = version_data[version_key]['url'];
} else { } else {
return await utils.addLog( return await utils.addLog(
'$cross', '$cross',
'deployer', 'deployer',
'Version missing in deployer manifest', 'Version missing in deployer manifest',
data.os data['os']
); );
} }
} }
@@ -442,22 +354,22 @@ export async function addDeployer(data: ToolData): Promise<string> {
* *
* @param data * @param data
*/ */
export async function addDevTools(data: ToolData): Promise<string> { export async function addDevTools(data: RS): Promise<string> {
switch (data.os) { switch (data['os']) {
case 'linux': case 'linux':
case 'darwin': case 'darwin':
return 'add_devtools ' + data.tool; return 'add_devtools ' + data['tool'];
case 'win32': case 'win32':
return await utils.addLog( return await utils.addLog(
'$tick', '$tick',
data.tool, data['tool'],
data.tool + ' is not a windows tool', data['tool'] + ' is not a windows tool',
'win32' 'win32'
); );
default: default:
return await utils.log( return await utils.log(
'Platform ' + data.os + ' is not supported', 'Platform ' + data['os'] + ' is not supported',
data.os, data['os'],
'error' 'error'
); );
} }
@@ -468,8 +380,8 @@ export async function addDevTools(data: ToolData): Promise<string> {
* *
* @param data * @param data
*/ */
export async function addPECL(data: ToolData): Promise<string> { export async function addPECL(data: RS): Promise<string> {
return await utils.getCommand(data.os, 'pecl'); return await utils.getCommand(data['os'], 'pecl');
} }
/** /**
@@ -477,13 +389,14 @@ export async function addPECL(data: ToolData): Promise<string> {
* *
* @param data * @param data
*/ */
export async function addPhing(data: ToolData): Promise<string> { export async function addPhing(data: RS): Promise<string> {
data.url = data.domain + '/get/phing-' + data.version + data.extension; data['url'] =
if (data.version != 'latest') { data['domain'] + '/get/phing-' + data['version'] + data['extension'];
[data.prefix, data.verb] = ['releases', 'download']; if (data['version'] != 'latest') {
data.domain = data.github; [data['prefix'], data['verb']] = ['releases', 'download'];
data.extension = '-' + data.version + data.extension; data['domain'] = data['github'];
data.url += ',' + (await getUrl(data)); data['extension'] = '-' + data['version'] + data['extension'];
data['url'] += ',' + (await getUrl(data));
} }
return await addArchive(data); return await addArchive(data);
} }
@@ -493,33 +406,33 @@ export async function addPhing(data: ToolData): Promise<string> {
* *
* @param data * @param data
*/ */
export async function addPhive(data: ToolData): Promise<string> { export async function addPhive(data: RS): Promise<string> {
switch (true) { switch (true) {
case /5\.[3-5]/.test(data.php_version): case /5\.[3-5]/.test(data['php_version']):
return await utils.addLog( return await utils.addLog(
'$cross', '$cross',
'phive', 'phive',
'Phive is not supported on PHP ' + data.php_version, 'Phive is not supported on PHP ' + data['php_version'],
data.os data['os']
); );
case /5\.6|7\.0/.test(data.php_version): case /5\.6|7\.0/.test(data['php_version']):
data.version = '0.12.1'; data['version'] = '0.12.1';
break; break;
case /7\.1/.test(data.php_version): case /7\.1/.test(data['php_version']):
data.version = '0.13.5'; data['version'] = '0.13.5';
break; break;
case /7\.2/.test(data.php_version): case /7\.2/.test(data['php_version']):
data.version = '0.14.5'; data['version'] = '0.14.5';
break; break;
case /7\.3|7\.4/.test(data.php_version): case /7\.3|7\.4/.test(data['php_version']):
data.version = '0.15.3'; data['version'] = '0.15.3';
break; break;
case /^latest$/.test(data.version): case /^latest$/.test(data['version']):
data.version = await getLatestVersion(data); data['version'] = await getLatestVersion(data);
break; break;
} }
data.extension = '-' + data.version + data.extension; data['extension'] = '-' + data['version'] + data['extension'];
data.url = await getUrl(data); data['url'] = await getUrl(data);
return await addArchive(data); return await addArchive(data);
} }
@@ -528,15 +441,16 @@ export async function addPhive(data: ToolData): Promise<string> {
* *
* @param data * @param data
*/ */
export async function addPHPUnitTools(data: ToolData): Promise<string> { export async function addPHPUnitTools(data: RS): Promise<string> {
/* istanbul ignore next */ /* istanbul ignore next */
if (data.version === 'latest') { if (data['version'] === 'latest') {
data.version = data['version'] =
(await packagist.search(data.packagist, data.php_version)) ?? 'latest'; (await packagist.search(data['packagist'], data['php_version'])) ??
'latest';
} }
data.url = await getPharUrl(data); data['url'] = await getPharUrl(data);
if (data.url.match(/-\d+/)) { if (data['url'].match(/-\d+/)) {
data.url += ',' + data.url.replace(/-(\d+)\.\d+\.\d+/, '-$1'); data['url'] += ',' + data['url'].replace(/-(\d+)\.\d+\.\d+/, '-$1');
} }
return await addArchive(data); return await addArchive(data);
} }
@@ -546,13 +460,13 @@ export async function addPHPUnitTools(data: ToolData): Promise<string> {
* *
* @param data * @param data
*/ */
export async function addWPCLI(data: ToolData): Promise<string> { export async function addWPCLI(data: RS): Promise<string> {
if (data.version === 'latest') { if (data['version'] === 'latest') {
data.uri = 'wp-cli/builds/blob/gh-pages/phar/wp-cli.phar?raw=true'; data['uri'] = 'wp-cli/builds/blob/gh-pages/phar/wp-cli.phar?raw=true';
data.url = [data.domain, data.uri].join('/'); data['url'] = [data['domain'], data['uri']].join('/');
} else { } else {
data.extension = '-' + data.version + data.extension; data['extension'] = '-' + data['version'] + data['extension'];
data.url = await getUrl(data); data['url'] = await getUrl(data);
} }
return await addArchive(data); return await addArchive(data);
} }
@@ -568,74 +482,56 @@ export async function getData(
release: string, release: string,
php_version: string, php_version: string,
os: string os: string
): Promise<ToolData> { ): Promise<RS> {
const json_file_path = path.join(__dirname, '../src/configs/tools.json'); const json_file_path = path.join(__dirname, '../src/configs/tools.json');
const json_file: string = fs.readFileSync(json_file_path, 'utf8'); const json_file: string = fs.readFileSync(json_file_path, 'utf8');
const json_objects: Record<string, ToolConfig> = JSON.parse(json_file); const json_objects: RSRS = JSON.parse(json_file);
release = release.replace(/\s+/g, ''); release = release.replace(/\s+/g, '');
const parts: string[] = release.split(':'); const parts: string[] = release.split(':');
const tool = parts[0]; const tool = parts[0];
const version = parts[1]; const version = parts[1];
let config: ToolConfig & {tool: string}; let data: RS;
if (Object.hasOwn(json_objects, tool)) { if (Object.keys(json_objects).includes(tool)) {
config = {...json_objects[tool], tool}; data = json_objects[tool];
data['tool'] = tool;
} else { } else {
const key: string | undefined = Object.keys(json_objects).find( const key: string | undefined = Object.keys(json_objects).find(
(key: string) => { (key: string) => {
return json_objects[key].alias == tool; return json_objects[key]['alias'] == tool;
} }
); );
if (key) { if (key) {
config = {...json_objects[key], tool: key}; data = json_objects[key];
} else if (tool.includes('/')) { data['tool'] = key;
config = { } else {
data = {
tool: tool.split('/')[1], tool: tool.split('/')[1],
repository: tool, repository: tool,
type: 'composer' type: 'composer'
}; };
} else { data = !tool.includes('/') ? {tool: tool} : data;
config = {tool};
} }
} }
const github = 'https://github.com'; data['github'] = 'https://github.com';
const domain = config.domain ?? github; data['domain'] ??= data['github'];
const data: ToolData = { data['extension'] ??= '.phar';
tool: config.tool, data['os'] = os;
version: '', data['php_version'] = php_version;
url: '', data['packagist'] ??= data['repository'];
os, data['prefix'] = data['github'] === data['domain'] ? 'releases' : '';
php_version, data['verb'] = data['github'] === data['domain'] ? 'download' : '';
github, data['fetch_latest'] ??= 'false';
domain, data['scope'] ??= 'global';
extension: config.extension ?? '.phar', data['version_parameter'] = JSON.stringify(data['version_parameter']) || '';
repository: config.repository ?? '', data['version_prefix'] ??= '';
prefix: domain === github ? 'releases' : '', data['release'] = await getRelease(release, data);
verb: domain === github ? 'download' : '', data['version'] = version
fetch_latest: config.fetch_latest ?? 'false',
scope: config.scope ?? 'global',
version_parameter:
config.version_parameter != null
? JSON.stringify(config.version_parameter)
: '',
version_prefix: config.version_prefix ?? '',
release: '',
packagist: config.packagist ?? config.repository ?? '',
type: config.type,
function: config.function,
alias: config.alias
};
data.release = await getRelease(release, data);
data.version = version
? await getVersion(version, data) ? await getVersion(version, data)
: await getLatestVersion(data); : await getLatestVersion(data);
data.url = await getUrl(data);
return data; return data;
} }
export const functionRecord: Record< export const functionRecord: Record<string, (data: RS) => Promise<string>> = {
ToolFunction,
(data: ToolData) => Promise<string>
> = {
castor: addCastor, castor: addCastor,
composer: addComposer, composer: addComposer,
deployer: addDeployer, deployer: addDeployer,
@@ -669,46 +565,43 @@ export async function addTools(
} }
const tools_list = await filterList(await utils.CSVArray(tools_csv)); const tools_list = await filterList(await utils.CSVArray(tools_csv));
await utils.asyncForEach(tools_list, async function (release: string) { await utils.asyncForEach(tools_list, async function (release: string) {
const data: ToolData = await getData(release, php_version, os); const data: RS = await getData(release, php_version, os);
script += '\n'; script += '\n';
switch (true) { switch (true) {
case data.error !== undefined: case data['error'] !== undefined:
script += await utils.addLog('$cross', data.tool, data.error, data.os);
break;
case 'phar' === data.type:
script += await addArchive(data);
break;
case 'composer' === data.type:
script += await addPackage(data);
break;
case 'custom-package' === data.type:
script += await utils.customPackage(
data.tool.split('-')[0],
'tools',
data.version,
data.os
);
break;
case 'custom-function' === data.type:
if (!data.function) {
script += await utils.addLog( script += await utils.addLog(
'$cross', '$cross',
data.tool, data['tool'],
data.tool + ' has no function defined. Please report this issue.', data['error'],
data.os data['os']
); );
} else {
script += await functionRecord[data.function](data);
}
break; break;
case /^none$/.test(data.tool): case 'phar' === data['type']:
data['url'] = await getUrl(data);
script += await addArchive(data);
break;
case 'composer' === data['type']:
script += await addPackage(data);
break;
case 'custom-package' === data['type']:
script += await utils.customPackage(
data['tool'].split('-')[0],
'tools',
data['version'],
data['os']
);
break;
case 'custom-function' === data['type']:
script += await functionRecord[data['function']](data);
break;
case /^none$/.test(data['tool']):
break; break;
default: default:
script += await utils.addLog( script += await utils.addLog(
'$cross', '$cross',
data.tool, data['tool'],
'Tool ' + data.tool + ' is not supported', 'Tool ' + data['tool'] + ' is not supported',
data.os data['os']
); );
break; break;
} }

View File

@@ -1,6 +1,6 @@
import fs from 'fs'; import fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import * as core from './core'; import * as core from '@actions/core';
import * as fetch from './fetch'; import * as fetch from './fetch';
/** /**
@@ -62,7 +62,7 @@ export async function getManifestURLS(): Promise<string[]> {
*/ */
export async function parseVersion(version: string): Promise<string> { export async function parseVersion(version: string): Promise<string> {
switch (true) { switch (true) {
case /^(latest|lowest|highest|nightly|master|\d+\.x)$/.test(version): case /^(latest|lowest|highest|nightly|\d+\.x)$/.test(version):
for (const manifestURL of await getManifestURLS()) { for (const manifestURL of await getManifestURLS()) {
const fetchResult = await fetch.fetch(manifestURL); const fetchResult = await fetch.fetch(manifestURL);
if (fetchResult['data'] ?? false) { if (fetchResult['data'] ?? false) {
@@ -97,8 +97,9 @@ export async function parseIniFile(ini_file: string): Promise<string> {
} }
/** /**
* Async foreach loop using modern for...of pattern * Async foreach loop
* *
* @author https://github.com/Atinux
* @param array * @param array
* @param callback * @param callback
*/ */
@@ -110,8 +111,8 @@ export async function asyncForEach(
array: Array<string> array: Array<string>
) => Promise<void> ) => Promise<void>
): Promise<void> { ): Promise<void> {
for (const [index, element] of array.entries()) { for (let index = 0; index < array.length; index++) {
await callback(element, index, array); await callback(array[index], index, array);
} }
} }

View File

@@ -3,7 +3,7 @@
"declaration": true, "declaration": true,
"esModuleInterop": true, "esModuleInterop": true,
"lib": [ "lib": [
"ES2024" "ES2021"
], ],
"module": "commonjs", "module": "commonjs",
"moduleResolution": "node", "moduleResolution": "node",
@@ -13,7 +13,7 @@
"rootDir": "./src", "rootDir": "./src",
"sourceMap": true, "sourceMap": true,
"strict": true, "strict": true,
"target": "ES2024" "target": "ES2021"
}, },
"exclude": ["__tests__", "lib", "node_modules"] "exclude": ["__tests__", "lib", "node_modules"]
} }