mirror of
https://github.com/graycoreio/github-actions-magento2.git
synced 2026-06-08 19:46:41 +00:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7ffa821441 | |||
| ebfdeb0b73 | |||
| 8a0f197a13 | |||
| 0bf08ef692 | |||
| 35c1ace2bc | |||
| d07afbacd0 | |||
| b71bb8b4aa | |||
| e39dd46f9c | |||
| b98313e100 | |||
| 0c7d14d885 | |||
| 6d4ca8d669 | |||
| b790da1859 | |||
| e89f6ad2e0 | |||
| 8e82fcc893 | |||
| 83ef32c838 | |||
| de7c47f07d |
@@ -42,15 +42,28 @@ on:
|
||||
description: "Your composer credentials (typically a stringified json object of the contents of your auth.json)"
|
||||
|
||||
jobs:
|
||||
compute_resolved:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
resolved: ${{ steps.resolve.outputs.resolved }}
|
||||
steps:
|
||||
- uses: graycoreio/github-actions-magento2/resolve-check-config@main
|
||||
id: resolve
|
||||
with:
|
||||
kind: extension
|
||||
matrix: ${{ inputs.matrix }}
|
||||
|
||||
unit-test-extension:
|
||||
runs-on: ${{ matrix.os }}
|
||||
needs: compute_resolved
|
||||
if: ${{ fromJSON(needs.compute_resolved.outputs.resolved)['unit-test-extension'].enabled != false }}
|
||||
strategy:
|
||||
matrix: ${{ fromJSON(inputs.matrix) }}
|
||||
matrix: ${{ fromJSON(needs.compute_resolved.outputs.resolved)['unit-test-extension'].matrix }}
|
||||
fail-fast: ${{ inputs.fail-fast }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/setup-magento@v8.0.0
|
||||
- uses: graycoreio/github-actions-magento2/setup-magento@main
|
||||
id: setup-magento
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
@@ -79,7 +92,7 @@ jobs:
|
||||
env:
|
||||
COMPOSER_AUTH: ${{ secrets.composer_auth }}
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/cache-magento@v8.0.0
|
||||
- uses: graycoreio/github-actions-magento2/cache-magento@main
|
||||
with:
|
||||
composer_cache_key: ${{ inputs.composer_cache_key && format('{0} | {1}', inputs.composer_cache_key, matrix.magento) || matrix.magento }}
|
||||
working-directory: ${{ steps.setup-magento.outputs.path }}
|
||||
@@ -113,13 +126,15 @@ jobs:
|
||||
|
||||
compile-extension:
|
||||
runs-on: ${{ matrix.os }}
|
||||
needs: compute_resolved
|
||||
if: ${{ fromJSON(needs.compute_resolved.outputs.resolved)['compile-extension'].enabled != false }}
|
||||
strategy:
|
||||
matrix: ${{ fromJSON(inputs.matrix) }}
|
||||
matrix: ${{ fromJSON(needs.compute_resolved.outputs.resolved)['compile-extension'].matrix }}
|
||||
fail-fast: ${{ inputs.fail-fast }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/setup-magento@v8.0.0
|
||||
- uses: graycoreio/github-actions-magento2/setup-magento@main
|
||||
id: setup-magento
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
@@ -148,7 +163,7 @@ jobs:
|
||||
env:
|
||||
COMPOSER_AUTH: ${{ secrets.composer_auth }}
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/cache-magento@v8.0.0
|
||||
- uses: graycoreio/github-actions-magento2/cache-magento@main
|
||||
with:
|
||||
composer_cache_key: ${{ inputs.composer_cache_key && format('{0} | {1}', inputs.composer_cache_key, matrix.magento) || matrix.magento }}
|
||||
working-directory: ${{ steps.setup-magento.outputs.path }}
|
||||
@@ -161,14 +176,25 @@ jobs:
|
||||
COMPOSER_AUTH: ${{ secrets.composer_auth }}
|
||||
COMPOSER_MIRROR_PATH_REPOS: 1
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/setup-di-compile@v8.0.0
|
||||
- uses: graycoreio/github-actions-magento2/setup-di-compile@main
|
||||
with:
|
||||
path: ${{ steps.setup-magento.outputs.path }}
|
||||
|
||||
compute_latest_matrix:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matrix: ${{ steps.supported-version.outputs.matrix }}
|
||||
steps:
|
||||
- uses: graycoreio/github-actions-magento2/supported-version@main
|
||||
id: supported-version
|
||||
with:
|
||||
kind: latest
|
||||
|
||||
coding-standard:
|
||||
runs-on: ${{ matrix.os }}
|
||||
needs: compute_latest_matrix
|
||||
strategy:
|
||||
matrix: ${{ fromJSON(inputs.matrix) }}
|
||||
matrix: ${{ fromJSON(needs.compute_latest_matrix.outputs.matrix) }}
|
||||
fail-fast: ${{ inputs.fail-fast }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
@@ -179,25 +205,27 @@ jobs:
|
||||
tools: composer:v${{ matrix.composer }}
|
||||
coverage: none
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/cache-magento@v8.0.0
|
||||
- uses: graycoreio/github-actions-magento2/cache-magento@main
|
||||
with:
|
||||
composer_cache_key: ${{ inputs.composer_cache_key && format('{0} | {1}', inputs.composer_cache_key, matrix.magento) || matrix.magento }}
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/coding-standard@v8.0.0
|
||||
- uses: graycoreio/github-actions-magento2/coding-standard@main
|
||||
with:
|
||||
path: ${{ inputs.path }}
|
||||
composer_auth: ${{ secrets.composer_auth }}
|
||||
|
||||
integration_test:
|
||||
runs-on: ${{ matrix.os }}
|
||||
needs: compute_resolved
|
||||
if: ${{ fromJSON(needs.compute_resolved.outputs.resolved)['integration_test'].enabled != false }}
|
||||
strategy:
|
||||
matrix: ${{ fromJSON(inputs.matrix) }}
|
||||
matrix: ${{ fromJSON(needs.compute_resolved.outputs.resolved)['integration_test'].matrix }}
|
||||
fail-fast: ${{ inputs.fail-fast }}
|
||||
services: ${{ matrix.services }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/setup-magento@v8.0.0
|
||||
- uses: graycoreio/github-actions-magento2/setup-magento@main
|
||||
id: setup-magento
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
@@ -226,7 +254,7 @@ jobs:
|
||||
env:
|
||||
COMPOSER_AUTH: ${{ secrets.composer_auth }}
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/cache-magento@v8.0.0
|
||||
- uses: graycoreio/github-actions-magento2/cache-magento@main
|
||||
with:
|
||||
composer_cache_key: ${{ inputs.composer_cache_key && format('{0} | {1}', inputs.composer_cache_key, matrix.magento) || matrix.magento }}
|
||||
working-directory: ${{ steps.setup-magento.outputs.path }}
|
||||
@@ -239,7 +267,7 @@ jobs:
|
||||
COMPOSER_AUTH: ${{ secrets.composer_auth }}
|
||||
COMPOSER_MIRROR_PATH_REPOS: 1
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/get-magento-version@v8.0.0
|
||||
- uses: graycoreio/github-actions-magento2/get-magento-version@main
|
||||
id: magento-version
|
||||
with:
|
||||
working-directory: ${{ steps.setup-magento.outputs.path }}
|
||||
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
compute_matrix:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matrix: ${{ steps.supported-version.outputs.matrix }}
|
||||
resolved: ${{ steps.resolve.outputs.resolved }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
if: inputs.store_artifact_name == ''
|
||||
@@ -46,22 +46,29 @@ jobs:
|
||||
name: ${{ inputs.store_artifact_name }}
|
||||
path: ${{ inputs.path }}
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/get-magento-version@v8.0.0
|
||||
- uses: graycoreio/github-actions-magento2/get-magento-version@main
|
||||
id: get-magento-version
|
||||
with:
|
||||
working-directory: ${{ inputs.path }}
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/supported-version@v8.0.0
|
||||
- uses: graycoreio/github-actions-magento2/supported-version@main
|
||||
id: supported-version
|
||||
with:
|
||||
kind: custom
|
||||
custom_versions: ${{ steps.get-magento-version.outputs.project }}:${{ fromJSON(steps.get-magento-version.outputs.version) }}
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/resolve-check-config@main
|
||||
id: resolve
|
||||
with:
|
||||
kind: store
|
||||
matrix: ${{ steps.supported-version.outputs.matrix }}
|
||||
|
||||
unit-test:
|
||||
runs-on: ${{ matrix.os }}
|
||||
needs: compute_matrix
|
||||
if: ${{ fromJSON(needs.compute_matrix.outputs.resolved)['unit-test'].enabled != false }}
|
||||
strategy:
|
||||
matrix: ${{ fromJSON(needs.compute_matrix.outputs.matrix) }}
|
||||
matrix: ${{ fromJSON(needs.compute_matrix.outputs.resolved)['unit-test'].matrix }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
@@ -73,7 +80,7 @@ jobs:
|
||||
name: ${{ inputs.store_artifact_name }}
|
||||
path: ${{ inputs.path }}
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/setup-magento@v8.0.0
|
||||
- uses: graycoreio/github-actions-magento2/setup-magento@main
|
||||
id: setup-magento
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
@@ -82,7 +89,7 @@ jobs:
|
||||
working-directory: ${{ inputs.path }}
|
||||
composer_auth: ${{ secrets.composer_auth }}
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/cache-magento@v8.0.0
|
||||
- uses: graycoreio/github-actions-magento2/cache-magento@main
|
||||
with:
|
||||
composer_cache_key: ${{ inputs.composer_cache_key }}
|
||||
working-directory: ${{ steps.setup-magento.outputs.path }}
|
||||
@@ -122,8 +129,9 @@ jobs:
|
||||
coding-standard:
|
||||
runs-on: ${{ matrix.os }}
|
||||
needs: compute_matrix
|
||||
if: ${{ fromJSON(needs.compute_matrix.outputs.resolved)['coding-standard'].enabled != false }}
|
||||
strategy:
|
||||
matrix: ${{ fromJSON(needs.compute_matrix.outputs.matrix) }}
|
||||
matrix: ${{ fromJSON(needs.compute_matrix.outputs.resolved)['coding-standard'].matrix }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
@@ -135,7 +143,7 @@ jobs:
|
||||
name: ${{ inputs.store_artifact_name }}
|
||||
path: ${{ inputs.path }}
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/setup-magento@v8.0.0
|
||||
- uses: graycoreio/github-actions-magento2/setup-magento@main
|
||||
id: setup-magento
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
@@ -144,7 +152,7 @@ jobs:
|
||||
working-directory: ${{ inputs.path }}
|
||||
composer_auth: ${{ secrets.composer_auth }}
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/cache-magento@v8.0.0
|
||||
- uses: graycoreio/github-actions-magento2/cache-magento@main
|
||||
with:
|
||||
composer_cache_key: ${{ inputs.composer_cache_key }}
|
||||
working-directory: ${{ steps.setup-magento.outputs.path }}
|
||||
@@ -169,7 +177,67 @@ jobs:
|
||||
EOF
|
||||
fi
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/coding-standard@v8.0.0
|
||||
- uses: graycoreio/github-actions-magento2/coding-standard@main
|
||||
with:
|
||||
path: ${{ steps.setup-magento.outputs.path }}
|
||||
composer_auth: ${{ secrets.composer_auth }}
|
||||
composer_auth: ${{ secrets.composer_auth }}
|
||||
|
||||
smoke-test:
|
||||
runs-on: ${{ matrix.os }}
|
||||
needs: compute_matrix
|
||||
if: ${{ fromJSON(needs.compute_matrix.outputs.resolved)['smoke-test'].enabled != false }}
|
||||
services: ${{ matrix.services }}
|
||||
strategy:
|
||||
matrix: ${{ fromJSON(needs.compute_matrix.outputs.resolved)['smoke-test'].matrix }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
if: inputs.store_artifact_name == ''
|
||||
|
||||
- uses: actions/download-artifact@v8
|
||||
if: inputs.store_artifact_name != ''
|
||||
with:
|
||||
name: ${{ inputs.store_artifact_name }}
|
||||
path: ${{ inputs.path }}
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/setup-magento@main
|
||||
id: setup-magento
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
tools: composer:v${{ matrix.composer }}
|
||||
mode: store
|
||||
working-directory: ${{ inputs.path }}
|
||||
composer_auth: ${{ secrets.composer_auth }}
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/cache-magento@main
|
||||
with:
|
||||
composer_cache_key: ${{ inputs.composer_cache_key }}
|
||||
working-directory: ${{ steps.setup-magento.outputs.path }}
|
||||
stamp: ${{ inputs.stamp }}
|
||||
|
||||
- name: Composer install
|
||||
working-directory: ${{ steps.setup-magento.outputs.path }}
|
||||
run: composer install
|
||||
env:
|
||||
COMPOSER_AUTH: ${{ secrets.composer_auth }}
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/setup-install@main
|
||||
id: setup-install
|
||||
with:
|
||||
services: ${{ toJSON(matrix.services) }}
|
||||
path: ${{ steps.setup-magento.outputs.path }}
|
||||
container_id: ${{ job.services['php-fpm'].id }}
|
||||
extra_args: --magento-init-params=MAGE_MODE=developer
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/configure-service-nginx@main
|
||||
with:
|
||||
container_id: ${{ job.services.nginx.id }}
|
||||
magento_path: ${{ inputs.path }}
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/smoke-test@main
|
||||
with:
|
||||
kind: page
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/smoke-test@main
|
||||
with:
|
||||
kind: graphql
|
||||
@@ -82,7 +82,7 @@ jobs:
|
||||
COMPOSER_AUTH: ${{ secrets.composer_auth }}
|
||||
name: Create Magento ${{ matrix.magento }} Project
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/get-magento-version@v8.0.0
|
||||
- uses: graycoreio/github-actions-magento2/get-magento-version@main
|
||||
id: magento-version
|
||||
with:
|
||||
working-directory: ${{ inputs.magento_directory }}
|
||||
|
||||
@@ -7,21 +7,20 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release-mode:
|
||||
description: 'auto = follow conventional commits; rc = bump the rc suffix; stable = open a PR that graduates the current rc to a stable release.'
|
||||
description: 'auto = follow conventional commits; rc = bump the rc suffix; graduate = graduate the current rc to a stable release.'
|
||||
type: choice
|
||||
required: false
|
||||
default: auto
|
||||
options:
|
||||
- auto
|
||||
- rc
|
||||
- stable
|
||||
- graduate
|
||||
|
||||
env:
|
||||
RELEASE_BRANCH: release-please--branches--main--components--github-actions-magento2
|
||||
|
||||
jobs:
|
||||
release-please:
|
||||
if: github.event_name != 'workflow_dispatch' || inputs.release-mode != 'stable'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -32,7 +31,7 @@ jobs:
|
||||
uses: googleapis/release-please-action@v4
|
||||
with:
|
||||
token: ${{ secrets.GRAYCORE_GITHUB_TOKEN }}
|
||||
config-file: ${{ inputs.release-mode == 'rc' && 'release-please-config.rc.json' || 'release-please-config.json' }}
|
||||
config-file: ${{ inputs.release-mode == 'rc' && 'release-please-config.rc.json' || (inputs.release-mode == 'graduate' && 'release-please-config.graduate.json' || 'release-please-config.json') }}
|
||||
|
||||
- name: Check if release branch exists
|
||||
id: branch-check
|
||||
@@ -85,69 +84,6 @@ jobs:
|
||||
git commit -m "chore: pin internal action refs to ${{ steps.pin-refs.outputs.VERSION }}"
|
||||
git push origin ${{ env.RELEASE_BRANCH }}
|
||||
|
||||
graduate:
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.release-mode == 'stable'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: main
|
||||
token: ${{ secrets.GRAYBOT_PIN_BACK_PAT }}
|
||||
|
||||
- name: Compute graduate version
|
||||
id: graduate
|
||||
run: |
|
||||
CURRENT=$(jq -r '."."' .release-please-manifest.json)
|
||||
STABLE="${CURRENT%%-*}"
|
||||
if [ "$CURRENT" = "$STABLE" ]; then
|
||||
echo "::error::Manifest version ${CURRENT} has no prerelease suffix; nothing to graduate."
|
||||
exit 1
|
||||
fi
|
||||
echo "CURRENT=${CURRENT}" >> $GITHUB_OUTPUT
|
||||
echo "STABLE=${STABLE}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Open graduate PR
|
||||
env:
|
||||
GRAYBOT_GPG_KEY: ${{ secrets.GRAYBOT_GPG_KEY }}
|
||||
GH_TOKEN: ${{ secrets.GRAYBOT_PIN_BACK_PAT }}
|
||||
CURRENT: ${{ steps.graduate.outputs.CURRENT }}
|
||||
STABLE: ${{ steps.graduate.outputs.STABLE }}
|
||||
run: |
|
||||
echo "$GRAYBOT_GPG_KEY" | gpg --batch --import
|
||||
export GPG_KEY_ID=$(gpg --list-secret-keys --keyid-format LONG | grep sec | awk '{print $2}' | cut -d/ -f2)
|
||||
git config --global user.signingkey $GPG_KEY_ID
|
||||
git config --global commit.gpgSign true
|
||||
git config --global user.email "automation@graycore.io"
|
||||
git config --global user.name "Beep Boop"
|
||||
BRANCH="chore/graduate-v${STABLE}"
|
||||
git checkout -b "$BRANCH"
|
||||
git commit --allow-empty \
|
||||
-m "chore: graduate ${CURRENT} to ${STABLE}" \
|
||||
-m "Release-As: ${STABLE}"
|
||||
git push --force origin "$BRANCH"
|
||||
EXISTING=$(gh pr list --head "$BRANCH" --json number --jq '.[0].number // empty')
|
||||
PR_BODY=$(cat <<EOF
|
||||
Graduates the release line from \`${CURRENT}\` to a stable \`${STABLE}\`.
|
||||
|
||||
When this PR is merged to \`main\`, release-please will pick up the \`Release-As\` footer below and open a release PR for \`v${STABLE}\`.
|
||||
|
||||
Release-As: ${STABLE}
|
||||
EOF
|
||||
)
|
||||
if [ -z "$EXISTING" ]; then
|
||||
gh pr create \
|
||||
--base main \
|
||||
--head "$BRANCH" \
|
||||
--title "chore: graduate ${CURRENT} → ${STABLE}" \
|
||||
--body "$PR_BODY"
|
||||
else
|
||||
gh pr edit "$EXISTING" --body "$PR_BODY"
|
||||
echo "PR #$EXISTING already exists for $BRANCH — updated body"
|
||||
fi
|
||||
|
||||
pinback:
|
||||
needs: release-please
|
||||
if: needs.release-please.outputs.releases_created == 'true'
|
||||
|
||||
@@ -1 +1 @@
|
||||
{".":"8.0.0"}
|
||||
{".":"8.2.0"}
|
||||
|
||||
@@ -2,6 +2,31 @@
|
||||
|
||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||
|
||||
## [8.2.0](https://github.com/graycoreio/github-actions-magento2/compare/v8.1.0...v8.2.0) (2026-05-17)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **check-extension:** allow configuraton via .github/check-extension.json ([#269](https://github.com/graycoreio/github-actions-magento2/issues/269)) ([0bf08ef](https://github.com/graycoreio/github-actions-magento2/commit/0bf08ef69291090e5fe3e3d47cb432c6c9107f30))
|
||||
* **resolve-check-config:** defined required integration test services required ([#269](https://github.com/graycoreio/github-actions-magento2/issues/269)) ([35c1ace](https://github.com/graycoreio/github-actions-magento2/commit/35c1ace2bc68be1356dc6565a8a05ff02e33d75d))
|
||||
|
||||
## [8.1.0](https://github.com/graycoreio/github-actions-magento2/compare/v8.0.0...v8.1.0) (2026-05-17)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **check-store:** add smoke-test action and use resolve-check-config ([#255](https://github.com/graycoreio/github-actions-magento2/issues/255)) ([e39dd46](https://github.com/graycoreio/github-actions-magento2/commit/e39dd46f9c53a0d2625cd5d19ad1cf18565b8c5c))
|
||||
* **configure-service-nginx:** add ability to adjust nginx conf after init ([#255](https://github.com/graycoreio/github-actions-magento2/issues/255)) ([0c7d14d](https://github.com/graycoreio/github-actions-magento2/commit/0c7d14d88573d92c81654b1107ef6a9e4d918cff))
|
||||
* **resolve-check-config:** add ability to use a config file to adjust jobs ([#255](https://github.com/graycoreio/github-actions-magento2/issues/255)) ([b98313e](https://github.com/graycoreio/github-actions-magento2/commit/b98313e10044a0a6a04546d3ff8ebe3a3f284f5b))
|
||||
* **setup-install:** add a container_id input to run setup:install against a specific container ([#255](https://github.com/graycoreio/github-actions-magento2/issues/255)) ([6d4ca8d](https://github.com/graycoreio/github-actions-magento2/commit/6d4ca8d669164d840d99e8af721309abb9f204ea))
|
||||
* **smoke-test:** add simple smoke test action ([#255](https://github.com/graycoreio/github-actions-magento2/issues/255)) ([b790da1](https://github.com/graycoreio/github-actions-magento2/commit/b790da18597e58a9013cc0f7e2c923f08c82f813))
|
||||
* **supported-version:** add service_preferences and support for php-fpm and nginx ([#255](https://github.com/graycoreio/github-actions-magento2/issues/255)) ([e89f6ad](https://github.com/graycoreio/github-actions-magento2/commit/e89f6ad2e08fcaa03cba92c8371e60ba67b3cf62))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **check-extension:** only run coding-standard on most recent version of Magento ([#265](https://github.com/graycoreio/github-actions-magento2/issues/265)) ([8e82fcc](https://github.com/graycoreio/github-actions-magento2/commit/8e82fcc89354c83523781c1f5fd4622dec19ca7b))
|
||||
|
||||
## [8.0.0](https://github.com/graycoreio/github-actions-magento2/compare/v8.0.0-rc.2...v8.0.0) (2026-05-14)
|
||||
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ The `composer.lock` hash is derived from `working-directory/composer.lock` using
|
||||
### Extension (download cache only)
|
||||
|
||||
```yml
|
||||
- uses: graycoreio/github-actions-magento2/cache-magento@v8.0.0 # x-release-please-version
|
||||
- uses: graycoreio/github-actions-magento2/cache-magento@v8.2.0 # x-release-please-version
|
||||
with:
|
||||
composer_cache_key: ${{ inputs.composer_cache_key }}
|
||||
```
|
||||
@@ -41,13 +41,13 @@ The `composer.lock` hash is derived from `working-directory/composer.lock` using
|
||||
### Extension or store (download + vendor stamp cache)
|
||||
|
||||
```yml
|
||||
- uses: graycoreio/github-actions-magento2/setup-magento@v8.0.0 # x-release-please-version
|
||||
- uses: graycoreio/github-actions-magento2/setup-magento@v8.2.0 # x-release-please-version
|
||||
id: setup-magento
|
||||
with:
|
||||
mode: extension # or store
|
||||
# ...
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/cache-magento@v8.0.0 # x-release-please-version
|
||||
- uses: graycoreio/github-actions-magento2/cache-magento@v8.2.0 # x-release-please-version
|
||||
with:
|
||||
composer_cache_key: ${{ inputs.composer_cache_key }}
|
||||
working-directory: ${{ steps.setup-magento.outputs.path }}
|
||||
@@ -69,7 +69,7 @@ As such, use `stamp: true` when `composer.lock` is stable across most runs — a
|
||||
> **Dependabot / Renovate:** Each time a Dependabot or Renovate PR is merged, the remaining open PRs rebase and each produces a new `composer.lock`. This cascades into a large number of unique cache entries, inflating storage costs without delivering proportional compute savings — because automated PRs are not waiting on fast feedback. The fix is to disable stamp caching for automated dependency PRs entirely:
|
||||
>
|
||||
> ```yml
|
||||
> - uses: graycoreio/github-actions-magento2/cache-magento@v8.0.0 # x-release-please-version
|
||||
> - uses: graycoreio/github-actions-magento2/cache-magento@v8.2.0 # x-release-please-version
|
||||
> with:
|
||||
> stamp: ${{ github.actor != 'dependabot[bot]' }}
|
||||
> ```
|
||||
|
||||
@@ -62,7 +62,7 @@ runs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/get-magento-version@v8.0.0
|
||||
- uses: graycoreio/github-actions-magento2/get-magento-version@main
|
||||
id: cache-magento-get-magento-version
|
||||
with:
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
tools: composer:v2
|
||||
coverage: none
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/coding-standard@v8.0.0 # x-release-please-version
|
||||
- uses: graycoreio/github-actions-magento2/coding-standard@v8.2.0 # x-release-please-version
|
||||
with:
|
||||
path: app/code # Optional, defaults to .
|
||||
version: 25 # Optional, will use the latest if omitted.
|
||||
|
||||
@@ -52,12 +52,12 @@ runs:
|
||||
fi
|
||||
|
||||
- name: Get Composer Version
|
||||
uses: graycoreio/github-actions-magento2/get-composer-version@v8.0.0
|
||||
uses: graycoreio/github-actions-magento2/get-composer-version@main
|
||||
id: get-composer-version
|
||||
if: steps.check-installed.outputs.installed != 'true'
|
||||
|
||||
- name: Check if allow-plugins option is available for this version of composer
|
||||
uses: graycoreio/github-actions-magento2/semver-compare@v8.0.0
|
||||
uses: graycoreio/github-actions-magento2/semver-compare@main
|
||||
id: is-allow-plugins-available
|
||||
if: steps.check-installed.outputs.installed != 'true'
|
||||
with:
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
# Configure Service Nginx
|
||||
|
||||
A GitHub Action that pushes a Magento-aware nginx configuration into an **already-running nginx service container**, reloads it, and waits for the container's healthcheck to pass.
|
||||
|
||||
The action does **not** start nginx. It assumes the calling workflow declared nginx as a `services:` container (typically alongside `php-fpm`, and other mandatory Magento services).
|
||||
|
||||
The shipped `default.conf` is a thin outer wrapper that defines a `fastcgi_backend` upstream pointing at `php-fpm:9000`, sets `$MAGE_ROOT`, and includes Magento's own `nginx.conf.sample` from your own Magento install. All real routing rules come from Magento's bundled file.
|
||||
|
||||
## When to use this
|
||||
|
||||
Use this action when you have a workflow that:
|
||||
|
||||
1. Boots nginx as `services:` containers with the workspace bind-mounted at `/var/www/html`, and
|
||||
2. Wants those containers to actually serve a Magento store you've already installed into the workspace (e.g. for end-to-end smoke tests, integration tests, or any HTTP-driven check).
|
||||
|
||||
You do **not** need this action if:
|
||||
|
||||
- You're not running nginx at all (unit tests, coding standards, static analysis).
|
||||
- nginx is started by something other than a GitHub Actions `services:` block
|
||||
- You've already configured nginx some other way and don't need a Magento-ready outer config.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- An nginx service container is running on the same Docker host as the runner, with an image matching the `image` input.
|
||||
- A `php-fpm` container, the included `default.conf` will set up a fast-cgi backend to `php-fpm:9000`.
|
||||
- The runner's workspace (`$GITHUB_WORKSPACE`) is bind-mounted into the nginx container at `/var/www/html`.
|
||||
- A Magento install exists at `${{ inputs.magento_path }}` relative to the workspace, with `nginx.conf.sample` present (it ships with Magento by default after `composer install`).
|
||||
|
||||
## Inputs
|
||||
|
||||
| Input | Required | Default | Description |
|
||||
| ------------------------ | -------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `container_id` | Yes | — | The ID of the running nginx service container. Pass `${{ job.services.nginx.id }}` (replace `nginx` with whatever you named the service). |
|
||||
| `magento_path` | No | `.` | Path to the Magento store, relative to the GitHub workspace. Combined with the `/var/www/html` mount prefix to compute the in-container `MAGE_ROOT`. |
|
||||
| `health_timeout_seconds` | No | `10` | How long to wait for nginx to report `healthy` after the config is pushed and the container restarts. |
|
||||
|
||||
## Usage
|
||||
|
||||
```yml
|
||||
jobs:
|
||||
smoke-test:
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
## There are other service requirements for Magento, but this is just for the explanation of this service
|
||||
nginx:
|
||||
image: nginx:1.27-alpine
|
||||
ports: ["80:80"]
|
||||
volumes:
|
||||
- ${{ github.workspace }}:/var/www/html
|
||||
options: --health-cmd "nginx -t" --health-interval=10s --health-retries=3
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: graycoreio/github-actions-magento2/setup-magento@main
|
||||
id: setup-magento
|
||||
with:
|
||||
mode: store
|
||||
- run: composer install
|
||||
working-directory: ${{ steps.setup-magento.outputs.path }}
|
||||
- uses: graycoreio/github-actions-magento2/setup-install@main
|
||||
with:
|
||||
services: ${{ toJSON(matrix.services) }}
|
||||
path: ${{ steps.setup-magento.outputs.path }}
|
||||
container_id: ${{ job.services['php-fpm'].id }}
|
||||
- uses: graycoreio/github-actions-magento2/configure-service-nginx@main
|
||||
with:
|
||||
container_id: ${{ job.services.nginx.id }}
|
||||
magento_path: ${{ inputs.path }}
|
||||
- uses: graycoreio/github-actions-magento2/smoke-test@main
|
||||
with:
|
||||
kind: page
|
||||
```
|
||||
@@ -0,0 +1,67 @@
|
||||
name: "Configure nginx service container"
|
||||
author: "Graycore"
|
||||
description: "Pushes a Magento-aware nginx config into an already-running nginx service container, restarts it, and waits for the container healthcheck to pass."
|
||||
|
||||
inputs:
|
||||
container_id:
|
||||
description: "The ID of the running nginx service container. Pass the value of `job.services.nginx.id` (replace `nginx` with whatever you named the service in `services:`)."
|
||||
required: true
|
||||
magento_path:
|
||||
description: "Path to the Magento store, relative to the GitHub workspace. The workspace is mounted at /var/www/html in both the nginx and php-fpm service containers, so this is combined with that prefix to compute MAGE_ROOT."
|
||||
required: false
|
||||
default: "."
|
||||
health_timeout_seconds:
|
||||
description: "How long to wait for nginx to become healthy after the config is pushed and the container is restarted."
|
||||
required: false
|
||||
default: "10"
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Push nginx config
|
||||
shell: bash
|
||||
env:
|
||||
NGINX_CID: ${{ inputs.container_id }}
|
||||
ACTION_PATH: ${{ github.action_path }}
|
||||
MAGENTO_PATH: ${{ inputs.magento_path }}
|
||||
run: |
|
||||
case "$MAGENTO_PATH" in
|
||||
""|".") MAGE_ROOT="/var/www/html" ;;
|
||||
/*) MAGE_ROOT="$MAGENTO_PATH" ;;
|
||||
*) MAGE_ROOT="/var/www/html/$MAGENTO_PATH" ;;
|
||||
esac
|
||||
MAGE_ROOT="${MAGE_ROOT%/}"
|
||||
|
||||
sed "s|__MAGE_ROOT__|$MAGE_ROOT|g" "$ACTION_PATH/conf.d/default.conf" > /tmp/default.conf
|
||||
|
||||
docker cp /tmp/default.conf "$NGINX_CID:/etc/nginx/conf.d/default.conf"
|
||||
|
||||
echo "--- default.conf in container ---"
|
||||
docker exec "$NGINX_CID" cat /etc/nginx/conf.d/default.conf
|
||||
docker exec "$NGINX_CID" nginx -t
|
||||
|
||||
- name: Restart nginx and wait for healthy
|
||||
shell: bash
|
||||
env:
|
||||
NGINX_CID: ${{ inputs.container_id }}
|
||||
TIMEOUT: ${{ inputs.health_timeout_seconds }}
|
||||
run: |
|
||||
docker restart "$NGINX_CID"
|
||||
deadline=$(( $(date +%s) + TIMEOUT ))
|
||||
last=""
|
||||
while [ "$(date +%s)" -lt "$deadline" ]; do
|
||||
last=$(docker inspect --format '{{.State.Health.Status}}' "$NGINX_CID" 2>/dev/null || echo "")
|
||||
running=$(docker inspect --format '{{.State.Running}}' "$NGINX_CID" 2>/dev/null || echo "")
|
||||
echo "running=$running health=$last"
|
||||
if [ "$last" = "healthy" ]; then
|
||||
exit 0
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
echo "nginx did not reach healthy state within ${TIMEOUT}s (last=$last)"
|
||||
docker logs "$NGINX_CID" 2>&1 | tail -80 || true
|
||||
exit 1
|
||||
|
||||
branding:
|
||||
icon: "code"
|
||||
color: "green"
|
||||
@@ -0,0 +1,10 @@
|
||||
upstream fastcgi_backend {
|
||||
server php-fpm:9000;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80 default_server;
|
||||
server_name _;
|
||||
set $MAGE_ROOT __MAGE_ROOT__;
|
||||
include __MAGE_ROOT__/nginx[.]conf*;
|
||||
}
|
||||
@@ -18,6 +18,23 @@ See the [check-extension.yaml](../../.github/workflows/check-extension.yaml)
|
||||
|
||||
The Magento matrix format outlined by the [supported versions action.](https://github.com/graycoreio/github-actions-magento2/tree/main/supported-version/supported.json)
|
||||
|
||||
## Configuration
|
||||
|
||||
Each check can be toggled on/off through an optional `.github/check-extension.json` file in the repo that calls this workflow.
|
||||
|
||||
You can learn more about this file here in the [`resolve-check-config` action.](../../resolve-check-config/README.md):
|
||||
|
||||
Reference the published JSON Schema with `$schema` to get autocompletion and inline validation in editors that support it:
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/graycoreio/github-actions-magento2/main/resolve-check-config/check-extension.schema.json",
|
||||
"jobs": {
|
||||
"integration_test": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```yml
|
||||
@@ -38,12 +55,12 @@ jobs:
|
||||
matrix: ${{ steps.supported-version.outputs.matrix }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: graycoreio/github-actions-magento2/supported-version@v8.0.0 # x-release-please-version
|
||||
- uses: graycoreio/github-actions-magento2/supported-version@v8.2.0 # x-release-please-version
|
||||
id: supported-version
|
||||
- run: echo ${{ steps.supported-version.outputs.matrix }}
|
||||
check-extension:
|
||||
needs: compute_matrix
|
||||
uses: graycoreio/github-actions-magento2/.github/workflows/check-extension.yaml@v8.0.0 # x-release-please-version
|
||||
uses: graycoreio/github-actions-magento2/.github/workflows/check-extension.yaml@v8.2.0 # x-release-please-version
|
||||
with:
|
||||
matrix: ${{ needs.compute_matrix.outputs.matrix }}
|
||||
```
|
||||
|
||||
@@ -24,6 +24,24 @@ See the [check-store.yaml](../../.github/workflows/check-store.yaml)
|
||||
|
||||
- **Unit Tests** — runs PHPUnit against custom code in `app/code`. Skipped automatically if no test files are found.
|
||||
- **Coding Standard** — runs the Magento Coding Standard against `app/code`. Uses your `phpcs.xml` (or `.phpcs.xml`, `phpcs.xml.dist`, `.phpcs.xml.dist`) if one exists, otherwise a sensible default is generated.
|
||||
- **Smoke Test** — boots your store against the supported-version service set (mysql, search, queue, cache, nginx, php-fpm) and runs the smoke probes against it.
|
||||
|
||||
## Configuration
|
||||
|
||||
Each check can be toggled on/off through an optional `.github/check-store.json` file in the repo that calls this workflow.
|
||||
|
||||
You can learn more about this file here in the [`resolve-check-config` action.](../../resolve-check-config/README.md):
|
||||
|
||||
Reference the published JSON Schema with `$schema` to get autocompletion and inline validation in editors that support it:
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/graycoreio/github-actions-magento2/main/resolve-check-config/check-store.schema.json",
|
||||
"jobs": {
|
||||
"coding-standard": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -40,7 +58,7 @@ on:
|
||||
|
||||
jobs:
|
||||
check-store:
|
||||
uses: graycoreio/github-actions-magento2/.github/workflows/check-store.yaml@v8.0.0 # x-release-please-version
|
||||
uses: graycoreio/github-actions-magento2/.github/workflows/check-store.yaml@v8.2.0 # x-release-please-version
|
||||
secrets:
|
||||
composer_auth: ${{ secrets.COMPOSER_AUTH }}
|
||||
```
|
||||
@@ -52,7 +70,7 @@ If your pipeline builds or prepares the store in a prior job and uploads it as a
|
||||
```yml
|
||||
jobs:
|
||||
check-store:
|
||||
uses: graycoreio/github-actions-magento2/.github/workflows/check-store.yaml@v8.0.0 # x-release-please-version
|
||||
uses: graycoreio/github-actions-magento2/.github/workflows/check-store.yaml@v8.2.0 # x-release-please-version
|
||||
secrets:
|
||||
composer_auth: ${{ secrets.COMPOSER_AUTH }}
|
||||
```
|
||||
|
||||
@@ -50,13 +50,13 @@ jobs:
|
||||
matrix: ${{ steps.supported-version.outputs.matrix }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: graycoreio/github-actions-magento2/supported-version@v8.0.0 # x-release-please-version
|
||||
- uses: graycoreio/github-actions-magento2/supported-version@v8.2.0 # x-release-please-version
|
||||
with:
|
||||
include_services: true
|
||||
id: supported-version
|
||||
integration-workflow:
|
||||
needs: compute_matrix
|
||||
uses: graycoreio/github-actions-magento2/.github/workflows/integration.yaml@v8.0.0 # x-release-please-version
|
||||
uses: graycoreio/github-actions-magento2/.github/workflows/integration.yaml@v8.2.0 # x-release-please-version
|
||||
with:
|
||||
package_name: my-vendor/package
|
||||
matrix: ${{ needs.compute_matrix.outputs.matrix }}
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: graycoreio/github-actions-magento2/fix-magento-install@v8.0.0 # x-release-please-version
|
||||
- uses: graycoreio/github-actions-magento2/fix-magento-install@v8.2.0 # x-release-please-version
|
||||
with:
|
||||
magento_directory: path/to/magento
|
||||
```
|
||||
|
||||
@@ -9,7 +9,7 @@ inputs:
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- uses: graycoreio/github-actions-magento2/get-magento-version@v8.0.0
|
||||
- uses: graycoreio/github-actions-magento2/get-magento-version@main
|
||||
id: init-magento-get-magento-version
|
||||
with:
|
||||
working-directory: ${{ inputs.magento_directory }}
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
name: A job to compute an installed Composer version.
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: graycoreio/github-actions-magento2/get-composer-version@v8.0.0 # x-release-please-version
|
||||
- uses: graycoreio/github-actions-magento2/get-composer-version@v8.2.0 # x-release-please-version
|
||||
id: get-composer-version
|
||||
- run: echo version ${{ steps.get-composer-version.outputs.version }}
|
||||
shell: bash
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
name: A job to compute an installed Magento version.
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: graycoreio/github-actions-magento2/get-magento-version@v8.0.0 # x-release-please-version
|
||||
- uses: graycoreio/github-actions-magento2/get-magento-version@v8.2.0 # x-release-please-version
|
||||
id: get-magento-version
|
||||
- run: echo version ${{ steps.get-magento-version.outputs.version }}
|
||||
shell: bash
|
||||
|
||||
Generated
+179
-303
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@graycoreio/github-actions-magento2",
|
||||
"version": "8.0.0",
|
||||
"version": "8.2.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@graycoreio/github-actions-magento2",
|
||||
"version": "8.0.0",
|
||||
"version": "8.2.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.11.1",
|
||||
@@ -18,7 +18,7 @@
|
||||
"@typescript-eslint/eslint-plugin": "^8.49.0",
|
||||
"@typescript-eslint/parser": "^8.49.0",
|
||||
"esbuild": "^0.25.12",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint": "^10.4.0",
|
||||
"jest": "^29.5.0",
|
||||
"ts-jest": "^29.4.6",
|
||||
"typescript": "^5.9.3"
|
||||
@@ -105,7 +105,6 @@
|
||||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.5",
|
||||
@@ -1063,163 +1062,107 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/config-array": {
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz",
|
||||
"integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==",
|
||||
"version": "0.23.5",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz",
|
||||
"integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@eslint/object-schema": "^2.1.7",
|
||||
"@eslint/object-schema": "^3.0.5",
|
||||
"debug": "^4.3.1",
|
||||
"minimatch": "^3.1.2"
|
||||
"minimatch": "^10.2.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/config-array/node_modules/balanced-match": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/config-array/node_modules/brace-expansion": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"version": "5.0.6",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
|
||||
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
"balanced-match": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/config-array/node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"version": "10.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
|
||||
"integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
"brace-expansion": "^5.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
"node": "18 || 20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/config-helpers": {
|
||||
"version": "0.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz",
|
||||
"integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==",
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz",
|
||||
"integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@eslint/core": "^0.17.0"
|
||||
"@eslint/core": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/core": {
|
||||
"version": "0.17.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz",
|
||||
"integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==",
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz",
|
||||
"integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@types/json-schema": "^7.0.15"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/eslintrc": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz",
|
||||
"integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ajv": "^6.12.4",
|
||||
"debug": "^4.3.2",
|
||||
"espree": "^10.0.1",
|
||||
"globals": "^14.0.0",
|
||||
"ignore": "^5.2.0",
|
||||
"import-fresh": "^3.2.1",
|
||||
"js-yaml": "^4.1.1",
|
||||
"minimatch": "^3.1.2",
|
||||
"strip-json-comments": "^3.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/eslintrc/node_modules/ignore": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/eslintrc/node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/js": {
|
||||
"version": "9.39.2",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz",
|
||||
"integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://eslint.org/donate"
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/object-schema": {
|
||||
"version": "2.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz",
|
||||
"integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==",
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz",
|
||||
"integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/plugin-kit": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz",
|
||||
"integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==",
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz",
|
||||
"integrity": "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@eslint/core": "^0.17.0",
|
||||
"@eslint/core": "^1.2.1",
|
||||
"levn": "^0.4.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
}
|
||||
},
|
||||
"node_modules/@fastify/busboy": {
|
||||
@@ -1814,6 +1757,13 @@
|
||||
"@babel/types": "^7.28.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/esrecurse": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz",
|
||||
"integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
@@ -1939,13 +1889,61 @@
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.49.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.49.0.tgz",
|
||||
"integrity": "sha512-KTExJfQ+svY8I10P4HdxKzWsvtVnsuCifU5MvXrRwoP2KOlNZ9ADNEWWsQTJgMxLzS5VLQKDjkCT/YzgsnqmZg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.49.0",
|
||||
"@typescript-eslint/typescript-estree": "8.49.0",
|
||||
"@typescript-eslint/utils": "8.49.0",
|
||||
"debug": "^4.3.4",
|
||||
"ts-api-utils": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.49.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.49.0.tgz",
|
||||
"integrity": "sha512-N3W7rJw7Rw+z1tRsHZbK395TWSYvufBXumYtEGzypgMUthlg0/hmCImeA8hgO2d2G4pd7ftpxxul2J8OdtdaFA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.7.0",
|
||||
"@typescript-eslint/scope-manager": "8.49.0",
|
||||
"@typescript-eslint/types": "8.49.0",
|
||||
"@typescript-eslint/typescript-estree": "8.49.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.49.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.49.0.tgz",
|
||||
"integrity": "sha512-N9lBGA9o9aqb1hVMc9hzySbhKibHmB+N3IpoShyV6HyQYRGIhlrO5rQgttypi+yEeKsKI4idxC8Jw6gXKD4THA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.49.0",
|
||||
"@typescript-eslint/types": "8.49.0",
|
||||
@@ -2022,31 +2020,6 @@
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.49.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.49.0.tgz",
|
||||
"integrity": "sha512-KTExJfQ+svY8I10P4HdxKzWsvtVnsuCifU5MvXrRwoP2KOlNZ9ADNEWWsQTJgMxLzS5VLQKDjkCT/YzgsnqmZg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.49.0",
|
||||
"@typescript-eslint/typescript-estree": "8.49.0",
|
||||
"@typescript-eslint/utils": "8.49.0",
|
||||
"debug": "^4.3.4",
|
||||
"ts-api-utils": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.49.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.49.0.tgz",
|
||||
@@ -2089,30 +2062,6 @@
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.49.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.49.0.tgz",
|
||||
"integrity": "sha512-N3W7rJw7Rw+z1tRsHZbK395TWSYvufBXumYtEGzypgMUthlg0/hmCImeA8hgO2d2G4pd7ftpxxul2J8OdtdaFA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.7.0",
|
||||
"@typescript-eslint/scope-manager": "8.49.0",
|
||||
"@typescript-eslint/types": "8.49.0",
|
||||
"@typescript-eslint/typescript-estree": "8.49.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.49.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.49.0.tgz",
|
||||
@@ -2145,12 +2094,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.15.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"version": "8.16.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
|
||||
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -2169,9 +2117,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ajv": {
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"version": "6.15.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
|
||||
"integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2241,13 +2189,6 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/argparse": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||
"dev": true,
|
||||
"license": "Python-2.0"
|
||||
},
|
||||
"node_modules/babel-jest": {
|
||||
"version": "29.7.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz",
|
||||
@@ -2434,7 +2375,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
@@ -2847,34 +2787,30 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint": {
|
||||
"version": "9.39.2",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz",
|
||||
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
||||
"version": "10.4.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-10.4.0.tgz",
|
||||
"integrity": "sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
"@eslint/config-array": "^0.21.1",
|
||||
"@eslint/config-helpers": "^0.4.2",
|
||||
"@eslint/core": "^0.17.0",
|
||||
"@eslint/eslintrc": "^3.3.1",
|
||||
"@eslint/js": "9.39.2",
|
||||
"@eslint/plugin-kit": "^0.4.1",
|
||||
"@eslint-community/regexpp": "^4.12.2",
|
||||
"@eslint/config-array": "^0.23.5",
|
||||
"@eslint/config-helpers": "^0.6.0",
|
||||
"@eslint/core": "^1.2.1",
|
||||
"@eslint/plugin-kit": "^0.7.1",
|
||||
"@humanfs/node": "^0.16.6",
|
||||
"@humanwhocodes/module-importer": "^1.0.1",
|
||||
"@humanwhocodes/retry": "^0.4.2",
|
||||
"@types/estree": "^1.0.6",
|
||||
"ajv": "^6.12.4",
|
||||
"chalk": "^4.0.0",
|
||||
"ajv": "^6.14.0",
|
||||
"cross-spawn": "^7.0.6",
|
||||
"debug": "^4.3.2",
|
||||
"escape-string-regexp": "^4.0.0",
|
||||
"eslint-scope": "^8.4.0",
|
||||
"eslint-visitor-keys": "^4.2.1",
|
||||
"espree": "^10.4.0",
|
||||
"esquery": "^1.5.0",
|
||||
"eslint-scope": "^9.1.2",
|
||||
"eslint-visitor-keys": "^5.0.1",
|
||||
"espree": "^11.2.0",
|
||||
"esquery": "^1.7.0",
|
||||
"esutils": "^2.0.2",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"file-entry-cache": "^8.0.0",
|
||||
@@ -2884,8 +2820,7 @@
|
||||
"imurmurhash": "^0.1.4",
|
||||
"is-glob": "^4.0.0",
|
||||
"json-stable-stringify-without-jsonify": "^1.0.1",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"minimatch": "^3.1.2",
|
||||
"minimatch": "^10.2.4",
|
||||
"natural-compare": "^1.4.0",
|
||||
"optionator": "^0.9.3"
|
||||
},
|
||||
@@ -2893,7 +2828,7 @@
|
||||
"eslint": "bin/eslint.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://eslint.org/donate"
|
||||
@@ -2908,17 +2843,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-scope": {
|
||||
"version": "8.4.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
|
||||
"integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
|
||||
"version": "9.1.2",
|
||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz",
|
||||
"integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"@types/esrecurse": "^4.3.1",
|
||||
"@types/estree": "^1.0.8",
|
||||
"esrecurse": "^4.3.0",
|
||||
"estraverse": "^5.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint"
|
||||
@@ -2937,25 +2874,37 @@
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint/node_modules/balanced-match": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint/node_modules/brace-expansion": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"version": "5.0.6",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
|
||||
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
"balanced-match": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint/node_modules/eslint-visitor-keys": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
|
||||
"integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
|
||||
"integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint"
|
||||
@@ -2972,44 +2921,47 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint/node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"version": "10.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
|
||||
"integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
"brace-expansion": "^5.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
"node": "18 || 20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/espree": {
|
||||
"version": "10.4.0",
|
||||
"resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
|
||||
"integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
|
||||
"version": "11.2.0",
|
||||
"resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz",
|
||||
"integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"acorn": "^8.15.0",
|
||||
"acorn": "^8.16.0",
|
||||
"acorn-jsx": "^5.3.2",
|
||||
"eslint-visitor-keys": "^4.2.1"
|
||||
"eslint-visitor-keys": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/espree/node_modules/eslint-visitor-keys": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
|
||||
"integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
|
||||
"integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint"
|
||||
@@ -3030,9 +2982,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esquery": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
|
||||
"integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
|
||||
"integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
@@ -3354,19 +3306,6 @@
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/globals": {
|
||||
"version": "14.0.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
|
||||
"integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/graceful-fs": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
@@ -3446,23 +3385,6 @@
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/import-fresh": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
||||
"integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"parent-module": "^1.0.0",
|
||||
"resolve-from": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/import-local": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz",
|
||||
@@ -3685,7 +3607,6 @@
|
||||
"integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jest/core": "^29.7.0",
|
||||
"@jest/types": "^29.6.3",
|
||||
@@ -4270,19 +4191,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"js-yaml": "bin/js-yaml.js"
|
||||
}
|
||||
},
|
||||
"node_modules/jsesc": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
|
||||
@@ -4411,13 +4319,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.merge": {
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||
@@ -4662,19 +4563,6 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/parent-module": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
||||
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"callsites": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/parse-json": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
|
||||
@@ -4970,16 +4858,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-from": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
||||
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve.exports": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz",
|
||||
@@ -5282,7 +5160,6 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -5450,7 +5327,6 @@
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@graycoreio/github-actions-magento2",
|
||||
"version": "8.0.0",
|
||||
"version": "8.2.0",
|
||||
"description": "Github Actions for Magento 2",
|
||||
"scripts": {
|
||||
"test": "cd supported-version && npm run test && cd - && cd setup-install && npm run test && cd -",
|
||||
@@ -27,7 +27,7 @@
|
||||
"@typescript-eslint/eslint-plugin": "^8.49.0",
|
||||
"@typescript-eslint/parser": "^8.49.0",
|
||||
"esbuild": "^0.25.12",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint": "^10.4.0",
|
||||
"jest": "^29.5.0",
|
||||
"ts-jest": "^29.4.6",
|
||||
"typescript": "^5.9.3"
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"bump-minor-pre-major": true,
|
||||
"bump-patch-for-minor-pre-major": true,
|
||||
"draft-pull-request": true,
|
||||
"prerelease": false,
|
||||
"include-component-in-tag": false,
|
||||
"include-v-in-tag": true,
|
||||
"pull-request-title-pattern": "chore: release ${version}",
|
||||
"packages": {
|
||||
".": {
|
||||
"release-type": "node",
|
||||
"versioning": "prerelease",
|
||||
"extra-files": [
|
||||
{ "type": "generic", "path": "*/README.md", "glob": true },
|
||||
{ "type": "generic", "path": "docs/workflows/*.md", "glob": true }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
# "Resolve Check Config" Action
|
||||
|
||||
Reads `.github/check-<kind>.json` (or a path you specify), validates job names against the known list for the selected workflow kind, and emits a per-job filtered version of the `supported-version` matrix. Each job in the output carries an `enabled` flag and its own `matrix`, where every entry's `services` map has been narrowed to the tiers that job actually needs. Consumers gate each job with `fromJSON(...)['<job>'].enabled != false` and feed `fromJSON(...)['<job>'].matrix` into `strategy.matrix`.
|
||||
|
||||
A missing config file is fine — every known job is emitted with its default tier list.
|
||||
|
||||
## Inputs
|
||||
|
||||
| Input | Description | Required | Default |
|
||||
|---------------|--------------------------------------------------------------------------------------------------------------------------------------------------------|----------|-------------------------------|
|
||||
| `kind` | Which reusable workflow this config belongs to: `store` or `extension`. Selects the default `config_path`, the known-job list, and the per-job tier defaults. | true | |
|
||||
| `matrix` | The matrix JSON emitted by the `supported-version` action. Each entry's `services` map is filtered per-job based on the resolved tier list. | true | |
|
||||
| `config_path` | Path to the check-config JSON file, relative to the runner workspace. | false | `.github/check-<kind>.json` |
|
||||
|
||||
## Usage
|
||||
|
||||
```yml
|
||||
jobs:
|
||||
compute_matrix:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
resolved: ${{ steps.resolve.outputs.resolved }}
|
||||
steps:
|
||||
- uses: graycoreio/github-actions-magento2/supported-version@v8.2.0 # x-release-please-version
|
||||
id: supported-version
|
||||
with:
|
||||
kind: currently-supported
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/resolve-check-config@v8.2.0 # x-release-please-version
|
||||
id: resolve
|
||||
with:
|
||||
kind: store
|
||||
matrix: ${{ steps.supported-version.outputs.matrix }}
|
||||
|
||||
smoke-test:
|
||||
runs-on: ${{ matrix.os }}
|
||||
needs: compute_matrix
|
||||
if: ${{ fromJSON(needs.compute_matrix.outputs.resolved)['smoke-test'].enabled != false }}
|
||||
services: ${{ matrix.services }}
|
||||
strategy:
|
||||
matrix: ${{ fromJSON(needs.compute_matrix.outputs.resolved)['smoke-test'].matrix }}
|
||||
steps:
|
||||
- run: echo "running with ${{ toJSON(matrix.services) }}"
|
||||
```
|
||||
|
||||
Example `.github/check-store.json` for opting out of a specific job:
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/graycoreio/github-actions-magento2/main/resolve-check-config/check-store.schema.json",
|
||||
"jobs": {
|
||||
"coding-standard": false
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,27 @@
|
||||
name: "Resolve check config"
|
||||
author: "Graycore"
|
||||
description: "Reads .github/check-<kind>.json (or a path you specify), validates job names against the known list for that workflow kind, and emits a per-job filtered version of the supported-version matrix. Missing config file is treated as 'all jobs enabled with their default tier list.'"
|
||||
|
||||
inputs:
|
||||
kind:
|
||||
required: true
|
||||
description: "Which reusable workflow this config belongs to: `store` or `extension`. Selects the default `config_path`, the known-job list used for validation, and the per-job default tier list."
|
||||
matrix:
|
||||
required: true
|
||||
description: "The matrix JSON emitted by the `supported-version` action. Each entry's `services` map is filtered per-job based on the resolved tier list and embedded in the per-job `matrix` output."
|
||||
config_path:
|
||||
required: false
|
||||
default: ""
|
||||
description: "Path to the check-config JSON file, relative to the runner workspace. Defaults to `.github/check-<kind>.json`. Missing file is fine — every known job is emitted with its default tier list."
|
||||
|
||||
outputs:
|
||||
resolved:
|
||||
description: "The per-job resolved configuration as a JSON object. Each top-level key is a known job name for the selected kind; values are objects with `enabled` (boolean) and `matrix` (a copy of the supported-version matrix where every entry's `services` is filtered to the tiers the job needs). Consumers default-enable omitted jobs via `fromJSON(...)['<job>'].enabled != false` and use `fromJSON(...)['<job>'].matrix` for `strategy.matrix`."
|
||||
|
||||
runs:
|
||||
using: "node24"
|
||||
main: "dist/index.js"
|
||||
|
||||
branding:
|
||||
icon: "check-square"
|
||||
color: "green"
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://raw.githubusercontent.com/graycoreio/github-actions-magento2/main/resolve-check-config/check-extension.schema.json",
|
||||
"title": "graycoreio check-extension config",
|
||||
"description": "Configuration consumed by the check-extension reusable workflow. Per-job toggles and settings live under `jobs`. Top-level remains open for future global keys.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"jobs": {
|
||||
"type": "object",
|
||||
"description": "Per-job configuration. Each key is a job name declared by check-extension; unknown keys are rejected.",
|
||||
"properties": {
|
||||
"unit-test-extension": { "$ref": "#/$defs/jobConfig" },
|
||||
"compile-extension": { "$ref": "#/$defs/jobConfig" },
|
||||
"coding-standard": { "$ref": "#/$defs/jobConfig" },
|
||||
"integration_test": { "$ref": "#/$defs/jobConfig" }
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": true,
|
||||
"$defs": {
|
||||
"jobConfig": {
|
||||
"description": "How a single job should be configured. Boolean form is shorthand for { enabled: <bool> }; object form allows extra per-job keys.",
|
||||
"oneOf": [
|
||||
{ "type": "boolean" },
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Whether the job should run. Defaults to true when the key is present.",
|
||||
"default": true
|
||||
},
|
||||
"services": {
|
||||
"type": "array",
|
||||
"description": "Tier names this job needs as GitHub Actions service containers. mysql is always implicit.",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": ["search", "queue", "cache", "web"]
|
||||
},
|
||||
"uniqueItems": true
|
||||
}
|
||||
},
|
||||
"additionalProperties": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://raw.githubusercontent.com/graycoreio/github-actions-magento2/main/resolve-check-config/check-store.schema.json",
|
||||
"title": "graycoreio check-store config",
|
||||
"description": "Configuration consumed by the check-store reusable workflow. Per-job toggles and settings live under `jobs`. Top-level remains open for future global keys.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"jobs": {
|
||||
"type": "object",
|
||||
"description": "Per-job configuration. Each key is a job name declared by check-store; unknown keys are rejected.",
|
||||
"properties": {
|
||||
"unit-test": { "$ref": "#/$defs/jobConfig" },
|
||||
"coding-standard": { "$ref": "#/$defs/jobConfig" },
|
||||
"smoke-test": { "$ref": "#/$defs/jobConfig" }
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": true,
|
||||
"$defs": {
|
||||
"jobConfig": {
|
||||
"description": "How a single job should be configured. Boolean form is shorthand for { enabled: <bool> }; object form allows extra per-job keys.",
|
||||
"oneOf": [
|
||||
{ "type": "boolean" },
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Whether the job should run. Defaults to true when the key is present.",
|
||||
"default": true
|
||||
}
|
||||
},
|
||||
"additionalProperties": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+69
File diff suppressed because one or more lines are too long
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"$schema": "../check-extension.schema.json",
|
||||
"jobs": {
|
||||
"unit-test-extension": true,
|
||||
"compile-extension": true,
|
||||
"coding-standard": true,
|
||||
"integration_test": {
|
||||
"services": ["search", "cache"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"$schema": "../check-extension.schema.json",
|
||||
"jobs": {
|
||||
"integration_test": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"$schema": "../check-store.schema.json",
|
||||
"jobs": {
|
||||
"unit-test": true,
|
||||
"coding-standard": true,
|
||||
"integration-test": {
|
||||
"services": ["search", "queue", "cache"]
|
||||
},
|
||||
"smoke-test": {
|
||||
"services": ["search", "queue", "cache", "nginx", "php-fpm"],
|
||||
"probes": ["page", "graphql"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"$schema": "../check-store.schema.json",
|
||||
"jobs": {
|
||||
"smoke-test": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
module.exports = {
|
||||
clearMocks: true,
|
||||
moduleFileExtensions: ['js', 'ts'],
|
||||
testMatch: ['**/*.spec.ts'],
|
||||
transform: {
|
||||
'^.+\\.ts$': 'ts-jest'
|
||||
},
|
||||
verbose: true
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "@graycoreio/github-actions-magento2-resolve-check-config",
|
||||
"version": "1.0.0",
|
||||
"description": "A Github Action that reads .github/<workflow>.json, validates it against the known job list, and emits resolved per-job configuration.",
|
||||
"main": "index.js",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "npx esbuild --outfile=dist/index.js --platform=node --bundle --minify src/index.ts",
|
||||
"test": "jest"
|
||||
},
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@actions/core": "0.0.0-PLACEHOLDER"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "0.0.0-PLACEHOLDER",
|
||||
"jest": "0.0.0-PLACEHOLDER",
|
||||
"ts-jest": "0.0.0-PLACEHOLDER"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import * as core from '@actions/core';
|
||||
import * as fs from 'fs';
|
||||
import * as nodePath from 'path';
|
||||
import { assertKind } from './kind';
|
||||
import { parseMatrixInput, parseRawConfig } from './parse';
|
||||
import { resolveConfig } from './resolve';
|
||||
|
||||
export const run = async (): Promise<void> => {
|
||||
try {
|
||||
const kind = assertKind(core.getInput('kind', { required: true }));
|
||||
const matrix = parseMatrixInput(core.getInput('matrix', { required: true }));
|
||||
const configPath = core.getInput('config_path') || `.github/check-${kind}.json`;
|
||||
const workspace = process.env.GITHUB_WORKSPACE || process.cwd();
|
||||
const absolute = nodePath.resolve(workspace, configPath);
|
||||
|
||||
let raw = {};
|
||||
if (fs.existsSync(absolute)) {
|
||||
const text = fs.readFileSync(absolute, 'utf-8');
|
||||
raw = parseRawConfig(text);
|
||||
core.info(`resolve-check-config: read ${absolute}`);
|
||||
} else {
|
||||
core.info(`resolve-check-config: ${absolute} not found — emitting defaults for every known job`);
|
||||
}
|
||||
|
||||
const resolved = resolveConfig(raw, kind, matrix);
|
||||
|
||||
core.setOutput('resolved', JSON.stringify(resolved));
|
||||
} catch (error) {
|
||||
core.setFailed((error as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -0,0 +1,26 @@
|
||||
import { assertKind, isKind } from './kind';
|
||||
|
||||
describe('isKind / assertKind', () => {
|
||||
it('accepts "store"', () => {
|
||||
expect(isKind('store')).toBe(true);
|
||||
expect(assertKind('store')).toBe('store');
|
||||
});
|
||||
|
||||
it('accepts "extension"', () => {
|
||||
expect(isKind('extension')).toBe(true);
|
||||
expect(assertKind('extension')).toBe('extension');
|
||||
});
|
||||
|
||||
it('rejects other strings', () => {
|
||||
expect(isKind('taco')).toBe(false);
|
||||
expect(() => assertKind('taco')).toThrowError(/`kind` must be 'store' or 'extension'/);
|
||||
});
|
||||
|
||||
it('rejects empty input', () => {
|
||||
expect(() => assertKind('')).toThrowError(/`kind` must be 'store' or 'extension'/);
|
||||
});
|
||||
|
||||
it('rejects non-string input', () => {
|
||||
expect(() => assertKind(undefined)).toThrowError(/`kind` must be 'store' or 'extension'/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Kind } from './types';
|
||||
|
||||
/**
|
||||
* Type guard for the `kind` input. Use this when you have an
|
||||
* `unknown` value (e.g. from `core.getInput`) and want to narrow it
|
||||
* without throwing.
|
||||
*/
|
||||
export const isKind = (value: unknown): value is Kind =>
|
||||
value === 'store' || value === 'extension';
|
||||
|
||||
/**
|
||||
* Narrows an `unknown` (typically the raw action input) to `Kind` or
|
||||
* throws a user-facing error naming the accepted values. Prefer this
|
||||
* at the action boundary so a bad `kind` fails fast with a clear
|
||||
* message rather than later as an obscure dispatch miss.
|
||||
*/
|
||||
export const assertKind = (value: unknown): Kind => {
|
||||
if (!isKind(value)) {
|
||||
throw new Error(`check-config: \`kind\` must be 'store' or 'extension' (got ${JSON.stringify(value)})`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import { EXTENSION_JOBS, KNOWN_JOBS_EXTENSION, resolveExtensionConfig } from './extension';
|
||||
import { Matrix } from '../types';
|
||||
|
||||
const FULL_SERVICES = {
|
||||
mysql: { image: 'mysql:8' },
|
||||
opensearch: { image: 'opensearchproject/opensearch:2' },
|
||||
rabbitmq: { image: 'rabbitmq:3' },
|
||||
valkey: { image: 'valkey:8' },
|
||||
nginx: { image: 'nginx:1.27' },
|
||||
'php-fpm': { image: 'php:8.3-fpm' },
|
||||
};
|
||||
|
||||
const MATRIX: Matrix = {
|
||||
include: [{ php: '8.3', services: { ...FULL_SERVICES } }],
|
||||
};
|
||||
|
||||
describe('EXTENSION_JOBS', () => {
|
||||
it('declares the check-extension jobs', () => {
|
||||
expect(Object.keys(EXTENSION_JOBS).sort()).toEqual([
|
||||
'coding-standard',
|
||||
'compile-extension',
|
||||
'integration_test',
|
||||
'unit-test-extension',
|
||||
]);
|
||||
});
|
||||
|
||||
it('keeps KNOWN_JOBS_EXTENSION in sync with the map keys', () => {
|
||||
expect([...KNOWN_JOBS_EXTENSION].sort()).toEqual(Object.keys(EXTENSION_JOBS).sort());
|
||||
});
|
||||
|
||||
it('declares integration_test required tiers (no web)', () => {
|
||||
expect(EXTENSION_JOBS['integration_test'].services).toEqual([]);
|
||||
expect([...EXTENSION_JOBS['integration_test'].requiredServices!].sort()).toEqual([
|
||||
'cache',
|
||||
'db',
|
||||
'queue',
|
||||
'search',
|
||||
]);
|
||||
});
|
||||
|
||||
it('leaves the non-service jobs with empty defaults', () => {
|
||||
for (const name of ['unit-test-extension', 'compile-extension', 'coding-standard']) {
|
||||
expect(EXTENSION_JOBS[name].services).toEqual([]);
|
||||
expect(EXTENSION_JOBS[name].requiredServices).toBeUndefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveExtensionConfig', () => {
|
||||
it('emits every known job', () => {
|
||||
const resolved = resolveExtensionConfig({}, MATRIX);
|
||||
expect(Object.keys(resolved).sort()).toEqual([
|
||||
'coding-standard',
|
||||
'compile-extension',
|
||||
'integration_test',
|
||||
'unit-test-extension',
|
||||
]);
|
||||
});
|
||||
|
||||
it('emits services={} for the non-service jobs', () => {
|
||||
const resolved = resolveExtensionConfig({}, MATRIX);
|
||||
for (const name of ['unit-test-extension', 'compile-extension', 'coding-standard']) {
|
||||
expect(resolved[name].matrix.include[0].services).toEqual({});
|
||||
}
|
||||
});
|
||||
|
||||
it('integration_test includes mysql/search/queue/cache but NOT nginx/php-fpm', () => {
|
||||
const resolved = resolveExtensionConfig({}, MATRIX);
|
||||
expect(Object.keys(resolved['integration_test'].matrix.include[0].services!).sort()).toEqual([
|
||||
'mysql',
|
||||
'opensearch',
|
||||
'rabbitmq',
|
||||
'valkey',
|
||||
]);
|
||||
});
|
||||
|
||||
it('keeps integration_test required tiers even when caller overrides services to []', () => {
|
||||
const resolved = resolveExtensionConfig(
|
||||
{ jobs: { integration_test: { services: [] } } },
|
||||
MATRIX,
|
||||
);
|
||||
expect(Object.keys(resolved['integration_test'].matrix.include[0].services!).sort()).toEqual([
|
||||
'mysql',
|
||||
'opensearch',
|
||||
'rabbitmq',
|
||||
'valkey',
|
||||
]);
|
||||
});
|
||||
|
||||
it('throws on a typo in the job name', () => {
|
||||
expect(() => resolveExtensionConfig({ jobs: { 'inteegration_test': false } }, MATRIX)).toThrowError(
|
||||
/unknown job "inteegration_test" for kind "extension"/
|
||||
);
|
||||
});
|
||||
|
||||
it('throws when a store-only job name is used', () => {
|
||||
expect(() => resolveExtensionConfig({ jobs: { 'smoke-test': false } }, MATRIX)).toThrowError(
|
||||
/unknown job "smoke-test" for kind "extension"/
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
import { resolveJobs } from '../parse';
|
||||
import { JobDefaults, Matrix, RawConfig, ResolvedConfig } from '../types';
|
||||
|
||||
/**
|
||||
* Per-job defaults for the `check-extension.yaml` reusable workflow.
|
||||
* Edit this map when a job is added, removed, or renamed in that
|
||||
* workflow — keys are validated against caller config and the values
|
||||
* supply the default tier list used when the caller doesn't override
|
||||
* `services` themselves.
|
||||
*/
|
||||
export const EXTENSION_JOBS: Record<string, JobDefaults> = {
|
||||
'unit-test-extension': { services: [] },
|
||||
'compile-extension': { services: [] },
|
||||
'coding-standard': { services: [] },
|
||||
'integration_test': {
|
||||
services: [],
|
||||
requiredServices: ['db', 'search', 'queue', 'cache'],
|
||||
},
|
||||
};
|
||||
|
||||
export const KNOWN_JOBS_EXTENSION: readonly string[] = Object.keys(EXTENSION_JOBS);
|
||||
|
||||
/**
|
||||
* Resolves a parsed config file + supported-version matrix against
|
||||
* the check-extension job list. Thin wrapper that binds the kind and
|
||||
* the per-job defaults so callers don't repeat the wiring.
|
||||
*/
|
||||
export const resolveExtensionConfig = (raw: RawConfig, matrix: Matrix): ResolvedConfig =>
|
||||
resolveJobs(raw, 'extension', EXTENSION_JOBS, matrix);
|
||||
@@ -0,0 +1,93 @@
|
||||
import { KNOWN_JOBS_STORE, resolveStoreConfig, STORE_JOBS } from './store';
|
||||
import { Matrix } from '../types';
|
||||
|
||||
const MATRIX: Matrix = {
|
||||
include: [{
|
||||
php: '8.3',
|
||||
services: {
|
||||
mysql: { image: 'mysql:8' },
|
||||
opensearch: { image: 'opensearchproject/opensearch:2' },
|
||||
rabbitmq: { image: 'rabbitmq:3' },
|
||||
valkey: { image: 'valkey:8' },
|
||||
nginx: { image: 'nginx:1.27' },
|
||||
'php-fpm': { image: 'php:8.3-fpm' },
|
||||
},
|
||||
}],
|
||||
};
|
||||
|
||||
describe('STORE_JOBS', () => {
|
||||
it('declares the check-store jobs', () => {
|
||||
expect(Object.keys(STORE_JOBS).sort()).toEqual(['coding-standard', 'smoke-test', 'unit-test']);
|
||||
});
|
||||
|
||||
it('declares smoke-test required tiers (end-user cannot toggle)', () => {
|
||||
expect(STORE_JOBS['smoke-test'].services).toEqual([]);
|
||||
expect([...STORE_JOBS['smoke-test'].requiredServices!].sort()).toEqual([
|
||||
'cache',
|
||||
'db',
|
||||
'queue',
|
||||
'search',
|
||||
'web',
|
||||
]);
|
||||
});
|
||||
|
||||
it('exposes empty service defaults for unit-test and coding-standard', () => {
|
||||
expect(STORE_JOBS['unit-test'].services).toEqual([]);
|
||||
expect(STORE_JOBS['coding-standard'].services).toEqual([]);
|
||||
});
|
||||
|
||||
it('keeps KNOWN_JOBS_STORE in sync with the map keys', () => {
|
||||
expect([...KNOWN_JOBS_STORE].sort()).toEqual(Object.keys(STORE_JOBS).sort());
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveStoreConfig', () => {
|
||||
it('emits every known job with default tier expansion, always including mysql for smoke-test', () => {
|
||||
const resolved = resolveStoreConfig({}, MATRIX);
|
||||
expect(Object.keys(resolved).sort()).toEqual(['coding-standard', 'smoke-test', 'unit-test']);
|
||||
expect(resolved['unit-test'].matrix.include[0].services).toEqual({});
|
||||
expect(Object.keys(resolved['smoke-test'].matrix.include[0].services!).sort()).toEqual([
|
||||
'mysql',
|
||||
'nginx',
|
||||
'opensearch',
|
||||
'php-fpm',
|
||||
'rabbitmq',
|
||||
'valkey',
|
||||
]);
|
||||
});
|
||||
|
||||
it('keeps every required service even when caller overrides smoke-test services to []', () => {
|
||||
const resolved = resolveStoreConfig(
|
||||
{ jobs: { 'smoke-test': { services: [] } } },
|
||||
MATRIX,
|
||||
);
|
||||
expect(Object.keys(resolved['smoke-test'].matrix.include[0].services!).sort()).toEqual([
|
||||
'mysql',
|
||||
'nginx',
|
||||
'opensearch',
|
||||
'php-fpm',
|
||||
'rabbitmq',
|
||||
'valkey',
|
||||
]);
|
||||
});
|
||||
|
||||
it('honors enabled=false for a job', () => {
|
||||
const resolved = resolveStoreConfig(
|
||||
{ jobs: { 'smoke-test': false } },
|
||||
MATRIX,
|
||||
);
|
||||
expect(resolved['smoke-test'].enabled).toBe(false);
|
||||
});
|
||||
|
||||
it('throws on a typo in the job name', () => {
|
||||
expect(() => resolveStoreConfig({ jobs: { 'smkoe-test': false } }, MATRIX)).toThrowError(
|
||||
/unknown job "smkoe-test" for kind "store"/
|
||||
);
|
||||
});
|
||||
|
||||
it('throws when an extension-only job name is used', () => {
|
||||
expect(() => resolveStoreConfig({ jobs: { 'unit-test-extension': false } }, MATRIX)).toThrowError(
|
||||
/unknown job "unit-test-extension" for kind "store"/
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
import { resolveJobs } from '../parse';
|
||||
import { JobDefaults, Matrix, RawConfig, ResolvedConfig } from '../types';
|
||||
|
||||
/**
|
||||
* Per-job defaults for the `check-store.yaml` reusable workflow.
|
||||
* Edit this map when a job is added, removed, or renamed in that
|
||||
* workflow — keys are validated against caller config and the values
|
||||
* supply the default tier list used when the caller doesn't override
|
||||
* `services` themselves.
|
||||
*/
|
||||
export const STORE_JOBS: Record<string, JobDefaults> = {
|
||||
'unit-test': { services: [] },
|
||||
'coding-standard': { services: [] },
|
||||
'smoke-test': {
|
||||
services: [],
|
||||
requiredServices: ['db', 'search', 'queue', 'cache', 'web'],
|
||||
},
|
||||
};
|
||||
|
||||
export const KNOWN_JOBS_STORE: readonly string[] = Object.keys(STORE_JOBS);
|
||||
|
||||
/**
|
||||
* Resolves a parsed config file + supported-version matrix against
|
||||
* the check-store job list. Thin wrapper that binds the kind and the
|
||||
* per-job defaults so callers don't repeat the wiring.
|
||||
*/
|
||||
export const resolveStoreConfig = (raw: RawConfig, matrix: Matrix): ResolvedConfig =>
|
||||
resolveJobs(raw, 'store', STORE_JOBS, matrix);
|
||||
@@ -0,0 +1,305 @@
|
||||
import {
|
||||
filterEntryServices,
|
||||
filterMatrixForJob,
|
||||
mergeRequiredTiers,
|
||||
normalizeJobEntry,
|
||||
parseMatrixInput,
|
||||
parseRawConfig,
|
||||
resolveJobs,
|
||||
} from './parse';
|
||||
import { JobDefaults, Matrix } from './types';
|
||||
|
||||
const FULL_SERVICES = {
|
||||
mysql: { image: 'mysql:8' },
|
||||
opensearch: { image: 'opensearchproject/opensearch:2' },
|
||||
rabbitmq: { image: 'rabbitmq:3' },
|
||||
valkey: { image: 'valkey:8' },
|
||||
nginx: { image: 'nginx:1.27' },
|
||||
'php-fpm': { image: 'php:8.3-fpm' },
|
||||
};
|
||||
|
||||
const MATRIX: Matrix = {
|
||||
include: [{ php: '8.3', services: { ...FULL_SERVICES } }],
|
||||
};
|
||||
|
||||
const noDefaults: JobDefaults = { services: [] };
|
||||
const smokeDefaults: JobDefaults = { services: ['search', 'queue', 'cache', 'web'] };
|
||||
|
||||
describe('normalizeJobEntry', () => {
|
||||
it('defaults enabled=true and uses the default tiers when entry is undefined', () => {
|
||||
expect(normalizeJobEntry('smoke-test', undefined, smokeDefaults)).toEqual({
|
||||
enabled: true,
|
||||
tiers: smokeDefaults.services,
|
||||
});
|
||||
});
|
||||
|
||||
it('treats true shorthand as enabled with defaults', () => {
|
||||
expect(normalizeJobEntry('smoke-test', true, smokeDefaults)).toEqual({
|
||||
enabled: true,
|
||||
tiers: smokeDefaults.services,
|
||||
});
|
||||
});
|
||||
|
||||
it('treats false shorthand as disabled with defaults', () => {
|
||||
expect(normalizeJobEntry('smoke-test', false, smokeDefaults)).toEqual({
|
||||
enabled: false,
|
||||
tiers: smokeDefaults.services,
|
||||
});
|
||||
});
|
||||
|
||||
it('empty object is enabled with defaults', () => {
|
||||
expect(normalizeJobEntry('smoke-test', {}, smokeDefaults)).toEqual({
|
||||
enabled: true,
|
||||
tiers: smokeDefaults.services,
|
||||
});
|
||||
});
|
||||
|
||||
it('preserves enabled when explicitly set', () => {
|
||||
expect(normalizeJobEntry('smoke-test', { enabled: false }, smokeDefaults)).toEqual({
|
||||
enabled: false,
|
||||
tiers: smokeDefaults.services,
|
||||
});
|
||||
});
|
||||
|
||||
it('overrides the default tiers when services is set', () => {
|
||||
expect(normalizeJobEntry('smoke-test', { services: ['cache', 'web'] }, smokeDefaults)).toEqual({
|
||||
enabled: true,
|
||||
tiers: ['cache', 'web'],
|
||||
});
|
||||
});
|
||||
|
||||
it('accepts an empty services array as "no services"', () => {
|
||||
expect(normalizeJobEntry('smoke-test', { services: [] }, smokeDefaults)).toEqual({
|
||||
enabled: true,
|
||||
tiers: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('throws when entry is a non-array primitive other than boolean', () => {
|
||||
expect(() => normalizeJobEntry('unit-test', 'true' as never, noDefaults)).toThrowError(
|
||||
/must be a boolean or an object/
|
||||
);
|
||||
});
|
||||
|
||||
it('throws when entry is an array', () => {
|
||||
expect(() => normalizeJobEntry('unit-test', [] as never, noDefaults)).toThrowError(/got array/);
|
||||
});
|
||||
|
||||
it('throws when services is not an array', () => {
|
||||
expect(() => normalizeJobEntry('smoke-test', { services: 'search' } as never, smokeDefaults)).toThrowError(
|
||||
/services must be an array of tier names/
|
||||
);
|
||||
});
|
||||
|
||||
it('throws when services contains an unknown tier', () => {
|
||||
expect(() => normalizeJobEntry('smoke-test', { services: ['llm'] } as never, smokeDefaults)).toThrowError(
|
||||
/services contains unknown tier "llm"/
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mergeRequiredTiers', () => {
|
||||
it('returns the input list when required is undefined', () => {
|
||||
expect(mergeRequiredTiers(['cache'], undefined)).toEqual(['cache']);
|
||||
});
|
||||
|
||||
it('returns the input list when required is empty', () => {
|
||||
expect(mergeRequiredTiers(['cache'], [])).toEqual(['cache']);
|
||||
});
|
||||
|
||||
it('prepends required tiers ahead of the input tiers', () => {
|
||||
expect(mergeRequiredTiers(['cache', 'web'], ['db'])).toEqual(['db', 'cache', 'web']);
|
||||
});
|
||||
|
||||
it('deduplicates when a required tier already appears in the input', () => {
|
||||
expect(mergeRequiredTiers(['db', 'cache'], ['db'])).toEqual(['db', 'cache']);
|
||||
});
|
||||
|
||||
it('deduplicates within required itself', () => {
|
||||
expect(mergeRequiredTiers(['cache'], ['db', 'db'])).toEqual(['db', 'cache']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('filterEntryServices', () => {
|
||||
it('returns services={} for an empty tier list', () => {
|
||||
const out = filterEntryServices({ php: '8.3', services: FULL_SERVICES }, []);
|
||||
expect(out.services).toEqual({});
|
||||
expect(out.php).toBe('8.3');
|
||||
});
|
||||
|
||||
it('keeps only services in the requested tiers', () => {
|
||||
const out = filterEntryServices({ php: '8.3', services: FULL_SERVICES }, ['cache', 'web']);
|
||||
expect(Object.keys(out.services!).sort()).toEqual(['nginx', 'php-fpm', 'valkey']);
|
||||
});
|
||||
|
||||
it('drops services that the matrix doesn\'t carry (elasticsearch absent)', () => {
|
||||
const out = filterEntryServices({ services: { opensearch: FULL_SERVICES.opensearch } }, ['search']);
|
||||
expect(Object.keys(out.services!)).toEqual(['opensearch']);
|
||||
});
|
||||
|
||||
it('emits services={} when the entry has no services map', () => {
|
||||
const out = filterEntryServices({ php: '8.3' }, ['cache']);
|
||||
expect(out.services).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('filterMatrixForJob', () => {
|
||||
it('preserves matrix shape, mapping every entry through filterEntryServices', () => {
|
||||
const out = filterMatrixForJob(MATRIX, ['queue']);
|
||||
expect(out.include).toHaveLength(1);
|
||||
expect(Object.keys(out.include[0].services!)).toEqual(['rabbitmq']);
|
||||
});
|
||||
|
||||
it('passes through unrelated top-level matrix keys', () => {
|
||||
const out = filterMatrixForJob({ ...MATRIX, magento: ['2.4.7'] } as Matrix, []);
|
||||
expect((out as Matrix).magento).toEqual(['2.4.7']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveJobs', () => {
|
||||
const jobs: Record<string, JobDefaults> = {
|
||||
'unit-test': noDefaults,
|
||||
'smoke-test': smokeDefaults,
|
||||
};
|
||||
|
||||
it('emits every known job, defaulted-enabled, when raw is empty', () => {
|
||||
const out = resolveJobs({}, 'store', jobs, MATRIX);
|
||||
expect(Object.keys(out).sort()).toEqual(['smoke-test', 'unit-test']);
|
||||
expect(out['unit-test'].enabled).toBe(true);
|
||||
expect(out['smoke-test'].enabled).toBe(true);
|
||||
});
|
||||
|
||||
it('emits services={} on entries for a no-default job', () => {
|
||||
const out = resolveJobs({}, 'store', jobs, MATRIX);
|
||||
expect(out['unit-test'].matrix.include[0].services).toEqual({});
|
||||
});
|
||||
|
||||
it('expands the smoke-test default tiers across the matrix entry', () => {
|
||||
const out = resolveJobs({}, 'store', jobs, MATRIX);
|
||||
expect(Object.keys(out['smoke-test'].matrix.include[0].services!).sort()).toEqual([
|
||||
'nginx',
|
||||
'opensearch',
|
||||
'php-fpm',
|
||||
'rabbitmq',
|
||||
'valkey',
|
||||
]);
|
||||
});
|
||||
|
||||
it('applies a caller-supplied services override', () => {
|
||||
const out = resolveJobs(
|
||||
{ jobs: { 'smoke-test': { services: ['cache'] } } },
|
||||
'store',
|
||||
jobs,
|
||||
MATRIX,
|
||||
);
|
||||
expect(Object.keys(out['smoke-test'].matrix.include[0].services!)).toEqual(['valkey']);
|
||||
});
|
||||
|
||||
it('always merges requiredServices into the matrix even when caller overrides services', () => {
|
||||
const withRequired: Record<string, JobDefaults> = {
|
||||
'smoke-test': { ...smokeDefaults, requiredServices: ['db'] },
|
||||
};
|
||||
const out = resolveJobs(
|
||||
{ jobs: { 'smoke-test': { services: ['cache'] } } },
|
||||
'store',
|
||||
withRequired,
|
||||
MATRIX,
|
||||
);
|
||||
expect(Object.keys(out['smoke-test'].matrix.include[0].services!).sort()).toEqual(['mysql', 'valkey']);
|
||||
});
|
||||
|
||||
it('keeps requiredServices even when caller overrides services to []', () => {
|
||||
const withRequired: Record<string, JobDefaults> = {
|
||||
'smoke-test': { ...smokeDefaults, requiredServices: ['db'] },
|
||||
};
|
||||
const out = resolveJobs(
|
||||
{ jobs: { 'smoke-test': { services: [] } } },
|
||||
'store',
|
||||
withRequired,
|
||||
MATRIX,
|
||||
);
|
||||
expect(Object.keys(out['smoke-test'].matrix.include[0].services!)).toEqual(['mysql']);
|
||||
});
|
||||
|
||||
it('honors caller enabled=false but still emits a filtered matrix', () => {
|
||||
const out = resolveJobs(
|
||||
{ jobs: { 'smoke-test': false } },
|
||||
'store',
|
||||
jobs,
|
||||
MATRIX,
|
||||
);
|
||||
expect(out['smoke-test'].enabled).toBe(false);
|
||||
expect(out['smoke-test'].matrix.include[0].services).toBeDefined();
|
||||
});
|
||||
|
||||
it('throws on unknown job names with the kind in the message', () => {
|
||||
expect(() => resolveJobs({ jobs: { taco: false } }, 'store', jobs, MATRIX)).toThrowError(
|
||||
/unknown job "taco" for kind "store"/
|
||||
);
|
||||
});
|
||||
|
||||
it('throws when `jobs` is not an object', () => {
|
||||
expect(() => resolveJobs({ jobs: 'oops' } as never, 'store', jobs, MATRIX)).toThrowError(
|
||||
/`jobs` must be an object/
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseRawConfig', () => {
|
||||
it('returns an empty object for empty input', () => {
|
||||
expect(parseRawConfig('')).toEqual({});
|
||||
});
|
||||
|
||||
it('returns an empty object for whitespace input', () => {
|
||||
expect(parseRawConfig(' \n ')).toEqual({});
|
||||
});
|
||||
|
||||
it('parses a valid object', () => {
|
||||
expect(parseRawConfig('{"jobs": {"unit-test": true}}')).toEqual({
|
||||
jobs: { 'unit-test': true },
|
||||
});
|
||||
});
|
||||
|
||||
it('throws on syntactically invalid JSON', () => {
|
||||
expect(() => parseRawConfig('{not json}')).toThrowError(/failed to parse JSON/);
|
||||
});
|
||||
|
||||
it('throws when top level is an array', () => {
|
||||
expect(() => parseRawConfig('[]')).toThrowError(/top-level value must be an object/);
|
||||
});
|
||||
|
||||
it('throws when top level is a primitive', () => {
|
||||
expect(() => parseRawConfig('"hello"')).toThrowError(/top-level value must be an object/);
|
||||
});
|
||||
|
||||
it('throws when top level is null', () => {
|
||||
expect(() => parseRawConfig('null')).toThrowError(/top-level value must be an object/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseMatrixInput', () => {
|
||||
it('parses a valid matrix', () => {
|
||||
const out = parseMatrixInput('{"include": [{"php": "8.3"}]}');
|
||||
expect(out.include).toEqual([{ php: '8.3' }]);
|
||||
});
|
||||
|
||||
it('throws on empty input', () => {
|
||||
expect(() => parseMatrixInput('')).toThrowError(/`matrix` input is required/);
|
||||
});
|
||||
|
||||
it('throws on invalid JSON', () => {
|
||||
expect(() => parseMatrixInput('{nope}')).toThrowError(/failed to parse `matrix` input/);
|
||||
});
|
||||
|
||||
it('throws when top level is an array', () => {
|
||||
expect(() => parseMatrixInput('[]')).toThrowError(/`matrix` must be a JSON object/);
|
||||
});
|
||||
|
||||
it('throws when include is missing', () => {
|
||||
expect(() => parseMatrixInput('{}')).toThrowError(/`matrix.include` must be an array/);
|
||||
});
|
||||
|
||||
it('throws when include is not an array', () => {
|
||||
expect(() => parseMatrixInput('{"include": "nope"}')).toThrowError(/`matrix.include` must be an array/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,174 @@
|
||||
import { JobDefaults, Kind, Matrix, MatrixEntry, RawConfig, RawJobConfig, ResolvedConfig, ResolvedJobConfig, Services } from './types';
|
||||
import { isTier, servicesForTiers, Tier } from './tier-map';
|
||||
|
||||
/**
|
||||
* Normalizes a single raw job entry to (enabled, tiers). Accepts
|
||||
* the boolean shorthand and the object form. Validates the shape
|
||||
* and the `services` tier list; throws on unexpected input. The
|
||||
* caller supplies the per-job default tiers, used when `services`
|
||||
* is omitted from the entry.
|
||||
*/
|
||||
export const normalizeJobEntry = (
|
||||
jobName: string,
|
||||
raw: RawJobConfig | undefined,
|
||||
defaults: JobDefaults,
|
||||
): { enabled: boolean; tiers: readonly Tier[] } => {
|
||||
if (raw === undefined) {
|
||||
return { enabled: true, tiers: defaults.services };
|
||||
}
|
||||
if (typeof raw === 'boolean') {
|
||||
return { enabled: raw, tiers: defaults.services };
|
||||
}
|
||||
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
|
||||
throw new Error(
|
||||
`check-config: job "${jobName}" must be a boolean or an object (got ${Array.isArray(raw) ? 'array' : typeof raw})`
|
||||
);
|
||||
}
|
||||
const { enabled, services } = raw as { enabled?: unknown; services?: unknown };
|
||||
const enabledValue = enabled === undefined ? true : Boolean(enabled);
|
||||
|
||||
if (services === undefined) {
|
||||
return { enabled: enabledValue, tiers: defaults.services };
|
||||
}
|
||||
if (!Array.isArray(services)) {
|
||||
throw new Error(`check-config: job "${jobName}".services must be an array of tier names`);
|
||||
}
|
||||
const tiers: Tier[] = [];
|
||||
for (const value of services) {
|
||||
if (!isTier(value)) {
|
||||
throw new Error(`check-config: job "${jobName}".services contains unknown tier "${String(value)}"`);
|
||||
}
|
||||
tiers.push(value);
|
||||
}
|
||||
return { enabled: enabledValue, tiers };
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges a job's `requiredServices` into the resolved tier list,
|
||||
* deduplicating while preserving order (required tiers first, then
|
||||
* the caller/default tiers in their original order).
|
||||
*/
|
||||
export const mergeRequiredTiers = (
|
||||
tiers: readonly Tier[],
|
||||
required: readonly Tier[] | undefined,
|
||||
): readonly Tier[] => {
|
||||
if (!required || required.length === 0) return tiers;
|
||||
const seen = new Set<Tier>();
|
||||
const merged: Tier[] = [];
|
||||
for (const tier of required) {
|
||||
if (!seen.has(tier)) { seen.add(tier); merged.push(tier); }
|
||||
}
|
||||
for (const tier of tiers) {
|
||||
if (!seen.has(tier)) { seen.add(tier); merged.push(tier); }
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy of `entry` with `services` filtered to the concrete
|
||||
* names produced by expanding `tiers` through the tier-map. An empty
|
||||
* tier list yields `services: {}`.
|
||||
*/
|
||||
export const filterEntryServices = (entry: MatrixEntry, tiers: readonly Tier[]): MatrixEntry => {
|
||||
const keep = servicesForTiers(tiers);
|
||||
const original = entry.services ?? {};
|
||||
const filtered: Services = {};
|
||||
for (const [name, config] of Object.entries(original)) {
|
||||
if (keep.has(name)) filtered[name] = config;
|
||||
}
|
||||
return { ...entry, services: filtered };
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-job filter applied to the supported-version matrix: returns a
|
||||
* shallow clone with every entry's `services` narrowed to the tiers
|
||||
* the job needs.
|
||||
*/
|
||||
export const filterMatrixForJob = (matrix: Matrix, tiers: readonly Tier[]): Matrix => ({
|
||||
...matrix,
|
||||
include: matrix.include.map(entry => filterEntryServices(entry, tiers)),
|
||||
});
|
||||
|
||||
/**
|
||||
* Shared per-kind resolver: walks the per-kind job map and emits one
|
||||
* `ResolvedJobConfig` per known job. Caller-supplied jobs override
|
||||
* the defaults; jobs the caller omitted still appear, carrying the
|
||||
* default `enabled: true` and the default tier list. Rejects unknown
|
||||
* job names from the config so typos surface in CI.
|
||||
*/
|
||||
export const resolveJobs = (
|
||||
raw: RawConfig,
|
||||
kind: Kind,
|
||||
jobs: Record<string, JobDefaults>,
|
||||
matrix: Matrix,
|
||||
): ResolvedConfig => {
|
||||
const rawJobs = raw.jobs ?? {};
|
||||
if (rawJobs === null || typeof rawJobs !== 'object' || Array.isArray(rawJobs)) {
|
||||
throw new Error(`check-config: \`jobs\` must be an object`);
|
||||
}
|
||||
for (const name of Object.keys(rawJobs)) {
|
||||
if (!(name in jobs)) {
|
||||
throw new Error(
|
||||
`check-config: unknown job "${name}" for kind "${kind}". Known jobs: ${Object.keys(jobs).join(', ')}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const resolved: ResolvedConfig = {};
|
||||
for (const [name, defaults] of Object.entries(jobs)) {
|
||||
const entry = (rawJobs as Record<string, RawJobConfig>)[name];
|
||||
const { enabled, tiers } = normalizeJobEntry(name, entry, defaults);
|
||||
const finalTiers = mergeRequiredTiers(tiers, defaults.requiredServices);
|
||||
resolved[name] = {
|
||||
enabled,
|
||||
matrix: filterMatrixForJob(matrix, finalTiers),
|
||||
} as ResolvedJobConfig;
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a JSON string into a RawConfig with shape validation
|
||||
* (must be an object, not an array or primitive). Empty/whitespace
|
||||
* input yields an empty config.
|
||||
*/
|
||||
export const parseRawConfig = (jsonText: string): RawConfig => {
|
||||
const trimmed = jsonText.trim();
|
||||
if (trimmed === '') return {};
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(trimmed);
|
||||
} catch (e) {
|
||||
throw new Error(`check-config: failed to parse JSON: ${(e as Error).message}`);
|
||||
}
|
||||
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
||||
throw new Error(`check-config: top-level value must be an object`);
|
||||
}
|
||||
return parsed as RawConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the `matrix` action input. Validates the top-level shape
|
||||
* (must be an object with an `include` array) so a malformed input
|
||||
* fails with a clear message at the boundary.
|
||||
*/
|
||||
export const parseMatrixInput = (jsonText: string): Matrix => {
|
||||
const trimmed = jsonText.trim();
|
||||
if (trimmed === '') {
|
||||
throw new Error('check-config: `matrix` input is required');
|
||||
}
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(trimmed);
|
||||
} catch (e) {
|
||||
throw new Error(`check-config: failed to parse \`matrix\` input as JSON: ${(e as Error).message}`);
|
||||
}
|
||||
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
||||
throw new Error('check-config: `matrix` must be a JSON object');
|
||||
}
|
||||
const include = (parsed as Record<string, unknown>).include;
|
||||
if (!Array.isArray(include)) {
|
||||
throw new Error('check-config: `matrix.include` must be an array');
|
||||
}
|
||||
return parsed as Matrix;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { resolveConfig } from './resolve';
|
||||
import { Matrix } from './types';
|
||||
|
||||
const MATRIX: Matrix = {
|
||||
include: [{
|
||||
php: '8.3',
|
||||
services: {
|
||||
mysql: { image: 'mysql:8' },
|
||||
opensearch: { image: 'opensearchproject/opensearch:2' },
|
||||
rabbitmq: { image: 'rabbitmq:3' },
|
||||
valkey: { image: 'valkey:8' },
|
||||
nginx: { image: 'nginx:1.27' },
|
||||
'php-fpm': { image: 'php:8.3-fpm' },
|
||||
},
|
||||
}],
|
||||
};
|
||||
|
||||
describe('resolveConfig', () => {
|
||||
it('routes kind=store to the store resolver', () => {
|
||||
const resolved = resolveConfig({ jobs: { 'smoke-test': false } }, 'store', MATRIX);
|
||||
expect(resolved['smoke-test'].enabled).toBe(false);
|
||||
expect(resolved['unit-test'].enabled).toBe(true);
|
||||
});
|
||||
|
||||
it('routes kind=extension to the extension resolver', () => {
|
||||
const resolved = resolveConfig({ jobs: { 'compile-extension': false } }, 'extension', MATRIX);
|
||||
expect(resolved['compile-extension'].enabled).toBe(false);
|
||||
expect(resolved['integration_test'].enabled).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects a job name from the other kind', () => {
|
||||
expect(() => resolveConfig({ jobs: { 'smoke-test': false } }, 'extension', MATRIX)).toThrowError(
|
||||
/unknown job "smoke-test" for kind "extension"/
|
||||
);
|
||||
expect(() => resolveConfig({ jobs: { 'unit-test-extension': false } }, 'store', MATRIX)).toThrowError(
|
||||
/unknown job "unit-test-extension" for kind "store"/
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Kind, Matrix, RawConfig, ResolvedConfig } from './types';
|
||||
import { resolveStoreConfig } from './kinds/store';
|
||||
import { resolveExtensionConfig } from './kinds/extension';
|
||||
|
||||
/**
|
||||
* Dispatches to the per-kind resolver. Each kind owns its own list
|
||||
* of jobs and per-job defaults; this function just routes the call
|
||||
* and forwards the supported-version matrix.
|
||||
*/
|
||||
export const resolveConfig = (raw: RawConfig, kind: Kind, matrix: Matrix): ResolvedConfig => {
|
||||
if (kind === 'store') return resolveStoreConfig(raw, matrix);
|
||||
return resolveExtensionConfig(raw, matrix);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { isTier, servicesForTiers, TIER_TO_SERVICES } from './tier-map';
|
||||
|
||||
describe('isTier', () => {
|
||||
it('accepts every key in TIER_TO_SERVICES', () => {
|
||||
for (const tier of Object.keys(TIER_TO_SERVICES)) {
|
||||
expect(isTier(tier)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects unknown strings', () => {
|
||||
expect(isTier('llm')).toBe(false);
|
||||
expect(isTier('')).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects non-strings', () => {
|
||||
expect(isTier(42)).toBe(false);
|
||||
expect(isTier(null)).toBe(false);
|
||||
expect(isTier(undefined)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('servicesForTiers', () => {
|
||||
it('returns an empty set for an empty tier list', () => {
|
||||
expect([...servicesForTiers([])]).toEqual([]);
|
||||
});
|
||||
|
||||
it('expands a single tier to its concrete service names', () => {
|
||||
expect([...servicesForTiers(['queue'])]).toEqual(['rabbitmq']);
|
||||
});
|
||||
|
||||
it('expands the search tier to both implementations', () => {
|
||||
expect([...servicesForTiers(['search'])].sort()).toEqual(['elasticsearch', 'opensearch']);
|
||||
});
|
||||
|
||||
it('expands the web tier to nginx + php-fpm', () => {
|
||||
expect([...servicesForTiers(['web'])].sort()).toEqual(['nginx', 'php-fpm']);
|
||||
});
|
||||
|
||||
it('unions across multiple tiers', () => {
|
||||
expect([...servicesForTiers(['cache', 'queue'])].sort()).toEqual(['rabbitmq', 'redis', 'valkey']);
|
||||
});
|
||||
|
||||
it('deduplicates if the same tier appears twice', () => {
|
||||
expect([...servicesForTiers(['queue', 'queue'])]).toEqual(['rabbitmq']);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* A category of services that can be selected for a job. Mirrors the
|
||||
* `Tier` concept in `supported-version` but adds the `web` tier so the
|
||||
* smoke-test and integration jobs can opt the nginx + php-fpm pair in
|
||||
* or out as a unit. Intentionally narrower than supported-version's
|
||||
* tier set: this file only names tiers that the resolve-check-config
|
||||
* schema lets callers toggle.
|
||||
*/
|
||||
export type Tier = 'db' | 'search' | 'queue' | 'cache' | 'web';
|
||||
|
||||
/**
|
||||
* Expansion of each tier to the concrete service names that may
|
||||
* appear in a supported-version matrix entry's `services` map.
|
||||
* Filtering a matrix entry's services for a tier list means keeping
|
||||
* the keys that union to the values of the selected tiers here.
|
||||
*/
|
||||
export const TIER_TO_SERVICES: Record<Tier, readonly string[]> = {
|
||||
db: ['mysql'],
|
||||
search: ['opensearch', 'elasticsearch'],
|
||||
queue: ['rabbitmq'],
|
||||
cache: ['valkey', 'redis'],
|
||||
web: ['nginx', 'php-fpm'],
|
||||
};
|
||||
|
||||
export const isTier = (value: unknown): value is Tier =>
|
||||
typeof value === 'string' && value in TIER_TO_SERVICES;
|
||||
|
||||
/**
|
||||
* Returns the flat set of concrete service names for a list of tiers.
|
||||
* Used to filter a matrix entry's `services` map down to only the
|
||||
* containers a particular job actually needs.
|
||||
*/
|
||||
export const servicesForTiers = (tiers: readonly Tier[]): Set<string> => {
|
||||
const result = new Set<string>();
|
||||
for (const tier of tiers) {
|
||||
for (const name of TIER_TO_SERVICES[tier]) result.add(name);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import { Tier } from './tier-map';
|
||||
|
||||
/**
|
||||
* Which reusable workflow a config belongs to. Selects the known-job
|
||||
* list used for validation and the default config path.
|
||||
*/
|
||||
export type Kind = 'store' | 'extension';
|
||||
|
||||
/**
|
||||
* A single service container definition from supported-version's
|
||||
* matrix output. We don't model the inner shape here — we just
|
||||
* preserve unknown keys when filtering.
|
||||
*/
|
||||
export interface ServiceConfig {
|
||||
image: string;
|
||||
env?: Record<string, string>;
|
||||
ports?: string[];
|
||||
options?: string;
|
||||
volumes?: string[];
|
||||
}
|
||||
|
||||
export interface Services {
|
||||
[serviceName: string]: ServiceConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* One row of supported-version's matrix. Carries the PHP/Composer/etc
|
||||
* coordinates plus the concrete `services` map this job should bring
|
||||
* up. We type the known fields supported-version emits and allow
|
||||
* extras to pass through untouched.
|
||||
*/
|
||||
export interface MatrixEntry {
|
||||
services?: Services;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* The matrix shape emitted by supported-version, suitable for
|
||||
* `strategy.matrix` in GitHub Actions.
|
||||
*/
|
||||
export interface Matrix {
|
||||
include: MatrixEntry[];
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-job static defaults declared by each kind module.
|
||||
*
|
||||
* `services` is the tier list used when the caller's config does not
|
||||
* override it — these tiers are user-toggleable through the schema.
|
||||
*
|
||||
* `requiredServices` is always merged in on top of the resolved list,
|
||||
* regardless of caller overrides. Use it for tiers a job structurally
|
||||
* cannot run without (e.g. mysql for a running store smoke-test) and
|
||||
* which therefore should not appear in the user-facing schema enum.
|
||||
*/
|
||||
export interface JobDefaults {
|
||||
services: readonly Tier[];
|
||||
requiredServices?: readonly Tier[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolved per-job output. `enabled` mirrors the input boolean (or
|
||||
* `enabled` key); `matrix` is supported-version's matrix with each
|
||||
* entry's `services` filtered to the tiers this job needs.
|
||||
*/
|
||||
export interface ResolvedJobConfig {
|
||||
enabled: boolean;
|
||||
matrix: Matrix;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map of job-name -> resolved config. Keys are exactly the job names
|
||||
* declared by the kind module (omitted-by-caller jobs still appear,
|
||||
* carrying defaults so the consumer's `if:` guard works uniformly).
|
||||
*/
|
||||
export interface ResolvedConfig {
|
||||
[jobName: string]: ResolvedJobConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shape of a single per-job entry in the user's JSON config file.
|
||||
* - `true` / `false`: shorthand for `{ enabled: true|false }`
|
||||
* - object: explicit enabled flag plus an optional tier list under
|
||||
* `services` (validated against the per-kind schema).
|
||||
*/
|
||||
export type RawJobConfig =
|
||||
| boolean
|
||||
| { enabled?: boolean; services?: string[]; [key: string]: unknown };
|
||||
|
||||
/**
|
||||
* Top-level shape of the user's JSON config file. Job toggles live
|
||||
* under `jobs`; the rest of the top level is reserved for future
|
||||
* global keys.
|
||||
*/
|
||||
export interface RawConfig {
|
||||
jobs?: { [jobName: string]: RawJobConfig };
|
||||
[key: string]: unknown;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
"typeRoots": ["../node_modules/@types"],
|
||||
"types": ["jest"]
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/sansec-ecomscan@v8.0.0 # x-release-please-version
|
||||
- uses: graycoreio/github-actions-magento2/sansec-ecomscan@v8.2.0 # x-release-please-version
|
||||
with:
|
||||
license: ${{ secrets.SANSEC_LICENSE_KEY }}
|
||||
```
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
name: A job to semantically compare two versions
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: graycoreio/github-actions-magento2/semver-compare@v8.0.0 # x-release-please-version
|
||||
- uses: graycoreio/github-actions-magento2/semver-compare@v8.2.0 # x-release-please-version
|
||||
with:
|
||||
version: 2.1.0
|
||||
compare_against: 2.2.3
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
matrix: ${{ steps.supported-version.outputs.matrix }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: graycoreio/github-actions-magento2/supported-version@v8.0.0 # x-release-please-version
|
||||
- uses: graycoreio/github-actions-magento2/supported-version@v8.2.0 # x-release-please-version
|
||||
id: supported-version
|
||||
|
||||
compile:
|
||||
@@ -40,19 +40,19 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/setup-magento@v8.0.0 # x-release-please-version
|
||||
- uses: graycoreio/github-actions-magento2/setup-magento@v8.2.0 # x-release-please-version
|
||||
id: setup-magento
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
tools: composer:v${{ matrix.composer }}
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/cache-magento@v8.0.0 # x-release-please-version
|
||||
- uses: graycoreio/github-actions-magento2/cache-magento@v8.2.0 # x-release-please-version
|
||||
|
||||
- run: composer install
|
||||
env:
|
||||
COMPOSER_AUTH: ${{ secrets.COMPOSER_AUTH }}
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/setup-di-compile@v8.0.0 # x-release-please-version
|
||||
- uses: graycoreio/github-actions-magento2/setup-di-compile@v8.2.0 # x-release-please-version
|
||||
with:
|
||||
path: ${{ steps.setup-magento.outputs.path }}
|
||||
```
|
||||
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
matrix: ${{ steps.supported-version.outputs.matrix }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: graycoreio/github-actions-magento2/supported-version@v8.0.0 # x-release-please-version
|
||||
- uses: graycoreio/github-actions-magento2/supported-version@v8.2.0 # x-release-please-version
|
||||
id: supported-version
|
||||
with:
|
||||
include_services: "true"
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/setup-magento@v8.0.0 # x-release-please-version
|
||||
- uses: graycoreio/github-actions-magento2/setup-magento@v8.2.0 # x-release-please-version
|
||||
id: setup-magento
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
@@ -64,7 +64,7 @@ jobs:
|
||||
env:
|
||||
COMPOSER_AUTH: ${{ secrets.COMPOSER_AUTH }}
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/setup-install@v8.0.0 # x-release-please-version
|
||||
- uses: graycoreio/github-actions-magento2/setup-install@v8.2.0 # x-release-please-version
|
||||
with:
|
||||
services: ${{ toJSON(matrix.services) }}
|
||||
path: ${{ steps.setup-magento.outputs.path }}
|
||||
|
||||
@@ -18,6 +18,11 @@ inputs:
|
||||
default: ""
|
||||
description: "Additional raw flags to append to the setup:install command."
|
||||
|
||||
container_id:
|
||||
required: false
|
||||
default: ""
|
||||
description: "When set, runs setup:install via `docker exec` inside the container with this `container_id` (typically the value of `job.services['php-fpm'].id`) and uses service-network aliases (mysql, redis, etc.) instead of runner-side localhost when running setup:install."
|
||||
|
||||
outputs:
|
||||
command:
|
||||
description: "The full bin/magento setup:install command that was run."
|
||||
|
||||
Vendored
+28
-28
File diff suppressed because one or more lines are too long
@@ -173,4 +173,100 @@ describe('buildInstallArgs', () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('container mode', () => {
|
||||
it('uses the mysql network alias and the internal port from ports[0]', () => {
|
||||
const services: Services = {
|
||||
mysql: { ...MYSQL_SERVICE, ports: ['33060:3306'] },
|
||||
};
|
||||
expect(buildInstallArgs(services, true)).toEqual([
|
||||
...BASE_ARGS,
|
||||
'--db-host=mysql:3306',
|
||||
'--db-name=magento_integration_tests',
|
||||
'--db-user=user',
|
||||
'--db-password=password',
|
||||
]);
|
||||
});
|
||||
|
||||
it('uses the opensearch alias and parses the internal port', () => {
|
||||
const services: Services = {
|
||||
opensearch: { image: 'opensearchproject/opensearch:2.19.1', ports: ['19200:9200'] },
|
||||
};
|
||||
expect(buildInstallArgs(services, true)).toEqual([
|
||||
...BASE_ARGS,
|
||||
'--search-engine=opensearch',
|
||||
'--opensearch-host=opensearch',
|
||||
'--opensearch-port=9200',
|
||||
]);
|
||||
});
|
||||
|
||||
it('uses the elasticsearch alias and parses the internal port', () => {
|
||||
const services: Services = {
|
||||
elasticsearch: { image: 'elasticsearch:8.11.4', ports: ['19200:9200'] },
|
||||
};
|
||||
expect(buildInstallArgs(services, true)).toEqual([
|
||||
...BASE_ARGS,
|
||||
'--search-engine=elasticsearch8',
|
||||
'--elasticsearch-host=elasticsearch',
|
||||
'--elasticsearch-port=9200',
|
||||
]);
|
||||
});
|
||||
|
||||
it('uses the rabbitmq alias and parses the internal port', () => {
|
||||
const services: Services = {
|
||||
rabbitmq: { image: 'rabbitmq:4.0-management', ports: ['15672:5672'] },
|
||||
};
|
||||
expect(buildInstallArgs(services, true)).toEqual([
|
||||
...BASE_ARGS,
|
||||
'--amqp-host=rabbitmq',
|
||||
'--amqp-port=5672',
|
||||
'--amqp-user=guest',
|
||||
'--amqp-password=guest',
|
||||
]);
|
||||
});
|
||||
|
||||
it('uses the valkey alias when valkey is the cache service', () => {
|
||||
const services: Services = {
|
||||
valkey: { image: 'valkey:8.0', ports: ['16379:6379'] },
|
||||
};
|
||||
expect(buildInstallArgs(services, true)).toEqual([
|
||||
...BASE_ARGS,
|
||||
'--session-save=redis',
|
||||
'--session-save-redis-host=valkey',
|
||||
'--session-save-redis-port=6379',
|
||||
'--cache-backend=redis',
|
||||
'--cache-backend-redis-server=valkey',
|
||||
'--cache-backend-redis-port=6379',
|
||||
]);
|
||||
});
|
||||
|
||||
it('uses the redis alias when redis is the cache service', () => {
|
||||
const services: Services = {
|
||||
redis: { image: 'redis:7.2', ports: ['16379:6379'] },
|
||||
};
|
||||
expect(buildInstallArgs(services, true)).toEqual([
|
||||
...BASE_ARGS,
|
||||
'--session-save=redis',
|
||||
'--session-save-redis-host=redis',
|
||||
'--session-save-redis-port=6379',
|
||||
'--cache-backend=redis',
|
||||
'--cache-backend-redis-server=redis',
|
||||
'--cache-backend-redis-port=6379',
|
||||
]);
|
||||
});
|
||||
|
||||
it('falls back to default internal ports when ports are absent', () => {
|
||||
const services: Services = {
|
||||
mysql: { ...MYSQL_SERVICE, ports: undefined },
|
||||
opensearch: { image: 'opensearchproject/opensearch:2.19.1' },
|
||||
rabbitmq: { image: 'rabbitmq:4.0-management' },
|
||||
valkey: { image: 'valkey:8.0' },
|
||||
};
|
||||
const args = buildInstallArgs(services, true);
|
||||
expect(args).toContain('--db-host=mysql:3306');
|
||||
expect(args).toContain('--opensearch-port=9200');
|
||||
expect(args).toContain('--amqp-port=5672');
|
||||
expect(args).toContain('--session-save-redis-port=6379');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,7 @@ export interface ServiceConfig {
|
||||
env?: Record<string, string>;
|
||||
ports?: string[];
|
||||
options?: string;
|
||||
volumes?: string[];
|
||||
}
|
||||
|
||||
export interface Services {
|
||||
@@ -12,6 +13,7 @@ export interface Services {
|
||||
rabbitmq?: ServiceConfig;
|
||||
redis?: ServiceConfig;
|
||||
valkey?: ServiceConfig;
|
||||
'php-fpm'?: ServiceConfig;
|
||||
}
|
||||
|
||||
const BASE_ARGS = [
|
||||
@@ -24,21 +26,29 @@ const BASE_ARGS = [
|
||||
'--backend-frontname=admin',
|
||||
];
|
||||
|
||||
const parsePort = (svc: ServiceConfig | undefined, index: 0 | 1, fallback: string): string => {
|
||||
return svc?.ports?.[0]?.split(':')[index] ?? fallback;
|
||||
};
|
||||
|
||||
export const buildMysqlPrepArgs = (mysql: ServiceConfig): string[] => {
|
||||
const rootPassword = mysql.env?.MYSQL_ROOT_PASSWORD ?? 'rootpassword';
|
||||
const port = mysql.ports?.[0]?.split(':')[0] ?? '3306';
|
||||
const port = parsePort(mysql, 0, '3306');
|
||||
return ['-h127.0.0.1', `--port=${port}`, '-uroot', `-p${rootPassword}`, '-e', 'SET GLOBAL log_bin_trust_function_creators = 1;'];
|
||||
};
|
||||
|
||||
export const buildInstallArgs = (services: Services | null): string[] => {
|
||||
export const buildInstallArgs = (services: Services | null, containerMode = false): string[] => {
|
||||
const args = [...BASE_ARGS];
|
||||
|
||||
if (!services) return args;
|
||||
|
||||
const portIdx: 0 | 1 = containerMode ? 1 : 0;
|
||||
const hostFor = (alias: string): string => containerMode ? alias : 'localhost';
|
||||
|
||||
if (services.mysql) {
|
||||
const dbPort = services.mysql.ports?.[0]?.split(':')[0] ?? '3306';
|
||||
const dbPort = parsePort(services.mysql, portIdx, '3306');
|
||||
const dbHost = containerMode ? `mysql:${dbPort}` : `127.0.0.1:${dbPort}`;
|
||||
args.push(
|
||||
`--db-host=127.0.0.1:${dbPort}`,
|
||||
`--db-host=${dbHost}`,
|
||||
`--db-name=${services.mysql.env?.MYSQL_DATABASE ?? 'magento'}`,
|
||||
`--db-user=${services.mysql.env?.MYSQL_USER ?? 'magento'}`,
|
||||
`--db-password=${services.mysql.env?.MYSQL_PASSWORD ?? 'magento'}`,
|
||||
@@ -46,39 +56,46 @@ export const buildInstallArgs = (services: Services | null): string[] => {
|
||||
}
|
||||
|
||||
if (services.opensearch) {
|
||||
const port = parsePort(services.opensearch, portIdx, '9200');
|
||||
args.push(
|
||||
'--search-engine=opensearch',
|
||||
'--opensearch-host=localhost',
|
||||
'--opensearch-port=9200',
|
||||
`--opensearch-host=${hostFor('opensearch')}`,
|
||||
`--opensearch-port=${port}`,
|
||||
);
|
||||
} else if (services.elasticsearch) {
|
||||
const majorVersion = services.elasticsearch.image.split(':')[1]?.split('.')[0];
|
||||
const port = parsePort(services.elasticsearch, portIdx, '9200');
|
||||
args.push(
|
||||
`--search-engine=elasticsearch${majorVersion}`,
|
||||
'--elasticsearch-host=localhost',
|
||||
'--elasticsearch-port=9200',
|
||||
`--elasticsearch-host=${hostFor('elasticsearch')}`,
|
||||
`--elasticsearch-port=${port}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (services.rabbitmq) {
|
||||
const port = parsePort(services.rabbitmq, portIdx, '5672');
|
||||
args.push(
|
||||
'--amqp-host=localhost',
|
||||
'--amqp-port=5672',
|
||||
`--amqp-host=${hostFor('rabbitmq')}`,
|
||||
`--amqp-port=${port}`,
|
||||
'--amqp-user=guest',
|
||||
'--amqp-password=guest',
|
||||
);
|
||||
}
|
||||
|
||||
if (services.valkey || services.redis) {
|
||||
const cacheKey: 'valkey' | 'redis' = services.valkey ? 'valkey' : 'redis';
|
||||
const cache = services[cacheKey]!;
|
||||
const port = parsePort(cache, portIdx, '6379');
|
||||
const host = hostFor(cacheKey);
|
||||
args.push(
|
||||
'--session-save=redis',
|
||||
'--session-save-redis-host=localhost',
|
||||
'--session-save-redis-port=6379',
|
||||
`--session-save-redis-host=${host}`,
|
||||
`--session-save-redis-port=${port}`,
|
||||
'--cache-backend=redis',
|
||||
'--cache-backend-redis-server=localhost',
|
||||
'--cache-backend-redis-port=6379',
|
||||
`--cache-backend-redis-server=${host}`,
|
||||
`--cache-backend-redis-port=${port}`,
|
||||
);
|
||||
}
|
||||
|
||||
return args;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,12 +1,25 @@
|
||||
import * as core from '@actions/core';
|
||||
import * as exec from '@actions/exec';
|
||||
import * as nodePath from 'path';
|
||||
import { buildInstallArgs, buildMysqlPrepArgs, Services } from './build-command';
|
||||
|
||||
const resolveContainerPath = (runnerPath: string): string => {
|
||||
const workspace = process.env.GITHUB_WORKSPACE || '';
|
||||
const absolute = nodePath.resolve(workspace, runnerPath);
|
||||
const relative = nodePath.relative(workspace, absolute);
|
||||
if (relative.startsWith('..')) {
|
||||
throw new Error(`container_id: path ${runnerPath} resolves outside GITHUB_WORKSPACE (${workspace})`);
|
||||
}
|
||||
return relative ? `/var/www/html/${relative}` : '/var/www/html';
|
||||
};
|
||||
|
||||
export async function run(): Promise<void> {
|
||||
try {
|
||||
const servicesInput = core.getInput('services');
|
||||
const path = core.getInput('path') || '.';
|
||||
const extraArgs = core.getInput('extra_args').trim();
|
||||
const containerId = core.getInput('container_id').trim();
|
||||
const containerMode = containerId !== '';
|
||||
|
||||
let services: Services | null = null;
|
||||
if (servicesInput && servicesInput !== 'null') {
|
||||
@@ -14,23 +27,45 @@ export async function run(): Promise<void> {
|
||||
}
|
||||
|
||||
// setup:install creates MySQL triggers, which requires log_bin_trust_function_creators=1
|
||||
// when binary logging is enabled.
|
||||
// when binary logging is enabled. The prep always runs runner-side against the published port.
|
||||
if (services?.mysql) {
|
||||
await exec.exec('mysql', buildMysqlPrepArgs(services.mysql));
|
||||
}
|
||||
|
||||
const args = buildInstallArgs(services);
|
||||
const args = buildInstallArgs(services, containerMode);
|
||||
|
||||
if (extraArgs) {
|
||||
args.push(...extraArgs.split(/\s+/));
|
||||
}
|
||||
|
||||
core.setOutput('command', `php bin/magento setup:install ${args.join(' ')}`);
|
||||
if (containerMode) {
|
||||
const containerPath = resolveContainerPath(path);
|
||||
|
||||
await exec.exec('php', ['bin/magento', 'setup:install', ...args], { cwd: path });
|
||||
const command = `docker exec -w ${containerPath} ${containerId} php bin/magento setup:install ${args.join(' ')}`;
|
||||
core.setOutput('command', command);
|
||||
|
||||
await exec.exec('docker', [
|
||||
'exec',
|
||||
'-w', containerPath,
|
||||
containerId,
|
||||
'php', 'bin/magento', 'setup:install',
|
||||
...args,
|
||||
]);
|
||||
|
||||
// setup:install runs as root inside the container, but php-fpm workers
|
||||
// serve requests as `www-data`. Hand ownership of the Magento writable
|
||||
// dirs to www-data so request-time cache/log writes succeed.
|
||||
await exec.exec('docker', [
|
||||
'exec', containerId, 'sh', '-c',
|
||||
`for d in var generated pub/static pub/media; do [ -d "${containerPath}/$d" ] && chown -R www-data:www-data "${containerPath}/$d"; done`,
|
||||
]);
|
||||
} else {
|
||||
core.setOutput('command', `php bin/magento setup:install ${args.join(' ')}`);
|
||||
await exec.exec('php', ['bin/magento', 'setup:install', ...args], { cwd: path });
|
||||
}
|
||||
} catch (error) {
|
||||
core.setFailed((error as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
run();
|
||||
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/setup-magento@v8.0.0 # x-release-please-version
|
||||
- uses: graycoreio/github-actions-magento2/setup-magento@v8.2.0 # x-release-please-version
|
||||
id: setup-magento
|
||||
with:
|
||||
php-version: "8.3"
|
||||
@@ -89,7 +89,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/setup-magento@v8.0.0 # x-release-please-version
|
||||
- uses: graycoreio/github-actions-magento2/setup-magento@v8.2.0 # x-release-please-version
|
||||
id: setup-magento
|
||||
with:
|
||||
php-version: "8.3"
|
||||
|
||||
@@ -111,7 +111,7 @@ runs:
|
||||
fi
|
||||
printf '%s\n' "$line" >> .gitattributes
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/fix-magento-install@v8.0.0
|
||||
- uses: graycoreio/github-actions-magento2/fix-magento-install@main
|
||||
name: Fix Magento Out of Box Install Issues
|
||||
with:
|
||||
magento_directory: ${{ steps.setup-magento-compute-directory.outputs.MAGENTO_DIRECTORY }}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
name: "Magento Smoke Test"
|
||||
author: "Graycore"
|
||||
description: "Hits a running Magento instance with a basic page or GraphQL probe and asserts the response looks right."
|
||||
|
||||
inputs:
|
||||
kind:
|
||||
description: "Which probe to run: `page` (GET / with title check) or `graphql` (POST /graphql with storeConfig query)."
|
||||
required: true
|
||||
host:
|
||||
description: "Host (and optional port) to probe. Defaults to `localhost`."
|
||||
required: false
|
||||
default: "localhost"
|
||||
timeout:
|
||||
description: "curl --max-time in seconds."
|
||||
required: false
|
||||
default: "60"
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Validate kind
|
||||
shell: bash
|
||||
env:
|
||||
KIND: ${{ inputs.kind }}
|
||||
run: |
|
||||
case "$KIND" in
|
||||
page|graphql) ;;
|
||||
*) echo "FATAL: kind must be 'page' or 'graphql' (got '$KIND')"; exit 1 ;;
|
||||
esac
|
||||
|
||||
- name: Smoke test page
|
||||
if: inputs.kind == 'page'
|
||||
shell: bash
|
||||
env:
|
||||
HOST: ${{ inputs.host }}
|
||||
TIMEOUT: ${{ inputs.timeout }}
|
||||
run: |
|
||||
status=$(curl -sS --max-time "$TIMEOUT" \
|
||||
-o /tmp/smoke-page.html -w "%{http_code}" "http://$HOST/")
|
||||
if [ "$status" != "200" ]; then
|
||||
echo "Page returned HTTP $status"
|
||||
head -c 4000 /tmp/smoke-page.html
|
||||
exit 1
|
||||
fi
|
||||
if ! grep -qE '<title>[[:space:]]*[^<[:space:]][^<]*</title>' /tmp/smoke-page.html; then
|
||||
echo "Page missing non-empty <title>"
|
||||
head -c 4000 /tmp/smoke-page.html
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Smoke test GraphQL
|
||||
if: inputs.kind == 'graphql'
|
||||
shell: bash
|
||||
env:
|
||||
HOST: ${{ inputs.host }}
|
||||
TIMEOUT: ${{ inputs.timeout }}
|
||||
run: |
|
||||
status=$(curl -sS --max-time "$TIMEOUT" \
|
||||
-H "Content-Type: application/json" \
|
||||
-X POST \
|
||||
-d '{"query":"{ storeConfig { store_code } }"}' \
|
||||
-o /tmp/smoke-graphql.json -w "%{http_code}" \
|
||||
"http://$HOST/graphql")
|
||||
if [ "$status" != "200" ]; then
|
||||
echo "GraphQL returned HTTP $status"
|
||||
cat /tmp/smoke-graphql.json
|
||||
exit 1
|
||||
fi
|
||||
if ! jq -e '.data.storeConfig.store_code' /tmp/smoke-graphql.json > /dev/null; then
|
||||
echo "GraphQL response missing data.storeConfig.store_code"
|
||||
cat /tmp/smoke-graphql.json
|
||||
exit 1
|
||||
fi
|
||||
|
||||
branding:
|
||||
icon: "check-circle"
|
||||
color: "green"
|
||||
@@ -18,6 +18,7 @@ See the [action.yml](./action.yml)
|
||||
| custom_versions | The versions you want to support, as a comma-separated string, i.e. 'magento/project-community-edition:2.3.7-p3, magento/project-community-edition:2.4.2-p2' | false | '' |
|
||||
| recent_time_frame | The time frame (from today) used when `kind` is `recent`. Combination of years (y), months (m), and days (d), e.g. `2y 2m 2d`. | false | '2y' |
|
||||
| include_services | Whether to include a `services` key in each matrix entry with GitHub Actions service container configurations for MySQL, search engine, RabbitMQ, and cache. | false | 'true' |
|
||||
| service_preferences | Comma-separated list of service implementations to prefer (e.g. `elasticsearch,valkey`). See [Service preferences](#service-preferences). | false | '' |
|
||||
|
||||
## Kinds
|
||||
- `currently-supported` - The currently supported Magento Open Source versions by Adobe.
|
||||
@@ -32,6 +33,47 @@ See the [action.yml](./action.yml)
|
||||
- `mage-os`
|
||||
- `magento-open-source` (default)
|
||||
|
||||
## Service preferences
|
||||
|
||||
When `include_services: true` (the default), each matrix entry is enriched with a `services` map. Some tiers of services (for example, search) have more than one valid implementation across the supported Magento versions:
|
||||
|
||||
- **search**: `opensearch` or `elasticsearch`
|
||||
- **cache**: `valkey` or `redis`
|
||||
|
||||
By default the action picks `opensearch` over `elasticsearch` and `valkey` over `redis` wherever both are available for the matrix entry's Magento version. `service_preferences` lets the caller override that default pick by naming the implementation they want.
|
||||
|
||||
Tiers without a preference fall back to the per-version default pick. Your preferences are **selective**, not **exclusive**.
|
||||
|
||||
### Format
|
||||
|
||||
A comma-separated list of service implementation names. Whitespace around names is tolerated.
|
||||
|
||||
```yml
|
||||
with:
|
||||
service_preferences: elasticsearch,valkey
|
||||
```
|
||||
|
||||
### Accepted names
|
||||
|
||||
| Name | Tier |
|
||||
|------------------|--------|
|
||||
| `mysql` | db |
|
||||
| `elasticsearch` | search |
|
||||
| `opensearch` | search |
|
||||
| `rabbitmq` | queue |
|
||||
| `redis` | cache |
|
||||
| `valkey` | cache |
|
||||
|
||||
### Example
|
||||
|
||||
```yml
|
||||
- uses: graycoreio/github-actions-magento2/supported-version@v8.2.0 # x-release-please-version
|
||||
id: supported-version
|
||||
with:
|
||||
kind: currently-supported
|
||||
service_preferences: opensearch,valkey
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```yml
|
||||
@@ -51,7 +93,7 @@ jobs:
|
||||
outputs:
|
||||
matrix: ${{ steps.supported-version.outputs.matrix }}
|
||||
steps:
|
||||
- uses: graycoreio/github-actions-magento2/supported-version@v8.0.0 # x-release-please-version
|
||||
- uses: graycoreio/github-actions-magento2/supported-version@v8.2.0 # x-release-please-version
|
||||
id: supported-version
|
||||
- run: echo ${{ steps.supported-version.outputs.matrix }}
|
||||
```
|
||||
|
||||
@@ -27,6 +27,11 @@ inputs:
|
||||
default: "true"
|
||||
description: "Whether to include a `services` key in each matrix entry with GitHub Actions service configurations."
|
||||
|
||||
service_preferences:
|
||||
required: false
|
||||
default: ""
|
||||
description: "Comma-separated list of service implementations to prefer (e.g. `elasticsearch,valkey`). Each name overrides the per-tier default implementation pick. Errors on unknown names, tier collisions (two names sharing a tier), or implementations not supported by every matrix entry for your selected kind."
|
||||
|
||||
outputs:
|
||||
matrix:
|
||||
description: "The Github Actions matrix of software technologies required to run Magento."
|
||||
|
||||
Vendored
+29
-25
File diff suppressed because one or more lines are too long
@@ -3,6 +3,7 @@ import { validateKind } from './kind/validate-kinds';
|
||||
import { getMatrixForKind } from './matrix/get-matrix-for-kind';
|
||||
import { validateProject } from "./project/validate-projects";
|
||||
import { buildServicesForEntry } from "./services/build-services";
|
||||
import { parseServicePreferences, validatePreferencesAgainstMatrix } from "./services/preferences";
|
||||
|
||||
|
||||
export async function run(): Promise<void> {
|
||||
@@ -12,19 +13,33 @@ export async function run(): Promise<void> {
|
||||
const project = core.getInput("project");
|
||||
const recent_time_frame = core.getInput("recent_time_frame");
|
||||
const include_services = core.getInput("include_services") === "true";
|
||||
const service_preferences_raw = core.getInput("service_preferences");
|
||||
|
||||
validateProject(<any>project)
|
||||
|
||||
validateKind(<any>kind, customVersions ? customVersions.split(',') : undefined);
|
||||
|
||||
const preferences = parseServicePreferences(service_preferences_raw);
|
||||
const hasPreferences = Object.keys(preferences).length > 0;
|
||||
|
||||
if (!include_services && hasPreferences) {
|
||||
throw new Error(
|
||||
'service_preferences cannot be combined with include_services: false. Set include_services: true or clear service_preferences.'
|
||||
);
|
||||
}
|
||||
|
||||
let matrix = getMatrixForKind(kind, project, customVersions, recent_time_frame);
|
||||
|
||||
if (include_services) {
|
||||
if (hasPreferences) {
|
||||
validatePreferencesAgainstMatrix(preferences, matrix.include);
|
||||
}
|
||||
const workspace = process.env.GITHUB_WORKSPACE || '';
|
||||
matrix = {
|
||||
magento: matrix.magento,
|
||||
include: matrix.include.map((entry) => ({
|
||||
...entry,
|
||||
services: buildServicesForEntry(entry)
|
||||
services: buildServicesForEntry(entry, preferences, workspace)
|
||||
}))
|
||||
};
|
||||
}
|
||||
@@ -36,4 +51,4 @@ export async function run(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
run()
|
||||
run()
|
||||
|
||||
@@ -3,6 +3,7 @@ export interface ServiceConfig {
|
||||
env?: Record<string, string>;
|
||||
ports?: string[];
|
||||
options?: string;
|
||||
volumes?: string[];
|
||||
}
|
||||
|
||||
export interface Services {
|
||||
|
||||
@@ -223,13 +223,17 @@ describe('buildServicesForEntry', () => {
|
||||
describe('complete service output', () => {
|
||||
it('should build all services when all are available', () => {
|
||||
const entry = createTestEntry();
|
||||
const services = buildServicesForEntry(entry);
|
||||
const services = buildServicesForEntry(entry, {}, '/runner/ws');
|
||||
|
||||
expect(Object.keys(services)).toHaveLength(4);
|
||||
expect(Object.keys(services).sort()).toEqual(
|
||||
['mysql', 'nginx', 'opensearch', 'php-fpm', 'rabbitmq', 'valkey']
|
||||
);
|
||||
expect(services.mysql).toBeDefined();
|
||||
expect(services.opensearch).toBeDefined();
|
||||
expect(services.rabbitmq).toBeDefined();
|
||||
expect(services.valkey).toBeDefined();
|
||||
expect(services.nginx).toBeDefined();
|
||||
expect(services['php-fpm']).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle entry with minimal services', () => {
|
||||
@@ -239,11 +243,123 @@ describe('buildServicesForEntry', () => {
|
||||
opensearch: '',
|
||||
rabbitmq: '',
|
||||
redis: '',
|
||||
valkey: ''
|
||||
valkey: '',
|
||||
nginx: '',
|
||||
php: ''
|
||||
});
|
||||
const services = buildServicesForEntry(entry);
|
||||
|
||||
expect(Object.keys(services)).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('web tier (nginx + php-fpm)', () => {
|
||||
it('emits both nginx and php-fpm when both data points are present', () => {
|
||||
const entry = createTestEntry();
|
||||
const services = buildServicesForEntry(entry, {}, '/runner/ws');
|
||||
|
||||
expect(services.nginx).toBeDefined();
|
||||
expect(services['php-fpm']).toBeDefined();
|
||||
});
|
||||
|
||||
it('uses the entry.nginx image for nginx', () => {
|
||||
const entry = createTestEntry({ nginx: 'nginx:1.27-alpine' });
|
||||
const services = buildServicesForEntry(entry, {}, '/runner/ws');
|
||||
|
||||
expect(services.nginx.image).toBe('nginx:1.27-alpine');
|
||||
});
|
||||
|
||||
it('composes the php-fpm image from entry.php using mappia', () => {
|
||||
const entry = createTestEntry({ php: '8.2' });
|
||||
const services = buildServicesForEntry(entry, {}, '/runner/ws');
|
||||
|
||||
expect(services['php-fpm'].image).toBe('mappia/magento-php:fpm-alpine8.2');
|
||||
});
|
||||
|
||||
it('mounts the runner workspace at /var/www/html on both services', () => {
|
||||
const entry = createTestEntry();
|
||||
const services = buildServicesForEntry(entry, {}, '/home/runner/work/foo/foo');
|
||||
|
||||
expect(services.nginx.volumes).toEqual(['/home/runner/work/foo/foo:/var/www/html']);
|
||||
expect(services['php-fpm'].volumes).toEqual(['/home/runner/work/foo/foo:/var/www/html']);
|
||||
});
|
||||
|
||||
it('exposes port 80 on nginx with the nginx -t healthcheck', () => {
|
||||
const entry = createTestEntry();
|
||||
const services = buildServicesForEntry(entry, {}, '/runner/ws');
|
||||
|
||||
expect(services.nginx.ports).toEqual(['80:80']);
|
||||
expect(services.nginx.options).toContain('nginx -t');
|
||||
});
|
||||
|
||||
it('skips both when entry.nginx is empty (they emit together or not at all)', () => {
|
||||
const entry = createTestEntry({ nginx: '' });
|
||||
const services = buildServicesForEntry(entry, {}, '/runner/ws');
|
||||
|
||||
expect(services.nginx).toBeUndefined();
|
||||
expect(services['php-fpm']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('skips both when entry.php is empty (they emit together or not at all)', () => {
|
||||
const entry = createTestEntry({ php: '' });
|
||||
const services = buildServicesForEntry(entry, {}, '/runner/ws');
|
||||
|
||||
expect(services.nginx).toBeUndefined();
|
||||
expect(services['php-fpm']).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('with service preferences', () => {
|
||||
it('uses elasticsearch when search preference is elasticsearch, even if opensearch is available', () => {
|
||||
const entry = createTestEntry();
|
||||
const services = buildServicesForEntry(entry, { search: 'elasticsearch' });
|
||||
|
||||
expect(services.elasticsearch).toBeDefined();
|
||||
expect(services.elasticsearch.image).toBe('elasticsearch:8.11.4');
|
||||
expect(services.opensearch).toBeUndefined();
|
||||
});
|
||||
|
||||
it('uses opensearch when search preference is opensearch (matches default)', () => {
|
||||
const entry = createTestEntry();
|
||||
const services = buildServicesForEntry(entry, { search: 'opensearch' });
|
||||
|
||||
expect(services.opensearch).toBeDefined();
|
||||
expect(services.elasticsearch).toBeUndefined();
|
||||
});
|
||||
|
||||
it('uses redis when cache preference is redis, even if valkey is available', () => {
|
||||
const entry = createTestEntry();
|
||||
const services = buildServicesForEntry(entry, { cache: 'redis' });
|
||||
|
||||
expect(services.redis).toBeDefined();
|
||||
expect(services.redis.image).toBe('redis:7.2');
|
||||
expect(services.valkey).toBeUndefined();
|
||||
});
|
||||
|
||||
it('applies preferences across multiple tiers independently', () => {
|
||||
const entry = createTestEntry();
|
||||
const services = buildServicesForEntry(entry, { search: 'elasticsearch', cache: 'redis' });
|
||||
|
||||
expect(services.elasticsearch).toBeDefined();
|
||||
expect(services.redis).toBeDefined();
|
||||
expect(services.opensearch).toBeUndefined();
|
||||
expect(services.valkey).toBeUndefined();
|
||||
});
|
||||
|
||||
it('falls back to default-pick for tiers without a preference', () => {
|
||||
const entry = createTestEntry();
|
||||
const services = buildServicesForEntry(entry, { search: 'elasticsearch' });
|
||||
|
||||
expect(services.elasticsearch).toBeDefined();
|
||||
expect(services.valkey).toBeDefined();
|
||||
});
|
||||
|
||||
it('treats single-implementation-tier preferences as no-ops', () => {
|
||||
const entry = createTestEntry();
|
||||
const withPref = buildServicesForEntry(entry, { db: 'mysql', queue: 'rabbitmq' });
|
||||
const withoutPref = buildServicesForEntry(entry);
|
||||
|
||||
expect(withPref).toEqual(withoutPref);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,8 +5,11 @@ import {
|
||||
opensearchConfig,
|
||||
rabbitmqConfig,
|
||||
redisConfig,
|
||||
valkeyConfig
|
||||
valkeyConfig,
|
||||
buildNginxConfig,
|
||||
buildPhpFpmConfig
|
||||
} from './service-config';
|
||||
import { ServicePreferences } from './preferences';
|
||||
|
||||
interface SearchEngineChoice {
|
||||
type: 'opensearch' | 'elasticsearch';
|
||||
@@ -19,10 +22,22 @@ interface CacheChoice {
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines which search engine to use for a matrix entry.
|
||||
* Prefers opensearch over elasticsearch.
|
||||
* Picks the search engine for a matrix entry. Honors caller's `service_preferences`
|
||||
* when set; otherwise prefers opensearch over elasticsearch.
|
||||
*/
|
||||
function getSearchEngineChoice(entry: PackageMatrixVersion): SearchEngineChoice | null {
|
||||
const getSearchEngineChoice = (entry: PackageMatrixVersion, preference?: string): SearchEngineChoice | null => {
|
||||
if (preference === 'opensearch') {
|
||||
if (entry.opensearch && entry.opensearch.trim() !== '') {
|
||||
return { type: 'opensearch', image: entry.opensearch };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (preference === 'elasticsearch') {
|
||||
if (entry.elasticsearch && entry.elasticsearch.trim() !== '') {
|
||||
return { type: 'elasticsearch', image: entry.elasticsearch };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (entry.opensearch && entry.opensearch.trim() !== '') {
|
||||
return { type: 'opensearch', image: entry.opensearch };
|
||||
}
|
||||
@@ -33,10 +48,22 @@ function getSearchEngineChoice(entry: PackageMatrixVersion): SearchEngineChoice
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines which cache to use for a matrix entry.
|
||||
* Prefers valkey over redis.
|
||||
* Picks the cache for a matrix entry. Honors caller's `service_preferences`
|
||||
* when set; otherwise prefers valkey over redis.
|
||||
*/
|
||||
function getCacheChoice(entry: PackageMatrixVersion): CacheChoice | null {
|
||||
const getCacheChoice = (entry: PackageMatrixVersion, preference?: string): CacheChoice | null => {
|
||||
if (preference === 'valkey') {
|
||||
if (entry.valkey && entry.valkey.trim() !== '') {
|
||||
return { type: 'valkey', image: entry.valkey };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (preference === 'redis') {
|
||||
if (entry.redis && entry.redis.trim() !== '') {
|
||||
return { type: 'redis', image: entry.redis };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (entry.valkey && entry.valkey.trim() !== '') {
|
||||
return { type: 'valkey', image: entry.valkey };
|
||||
}
|
||||
@@ -47,18 +74,25 @@ function getCacheChoice(entry: PackageMatrixVersion): CacheChoice | null {
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the services object for a single matrix entry.
|
||||
* Builds the services object for a single matrix entry. Emits every
|
||||
* tier the entry has data for: mysql, search (opensearch/elasticsearch),
|
||||
* queue (rabbitmq), cache (valkey/redis), and web (nginx + php-fpm
|
||||
* together). The web tier requires `workspace` so the volume mount
|
||||
* has a real path; if the entry lacks either nginx or php data the
|
||||
* web tier is skipped entirely (they're coupled — emit both or neither).
|
||||
*/
|
||||
export function buildServicesForEntry(entry: PackageMatrixVersion): Services {
|
||||
export const buildServicesForEntry = (
|
||||
entry: PackageMatrixVersion,
|
||||
preferences: ServicePreferences = {},
|
||||
workspace: string = ''
|
||||
): Services => {
|
||||
const services: Services = {};
|
||||
|
||||
// MySQL is always included if present
|
||||
if (entry.mysql && entry.mysql.trim() !== '') {
|
||||
services.mysql = mysqlConfig.getConfig(entry.mysql);
|
||||
}
|
||||
|
||||
// Search engine: prefer opensearch over elasticsearch
|
||||
const searchEngine = getSearchEngineChoice(entry);
|
||||
const searchEngine = getSearchEngineChoice(entry, preferences.search);
|
||||
if (searchEngine) {
|
||||
if (searchEngine.type === 'opensearch') {
|
||||
services.opensearch = opensearchConfig.getConfig(searchEngine.image);
|
||||
@@ -67,13 +101,11 @@ export function buildServicesForEntry(entry: PackageMatrixVersion): Services {
|
||||
}
|
||||
}
|
||||
|
||||
// RabbitMQ
|
||||
if (entry.rabbitmq && entry.rabbitmq.trim() !== '') {
|
||||
services.rabbitmq = rabbitmqConfig.getConfig(entry.rabbitmq);
|
||||
}
|
||||
|
||||
// Cache: prefer valkey over redis
|
||||
const cache = getCacheChoice(entry);
|
||||
const cache = getCacheChoice(entry, preferences.cache);
|
||||
if (cache) {
|
||||
if (cache.type === 'valkey') {
|
||||
services.valkey = valkeyConfig.getConfig(cache.image);
|
||||
@@ -82,5 +114,12 @@ export function buildServicesForEntry(entry: PackageMatrixVersion): Services {
|
||||
}
|
||||
}
|
||||
|
||||
const nginxImage = (entry.nginx || '').trim();
|
||||
const phpVersion = String(entry.php ?? '').trim();
|
||||
if (nginxImage !== '' && phpVersion !== '') {
|
||||
services.nginx = buildNginxConfig(nginxImage, workspace);
|
||||
services['php-fpm'] = buildPhpFpmConfig(phpVersion, workspace);
|
||||
}
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export { Tier, ServicePreferences, KNOWN_SERVICE_NAMES, tierFor } from './tier-map';
|
||||
export { parseServicePreferences } from './parse-service-preferences';
|
||||
export { validatePreferencesAgainstMatrix } from './validate-preferences-against-matrix';
|
||||
@@ -0,0 +1,50 @@
|
||||
import { parseServicePreferences } from './parse-service-preferences';
|
||||
|
||||
describe('parseServicePreferences', () => {
|
||||
it('returns an empty map when input is empty', () => {
|
||||
expect(parseServicePreferences('')).toEqual({});
|
||||
});
|
||||
|
||||
it('returns an empty map for whitespace-only input', () => {
|
||||
expect(parseServicePreferences(' ')).toEqual({});
|
||||
});
|
||||
|
||||
it('maps a single name to its tier', () => {
|
||||
expect(parseServicePreferences('opensearch')).toEqual({ search: 'opensearch' });
|
||||
});
|
||||
|
||||
it('maps two names in different tiers', () => {
|
||||
expect(parseServicePreferences('elasticsearch,valkey')).toEqual({
|
||||
search: 'elasticsearch',
|
||||
cache: 'valkey',
|
||||
});
|
||||
});
|
||||
|
||||
it('accepts single-implementation-tier names as no-op-like preferences', () => {
|
||||
expect(parseServicePreferences('mysql,rabbitmq')).toEqual({
|
||||
db: 'mysql',
|
||||
queue: 'rabbitmq',
|
||||
});
|
||||
});
|
||||
|
||||
it('tolerates whitespace around names', () => {
|
||||
expect(parseServicePreferences(' opensearch , valkey ')).toEqual({
|
||||
search: 'opensearch',
|
||||
cache: 'valkey',
|
||||
});
|
||||
});
|
||||
|
||||
it('throws on unknown service name', () => {
|
||||
expect(() => parseServicePreferences('foobar')).toThrowError(/unknown service "foobar"/);
|
||||
});
|
||||
|
||||
it('throws on a collision in the search tier', () => {
|
||||
expect(() => parseServicePreferences('elasticsearch,opensearch')).toThrowError(
|
||||
/collision in tier "search"/
|
||||
);
|
||||
});
|
||||
|
||||
it('throws on a collision in the cache tier', () => {
|
||||
expect(() => parseServicePreferences('redis,valkey')).toThrowError(/collision in tier "cache"/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
import { ServicePreferences, SERVICE_TIER_MAP, KNOWN_SERVICE_NAMES } from './tier-map';
|
||||
|
||||
/**
|
||||
* Parses the comma-separated `service_preferences` input into a
|
||||
* tier-to-implementation map. Throws on unknown service names or when
|
||||
* two names collide in the same tier.
|
||||
*/
|
||||
export const parseServicePreferences = (raw: string): ServicePreferences => {
|
||||
const trimmed = (raw || '').trim();
|
||||
if (trimmed === '') return {};
|
||||
|
||||
const names = trimmed.split(',').map(s => s.trim()).filter(s => s !== '');
|
||||
const preferences: ServicePreferences = {};
|
||||
|
||||
for (const name of names) {
|
||||
const tier = SERVICE_TIER_MAP[name];
|
||||
if (!tier) {
|
||||
throw new Error(
|
||||
`service_preferences: unknown service "${name}". Known services: ${KNOWN_SERVICE_NAMES.join(', ')}`
|
||||
);
|
||||
}
|
||||
const existing = preferences[tier];
|
||||
if (existing) {
|
||||
throw new Error(
|
||||
`service_preferences: collision in tier "${tier}" — both "${existing}" and "${name}" specified`
|
||||
);
|
||||
}
|
||||
preferences[tier] = name;
|
||||
}
|
||||
|
||||
return preferences;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { tierFor } from './tier-map';
|
||||
|
||||
describe('tierFor', () => {
|
||||
it.each([
|
||||
['mysql', 'db'],
|
||||
['elasticsearch', 'search'],
|
||||
['opensearch', 'search'],
|
||||
['rabbitmq', 'queue'],
|
||||
['redis', 'cache'],
|
||||
['valkey', 'cache'],
|
||||
])('maps %s to %s', (name, tier) => {
|
||||
expect(tierFor(name)).toBe(tier);
|
||||
});
|
||||
|
||||
it('returns undefined for unknown names', () => {
|
||||
expect(tierFor('foobar')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* A category of services that Magento can choose between. Each tier
|
||||
* has one or more concrete implementations (e.g. the `search` tier
|
||||
* resolves to either `opensearch` or `elasticsearch`).
|
||||
*/
|
||||
export type Tier = 'db' | 'search' | 'queue' | 'cache';
|
||||
|
||||
/**
|
||||
* Caller-expressed preference of "which implementation to use for which tier."
|
||||
* Produced by `parseServicePreferences` from the `service_preferences`
|
||||
* action input, then threaded into `buildServicesForEntry` to override
|
||||
* the per-tier default pick. Partial because callers only need to name
|
||||
* the tiers they care about; unset tiers fall back to the version's
|
||||
* default implementation.
|
||||
*/
|
||||
export type ServicePreferences = Partial<Record<Tier, string>>;
|
||||
|
||||
/**
|
||||
* Reverse lookup: which tier does each known service implementation
|
||||
* belong to. The keys of this map define the closed set of legal
|
||||
* service names in `service_preferences`; anything not listed here is
|
||||
* rejected as an unknown service. nginx and php-fpm are intentionally
|
||||
* absent — they're complementary (the `web` tier emits both together),
|
||||
* not alternatives, so picking one via preferences would be meaningless.
|
||||
*/
|
||||
export const SERVICE_TIER_MAP: Record<string, Tier> = {
|
||||
mysql: 'db',
|
||||
elasticsearch: 'search',
|
||||
opensearch: 'search',
|
||||
rabbitmq: 'queue',
|
||||
redis: 'cache',
|
||||
valkey: 'cache',
|
||||
};
|
||||
|
||||
/**
|
||||
* Forward lookup: the implementations available within each tier,
|
||||
* ordered by default preference (first entry wins when no caller
|
||||
* preference is supplied). Used by the validator to render
|
||||
* "supported: a, b" lists in compatibility errors and by
|
||||
* `buildServicesForEntry` to know what to fall back to.
|
||||
*/
|
||||
export const TIER_IMPLEMENTATIONS: Record<Tier, string[]> = {
|
||||
db: ['mysql'],
|
||||
search: ['opensearch', 'elasticsearch'],
|
||||
queue: ['rabbitmq'],
|
||||
cache: ['valkey', 'redis'],
|
||||
};
|
||||
|
||||
/**
|
||||
* Flat list of every legal service name in `service_preferences`.
|
||||
* Surfaced in unknown-service error messages so the caller sees
|
||||
* exactly what's accepted without having to read source.
|
||||
*/
|
||||
export const KNOWN_SERVICE_NAMES = Object.keys(SERVICE_TIER_MAP);
|
||||
|
||||
/**
|
||||
* Returns the tier a service name belongs to, or `undefined` if the
|
||||
* name isn't recognized. Callers use the undefined return as the
|
||||
* signal to reject the input as unknown.
|
||||
*/
|
||||
export const tierFor = (serviceName: string): Tier | undefined => {
|
||||
return SERVICE_TIER_MAP[serviceName];
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
import { validatePreferencesAgainstMatrix } from './validate-preferences-against-matrix';
|
||||
import { PackageMatrixVersion } from '../../matrix/matrix-type';
|
||||
|
||||
const baseEntry = (overrides: Partial<PackageMatrixVersion> = {}): PackageMatrixVersion => ({
|
||||
magento: 'magento/project-community-edition:2.4.7',
|
||||
version: '2.4.7',
|
||||
php: '8.3',
|
||||
composer: '2.7.4',
|
||||
mysql: 'mysql:8.4',
|
||||
elasticsearch: 'elasticsearch:8.11.4',
|
||||
opensearch: 'opensearchproject/opensearch:2.19.1',
|
||||
rabbitmq: 'rabbitmq:4.0-management',
|
||||
redis: 'redis:7.2',
|
||||
varnish: 'varnish:7.5',
|
||||
valkey: 'valkey:8.0',
|
||||
nginx: 'nginx:1.26',
|
||||
os: 'ubuntu-latest',
|
||||
release: '2024-04-09T00:00:00+0000',
|
||||
eol: '2027-04-09T00:00:00+0000',
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe('validatePreferencesAgainstMatrix', () => {
|
||||
it('does not throw when all entries support the preference', () => {
|
||||
const entries = [baseEntry({ version: '2.4.7' }), baseEntry({ version: '2.4.6' })];
|
||||
expect(() => validatePreferencesAgainstMatrix({ search: 'opensearch' }, entries)).not.toThrow();
|
||||
});
|
||||
|
||||
it('throws when an entry lacks the preferenced implementation, listing the offender', () => {
|
||||
const entries = [
|
||||
baseEntry({ version: '2.4.7' }),
|
||||
baseEntry({ version: '2.4.5', opensearch: '' }),
|
||||
];
|
||||
expect(() => validatePreferencesAgainstMatrix({ search: 'opensearch' }, entries)).toThrowError(
|
||||
/not satisfied for:\n\s+- magento 2\.4\.5 \(supported: elasticsearch\)/
|
||||
);
|
||||
});
|
||||
|
||||
it('lists all offenders, not just the first', () => {
|
||||
const entries = [
|
||||
baseEntry({ version: '2.4.5', opensearch: '' }),
|
||||
baseEntry({ version: '2.4.4', opensearch: '' }),
|
||||
];
|
||||
expect(() => validatePreferencesAgainstMatrix({ search: 'opensearch' }, entries)).toThrowError(
|
||||
/magento 2\.4\.5[\s\S]*magento 2\.4\.4/
|
||||
);
|
||||
});
|
||||
|
||||
it('reports "<none>" when the entry supports nothing in the tier', () => {
|
||||
const entries = [baseEntry({ version: '2.4.0', opensearch: '', elasticsearch: '' })];
|
||||
expect(() => validatePreferencesAgainstMatrix({ search: 'opensearch' }, entries)).toThrowError(
|
||||
/magento 2\.4\.0 \(supported: <none>\)/
|
||||
);
|
||||
});
|
||||
|
||||
it('reports violations across multiple tiers', () => {
|
||||
const entries = [baseEntry({ version: '2.4.5', opensearch: '', valkey: '' })];
|
||||
let captured: Error | null = null;
|
||||
try {
|
||||
validatePreferencesAgainstMatrix({ search: 'opensearch', cache: 'valkey' }, entries);
|
||||
} catch (e) {
|
||||
captured = e as Error;
|
||||
}
|
||||
expect(captured).not.toBeNull();
|
||||
expect(captured!.message).toMatch(/opensearch/);
|
||||
expect(captured!.message).toMatch(/valkey/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
import { PackageMatrixVersion } from '../../matrix/matrix-type';
|
||||
import { ServicePreferences, Tier, TIER_IMPLEMENTATIONS } from './tier-map';
|
||||
|
||||
/**
|
||||
* Verifies that every preferenced implementation is supported by every
|
||||
* matrix entry. Collects all violations and throws a single
|
||||
* consolidated error.
|
||||
*/
|
||||
export const validatePreferencesAgainstMatrix = (
|
||||
preferences: ServicePreferences,
|
||||
entries: PackageMatrixVersion[]
|
||||
): void => {
|
||||
const errors: string[] = [];
|
||||
|
||||
for (const tierKey of Object.keys(preferences) as Tier[]) {
|
||||
const implementation = preferences[tierKey]!;
|
||||
const tierImplementations = TIER_IMPLEMENTATIONS[tierKey];
|
||||
|
||||
const offenders: { version: string; supported: string[] }[] = [];
|
||||
for (const entry of entries) {
|
||||
const value = entry[implementation as keyof PackageMatrixVersion];
|
||||
const isSupported = typeof value === 'string' && value.trim() !== '';
|
||||
if (!isSupported) {
|
||||
const supported = tierImplementations.filter(name => {
|
||||
const v = entry[name as keyof PackageMatrixVersion];
|
||||
return typeof v === 'string' && v.trim() !== '';
|
||||
});
|
||||
offenders.push({ version: entry.version, supported });
|
||||
}
|
||||
}
|
||||
|
||||
if (offenders.length > 0) {
|
||||
const list = offenders
|
||||
.map(o => ` - magento ${o.version} (supported: ${o.supported.length > 0 ? o.supported.join(', ') : '<none>'})`)
|
||||
.join('\n');
|
||||
errors.push(`service_preferences "${implementation}" is not satisfied for:\n${list}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw new Error(errors.join('\n\n'));
|
||||
}
|
||||
}
|
||||
@@ -80,4 +80,29 @@ export const valkeyConfig: ServiceTemplate = {
|
||||
ports: ['6379:6379']
|
||||
};
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds the nginx service config for the web tier. Takes the image
|
||||
* (sourced from the matrix entry's `nginx` field) and the runner
|
||||
* workspace path so the volume mount lands the Magento install at
|
||||
* `/var/www/html` inside the container. Pairs with `buildPhpFpmConfig`
|
||||
* — they're emitted together as the web tier.
|
||||
*/
|
||||
export const buildNginxConfig = (image: string, workspace: string): ServiceConfig => ({
|
||||
image,
|
||||
ports: ['80:80'],
|
||||
volumes: [`${workspace}:/var/www/html`],
|
||||
options: '--health-cmd "nginx -t" --health-interval=10s --health-retries=3 --health-timeout=5s --health-start-period=5s'
|
||||
});
|
||||
|
||||
/**
|
||||
* Builds the php-fpm service config for the web tier. Composes the
|
||||
* image from the matrix entry's `php` version (the mappia magento-php
|
||||
* image stream is the only widely-used Magento-aware php-fpm image).
|
||||
* Pairs with `buildNginxConfig`.
|
||||
*/
|
||||
export const buildPhpFpmConfig = (phpVersion: string, workspace: string): ServiceConfig => ({
|
||||
image: `mappia/magento-php:fpm-alpine${phpVersion}`,
|
||||
volumes: [`${workspace}:/var/www/html`]
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user