mirror of
https://github.com/graycoreio/github-actions-magento2.git
synced 2026-06-08 19:46:41 +00:00
Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a097371e37 | |||
| 5ee0768610 | |||
| 3c51e99538 | |||
| 32a5fd2bad | |||
| 91bd008e62 | |||
| 36953b919c | |||
| 83f9433da0 | |||
| a7d48f567e | |||
| 76eb9064ff | |||
| 147245e120 | |||
| 4df4b25e05 | |||
| fa8e597365 | |||
| 1ea5a10ef6 | |||
| 863444afbd | |||
| befe0807f7 | |||
| 1e63c019ed | |||
| 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 }}
|
||||
@@ -273,7 +301,7 @@ jobs:
|
||||
run: ../../../vendor/bin/phpunit -c phpunit.xml.dist --testsuite Extension_Integration_Tests
|
||||
|
||||
- name: Upload test sandbox dir
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
if: failure()
|
||||
with:
|
||||
name: sandbox-data-${{ steps.magento-version.outputs.version }}
|
||||
|
||||
@@ -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,30 @@ 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:
|
||||
project: ${{ steps.get-magento-version.outputs.supported_version_project }}
|
||||
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 +81,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 +90,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 +130,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 +144,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 +153,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 +178,72 @@ 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
|
||||
if: contains(fromJSON(needs.compute_matrix.outputs.resolved)['smoke-test'].probes, 'page')
|
||||
with:
|
||||
kind: page
|
||||
|
||||
## graphql is opt-in: editions without GraphQL modules (e.g. mage-os
|
||||
## minimal) have no /graphql endpoint. Enable it per store via
|
||||
## `.github/check-store.json` -> jobs.smoke-test.probes: ["page", "graphql"].
|
||||
- uses: graycoreio/github-actions-magento2/smoke-test@main
|
||||
if: contains(fromJSON(needs.compute_matrix.outputs.resolved)['smoke-test'].probes, 'graphql')
|
||||
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 }}
|
||||
@@ -182,7 +182,7 @@ jobs:
|
||||
name: Run Integration Tests
|
||||
|
||||
- name: Upload test sandbox dir
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
if: failure()
|
||||
with:
|
||||
name: sandbox-data-${{ steps.magento-version.outputs.version }}
|
||||
|
||||
@@ -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
|
||||
@@ -29,10 +28,10 @@ jobs:
|
||||
releases_created: ${{ steps.release.outputs.releases_created }}
|
||||
steps:
|
||||
- id: release
|
||||
uses: googleapis/release-please-action@v4
|
||||
uses: googleapis/release-please-action@v5
|
||||
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.5.0"}
|
||||
|
||||
@@ -2,6 +2,56 @@
|
||||
|
||||
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.5.0](https://github.com/graycoreio/github-actions-magento2/compare/v8.4.0...v8.5.0) (2026-05-27)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **setup-install:** run with --no-interaction ([32a5fd2](https://github.com/graycoreio/github-actions-magento2/commit/32a5fd2badfe558e7dced9606765d0d44632c6f0))
|
||||
|
||||
## [8.4.0](https://github.com/graycoreio/github-actions-magento2/compare/v8.3.0...v8.4.0) (2026-05-25)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* remove rabbitmq from supported-version for mage-os/minimal ([83f9433](https://github.com/graycoreio/github-actions-magento2/commit/83f9433da0d7f20efbf090fd8ed75a0a39000797))
|
||||
|
||||
## [8.3.0](https://github.com/graycoreio/github-actions-magento2/compare/v8.2.0...v8.3.0) (2026-05-25)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **check-store:** use the project when computing underlying version requirements ([fa8e597](https://github.com/graycoreio/github-actions-magento2/commit/fa8e59736563d5969f5c8ebaccd23c48f0628721))
|
||||
* **get-magento-version:** add support for MageOS minimal distro ([863444a](https://github.com/graycoreio/github-actions-magento2/commit/863444afbd137d32157392b964f06503f021ee6c))
|
||||
* **get-magento-version:** emit supported-version project name as an output ([1ea5a10](https://github.com/graycoreio/github-actions-magento2/commit/1ea5a10ef67d6fda8d10e078895adc9bea434477))
|
||||
* **supported-version:** add support for MageOS 3 ([1e63c01](https://github.com/graycoreio/github-actions-magento2/commit/1e63c019edb63ee0bcd4576b4125b73520ca8864))
|
||||
* **supported-version:** add support for MageOS Minimal edition ([befe080](https://github.com/graycoreio/github-actions-magento2/commit/befe0807f7636c125d7e650f2d08012b28554a54))
|
||||
|
||||
## [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.5.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.5.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.5.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.5.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.5.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 — see [`check-extension.schema.json`](../../resolve-check-config/check-extension.schema.json):
|
||||
|
||||
```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.5.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.5.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 — see [`check-store.schema.json`](../../resolve-check-config/check-store.schema.json):
|
||||
|
||||
```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.5.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.5.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.5.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.5.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.5.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.5.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.5.0 # x-release-please-version
|
||||
id: get-magento-version
|
||||
- run: echo version ${{ steps.get-magento-version.outputs.version }}
|
||||
shell: bash
|
||||
|
||||
@@ -15,6 +15,9 @@ outputs:
|
||||
project:
|
||||
description: 'The Magento project package name (e.g. magento/project-community-edition)'
|
||||
value: ${{ steps.get-magento-version.outputs.project }}
|
||||
supported_version_project:
|
||||
description: 'The `project` value to pass to the supported-version action (e.g. `magento-open-source`, `mage-os`, `mage-os-minimal`). Empty when no Magento project is detected.'
|
||||
value: ${{ steps.get-magento-version.outputs.supported_version_project }}
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"packages": [
|
||||
{
|
||||
"name": "mage-os/product-minimal-edition",
|
||||
"version": "3.0.0"
|
||||
}
|
||||
],
|
||||
"packages-dev": []
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
set -uo pipefail
|
||||
|
||||
WORKING_DIR="${1:-.}"
|
||||
PATTERN="magento/product-(community|enterprise)-edition|mage-os/product-community-edition"
|
||||
PATTERN="magento/product-(community|enterprise)-edition|mage-os/product-(community|minimal)-edition"
|
||||
|
||||
cd "$WORKING_DIR"
|
||||
|
||||
@@ -16,5 +16,13 @@ PRODUCT=$(echo "${RESULT:-}" | awk '{print $1}')
|
||||
VERSION=$(echo "${RESULT:-}" | awk '{print $2}' | sed 's/^v//')
|
||||
PROJECT=$(echo "$PRODUCT" | sed 's/product-/project-/')
|
||||
|
||||
case "$PROJECT" in
|
||||
magento/*) SUPPORTED_VERSION_PROJECT="magento-open-source" ;;
|
||||
mage-os/project-community-edition) SUPPORTED_VERSION_PROJECT="mage-os" ;;
|
||||
mage-os/project-minimal-edition) SUPPORTED_VERSION_PROJECT="mage-os-minimal" ;;
|
||||
*) SUPPORTED_VERSION_PROJECT="" ;;
|
||||
esac
|
||||
|
||||
echo "version=\"$VERSION\""
|
||||
echo "project=$PROJECT"
|
||||
echo "supported_version_project=$SUPPORTED_VERSION_PROJECT"
|
||||
|
||||
+23
-12
@@ -24,31 +24,42 @@ field() {
|
||||
}
|
||||
|
||||
OUT=$(bash "$SCRIPT" "$FIXTURES/store-lock")
|
||||
assert_eq "store lock: version" '"2.4.7"' "$(field "$OUT" version)"
|
||||
assert_eq "store lock: project" "magento/project-community-edition" "$(field "$OUT" project)"
|
||||
assert_eq "store lock: version" '"2.4.7"' "$(field "$OUT" version)"
|
||||
assert_eq "store lock: project" "magento/project-community-edition" "$(field "$OUT" project)"
|
||||
assert_eq "store lock: supported_version_project" "magento-open-source" "$(field "$OUT" supported_version_project)"
|
||||
|
||||
OUT=$(bash "$SCRIPT" "$FIXTURES/store-v-prefix")
|
||||
assert_eq "store v-prefix: version" '"2.4.6"' "$(field "$OUT" version)"
|
||||
|
||||
OUT=$(bash "$SCRIPT" "$FIXTURES/enterprise")
|
||||
assert_eq "enterprise: version" '"2.4.7-p1"' "$(field "$OUT" version)"
|
||||
assert_eq "enterprise: project" "magento/project-enterprise-edition" "$(field "$OUT" project)"
|
||||
assert_eq "enterprise: version" '"2.4.7-p1"' "$(field "$OUT" version)"
|
||||
assert_eq "enterprise: project" "magento/project-enterprise-edition" "$(field "$OUT" project)"
|
||||
assert_eq "enterprise: supported_version_project" "magento-open-source" "$(field "$OUT" supported_version_project)"
|
||||
|
||||
OUT=$(bash "$SCRIPT" "$FIXTURES/mage-os")
|
||||
assert_eq "mage-os: version" '"1.0.0"' "$(field "$OUT" version)"
|
||||
assert_eq "mage-os: project" "mage-os/project-community-edition" "$(field "$OUT" project)"
|
||||
assert_eq "mage-os: version" '"1.0.0"' "$(field "$OUT" version)"
|
||||
assert_eq "mage-os: project" "mage-os/project-community-edition" "$(field "$OUT" project)"
|
||||
assert_eq "mage-os: supported_version_project" "mage-os" "$(field "$OUT" supported_version_project)"
|
||||
|
||||
OUT=$(bash "$SCRIPT" "$FIXTURES/mage-os-minimal")
|
||||
assert_eq "mage-os-minimal: version" '"3.0.0"' "$(field "$OUT" version)"
|
||||
assert_eq "mage-os-minimal: project" "mage-os/project-minimal-edition" "$(field "$OUT" project)"
|
||||
assert_eq "mage-os-minimal: supported_version_project" "mage-os-minimal" "$(field "$OUT" supported_version_project)"
|
||||
|
||||
OUT=$(bash "$SCRIPT" "$FIXTURES/store-json")
|
||||
assert_eq "store json: version" '"2.4.6-p1"' "$(field "$OUT" version)"
|
||||
assert_eq "store json: project" "magento/project-community-edition" "$(field "$OUT" project)"
|
||||
assert_eq "store json: version" '"2.4.6-p1"' "$(field "$OUT" version)"
|
||||
assert_eq "store json: project" "magento/project-community-edition" "$(field "$OUT" project)"
|
||||
assert_eq "store json: supported_version_project" "magento-open-source" "$(field "$OUT" supported_version_project)"
|
||||
|
||||
OUT=$(bash "$SCRIPT" "$FIXTURES/extension")
|
||||
assert_eq "extension: version" '""' "$(field "$OUT" version)"
|
||||
assert_eq "extension: project" "" "$(field "$OUT" project)"
|
||||
assert_eq "extension: version" '""' "$(field "$OUT" version)"
|
||||
assert_eq "extension: project" "" "$(field "$OUT" project)"
|
||||
assert_eq "extension: supported_version_project" "" "$(field "$OUT" supported_version_project)"
|
||||
|
||||
OUT=$(bash "$SCRIPT" "$FIXTURES/empty")
|
||||
assert_eq "empty dir: version" '""' "$(field "$OUT" version)"
|
||||
assert_eq "empty dir: project" "" "$(field "$OUT" project)"
|
||||
assert_eq "empty dir: version" '""' "$(field "$OUT" version)"
|
||||
assert_eq "empty dir: project" "" "$(field "$OUT" project)"
|
||||
assert_eq "empty dir: supported_version_project" "" "$(field "$OUT" supported_version_project)"
|
||||
|
||||
echo ""
|
||||
echo "$PASS passed, $FAIL failed"
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@graycoreio/github-actions-magento2",
|
||||
"version": "8.0.0",
|
||||
"version": "8.5.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@graycoreio/github-actions-magento2",
|
||||
"version": "8.0.0",
|
||||
"version": "8.5.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.11.1",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@graycoreio/github-actions-magento2",
|
||||
"version": "8.0.0",
|
||||
"version": "8.5.0",
|
||||
"description": "Github Actions for Magento 2",
|
||||
"scripts": {
|
||||
"test": "cd supported-version && npm run test && cd - && cd setup-install && npm run test && cd -",
|
||||
|
||||
@@ -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,62 @@
|
||||
# "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.
|
||||
|
||||
## Schemas
|
||||
|
||||
Reference the published JSON Schema from your config's `$schema` key for autocompletion and inline validation in editors that support it:
|
||||
|
||||
- [`check-store.schema.json`](./check-store.schema.json) — config for the [MageCheck Store](../docs/workflows/check-store.md) workflow
|
||||
- [`check-extension.schema.json`](./check-extension.schema.json) — config for the [MageCheck Extension](../docs/workflows/check-extension.md) workflow
|
||||
|
||||
## 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.5.0 # x-release-please-version
|
||||
id: supported-version
|
||||
with:
|
||||
kind: currently-supported
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/resolve-check-config@v8.5.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,62 @@
|
||||
{
|
||||
"$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/smokeJobConfig" }
|
||||
},
|
||||
"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
|
||||
}
|
||||
]
|
||||
},
|
||||
"smokeJobConfig": {
|
||||
"description": "How the smoke-test job should be configured. Boolean form is shorthand for { enabled: <bool> }; object form adds a `probes` list on top of `enabled`.",
|
||||
"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
|
||||
},
|
||||
"probes": {
|
||||
"type": "array",
|
||||
"description": "Which smoke-test probes to run. Defaults to [\"page\"]. Add \"graphql\" to also probe the GraphQL endpoint — only for editions that ship GraphQL modules (the mage-os minimal edition does not, so /graphql 404s there).",
|
||||
"items": { "enum": ["page", "graphql"] },
|
||||
"default": ["page"]
|
||||
}
|
||||
},
|
||||
"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,122 @@
|
||||
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('defaults smoke-test to the page probe only (graphql is opt-in)', () => {
|
||||
expect(STORE_JOBS['smoke-test'].probes).toEqual(['page']);
|
||||
});
|
||||
|
||||
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('emits the default page-only probe list for smoke-test', () => {
|
||||
const resolved = resolveStoreConfig({}, MATRIX);
|
||||
expect(resolved['smoke-test'].probes).toEqual(['page']);
|
||||
});
|
||||
|
||||
it('honors a smoke-test probes override', () => {
|
||||
const resolved = resolveStoreConfig(
|
||||
{ jobs: { 'smoke-test': { probes: ['page', 'graphql'] } } },
|
||||
MATRIX,
|
||||
);
|
||||
expect(resolved['smoke-test'].probes).toEqual(['page', 'graphql']);
|
||||
});
|
||||
|
||||
it('does not emit probes on jobs without a probe concept', () => {
|
||||
const resolved = resolveStoreConfig({}, MATRIX);
|
||||
expect(resolved['unit-test'].probes).toBeUndefined();
|
||||
expect(resolved['coding-standard'].probes).toBeUndefined();
|
||||
});
|
||||
|
||||
it('rejects probes on a job that does not support it', () => {
|
||||
expect(() => resolveStoreConfig({ jobs: { 'unit-test': { probes: ['page'] } } }, MATRIX)).toThrowError(
|
||||
/job "unit-test" does not support "probes"/
|
||||
);
|
||||
});
|
||||
|
||||
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,29 @@
|
||||
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'],
|
||||
probes: ['page'],
|
||||
},
|
||||
};
|
||||
|
||||
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,367 @@
|
||||
import {
|
||||
filterEntryServices,
|
||||
filterMatrixForJob,
|
||||
mergeRequiredTiers,
|
||||
normalizeJobEntry,
|
||||
normalizeProbes,
|
||||
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'] };
|
||||
const probeDefaults: JobDefaults = { services: [], probes: ['page'] };
|
||||
|
||||
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"/
|
||||
);
|
||||
});
|
||||
|
||||
it('carries the default probes when the entry omits them', () => {
|
||||
expect(normalizeJobEntry('smoke-test', { services: [] }, probeDefaults)).toEqual({
|
||||
enabled: true,
|
||||
tiers: [],
|
||||
probes: ['page'],
|
||||
});
|
||||
});
|
||||
|
||||
it('carries the default probes for the boolean shorthand', () => {
|
||||
expect(normalizeJobEntry('smoke-test', true, probeDefaults)).toEqual({
|
||||
enabled: true,
|
||||
tiers: [],
|
||||
probes: ['page'],
|
||||
});
|
||||
});
|
||||
|
||||
it('overrides the default probes when probes is set', () => {
|
||||
expect(normalizeJobEntry('smoke-test', { probes: ['page', 'graphql'] }, probeDefaults)).toEqual({
|
||||
enabled: true,
|
||||
tiers: [],
|
||||
probes: ['page', 'graphql'],
|
||||
});
|
||||
});
|
||||
|
||||
it('omits probes for a job that declares no probe defaults', () => {
|
||||
expect(normalizeJobEntry('unit-test', { services: [] }, noDefaults).probes).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('normalizeProbes', () => {
|
||||
it('returns the defaults when probes is omitted', () => {
|
||||
expect(normalizeProbes('smoke-test', undefined, ['page'])).toEqual(['page']);
|
||||
});
|
||||
|
||||
it('returns undefined for a job with no probe defaults when omitted', () => {
|
||||
expect(normalizeProbes('unit-test', undefined, undefined)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('throws when probes is set on a job that does not support it', () => {
|
||||
expect(() => normalizeProbes('unit-test', ['page'], undefined)).toThrowError(
|
||||
/job "unit-test" does not support "probes"/
|
||||
);
|
||||
});
|
||||
|
||||
it('throws when probes is not an array', () => {
|
||||
expect(() => normalizeProbes('smoke-test', 'page', ['page'])).toThrowError(
|
||||
/probes must be an array of probe names/
|
||||
);
|
||||
});
|
||||
|
||||
it('throws when probes contains an unknown probe', () => {
|
||||
expect(() => normalizeProbes('smoke-test', ['rest'], ['page'])).toThrowError(
|
||||
/probes contains unknown probe "rest"/
|
||||
);
|
||||
});
|
||||
|
||||
it('accepts an empty probes array', () => {
|
||||
expect(normalizeProbes('smoke-test', [], ['page'])).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
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,211 @@
|
||||
import { JobDefaults, Kind, Matrix, MatrixEntry, RawConfig, RawJobConfig, ResolvedConfig, ResolvedJobConfig, Services } from './types';
|
||||
import { isTier, servicesForTiers, Tier } from './tier-map';
|
||||
import { isProbe, Probe } from './probe';
|
||||
|
||||
/**
|
||||
* Normalizes the `probes` value from a job entry. Returns the
|
||||
* caller's list when present (validated), the job's default probe
|
||||
* list when omitted, or `undefined` for jobs that have no probe
|
||||
* concept. Throws if a job without probe defaults is given `probes`.
|
||||
*/
|
||||
export const normalizeProbes = (
|
||||
jobName: string,
|
||||
raw: unknown,
|
||||
defaults: readonly Probe[] | undefined,
|
||||
): readonly Probe[] | undefined => {
|
||||
if (raw === undefined) {
|
||||
return defaults;
|
||||
}
|
||||
if (defaults === undefined) {
|
||||
throw new Error(`check-config: job "${jobName}" does not support "probes"`);
|
||||
}
|
||||
if (!Array.isArray(raw)) {
|
||||
throw new Error(`check-config: job "${jobName}".probes must be an array of probe names`);
|
||||
}
|
||||
const probes: Probe[] = [];
|
||||
for (const value of raw) {
|
||||
if (!isProbe(value)) {
|
||||
throw new Error(`check-config: job "${jobName}".probes contains unknown probe "${String(value)}"`);
|
||||
}
|
||||
probes.push(value);
|
||||
}
|
||||
return probes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes a single raw job entry to (enabled, tiers, probes).
|
||||
* Accepts the boolean shorthand and the object form. Validates the
|
||||
* shape, the `services` tier list, and the `probes` list; throws on
|
||||
* unexpected input. The caller supplies the per-job defaults, used
|
||||
* when `services`/`probes` are omitted from the entry. `probes` is
|
||||
* `undefined` for jobs that declare no probe defaults.
|
||||
*/
|
||||
export const normalizeJobEntry = (
|
||||
jobName: string,
|
||||
raw: RawJobConfig | undefined,
|
||||
defaults: JobDefaults,
|
||||
): { enabled: boolean; tiers: readonly Tier[]; probes?: readonly Probe[] } => {
|
||||
if (raw === undefined) {
|
||||
return { enabled: true, tiers: defaults.services, probes: defaults.probes };
|
||||
}
|
||||
if (typeof raw === 'boolean') {
|
||||
return { enabled: raw, tiers: defaults.services, probes: defaults.probes };
|
||||
}
|
||||
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, probes } = raw as { enabled?: unknown; services?: unknown; probes?: unknown };
|
||||
const enabledValue = enabled === undefined ? true : Boolean(enabled);
|
||||
const resolvedProbes = normalizeProbes(jobName, probes, defaults.probes);
|
||||
|
||||
if (services === undefined) {
|
||||
return { enabled: enabledValue, tiers: defaults.services, probes: resolvedProbes };
|
||||
}
|
||||
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, probes: resolvedProbes };
|
||||
}
|
||||
|
||||
/**
|
||||
* 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, probes } = normalizeJobEntry(name, entry, defaults);
|
||||
const finalTiers = mergeRequiredTiers(tiers, defaults.requiredServices);
|
||||
const resolvedEntry: ResolvedJobConfig = {
|
||||
enabled,
|
||||
matrix: filterMatrixForJob(matrix, finalTiers),
|
||||
};
|
||||
if (probes !== undefined) {
|
||||
resolvedEntry.probes = [...probes];
|
||||
}
|
||||
resolved[name] = resolvedEntry;
|
||||
}
|
||||
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,13 @@
|
||||
/**
|
||||
* A smoke-test probe the check-store workflow can run against a
|
||||
* running store. `page` does a GET / and asserts a non-empty title;
|
||||
* `graphql` POSTs a storeConfig query to /graphql. Probes are opt-in
|
||||
* per job because not every edition exposes every surface (e.g. the
|
||||
* mage-os minimal edition ships no GraphQL modules, so /graphql 404s).
|
||||
*/
|
||||
export const PROBES = ['page', 'graphql'] as const;
|
||||
|
||||
export type Probe = (typeof PROBES)[number];
|
||||
|
||||
export const isProbe = (value: unknown): value is Probe =>
|
||||
typeof value === 'string' && (PROBES as readonly string[]).includes(value);
|
||||
@@ -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,108 @@
|
||||
import { Tier } from './tier-map';
|
||||
import { Probe } from './probe';
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* `probes` is the default smoke-test probe list used when the caller
|
||||
* does not override it. Only jobs that declare it support the
|
||||
* `probes` config key; omit it for jobs that have no probe concept.
|
||||
*/
|
||||
export interface JobDefaults {
|
||||
services: readonly Tier[];
|
||||
requiredServices?: readonly Tier[];
|
||||
probes?: readonly Probe[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
probes?: Probe[];
|
||||
[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` and an optional probe list under `probes` (both
|
||||
* validated against the per-kind schema).
|
||||
*/
|
||||
export type RawJobConfig =
|
||||
| boolean
|
||||
| { enabled?: boolean; services?: string[]; probes?: 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.5.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.5.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.5.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.5.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.5.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.5.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.5.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.5.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.5.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
@@ -8,6 +8,7 @@ const BASE_ARGS = [
|
||||
'--admin-firstname=Admin',
|
||||
'--admin-lastname=User',
|
||||
'--backend-frontname=admin',
|
||||
'--no-interaction',
|
||||
];
|
||||
|
||||
const MYSQL_SERVICE = {
|
||||
@@ -60,6 +61,10 @@ describe('buildInstallArgs', () => {
|
||||
it('returns only base args when services is empty', () => {
|
||||
expect(buildInstallArgs({})).toEqual(BASE_ARGS);
|
||||
});
|
||||
|
||||
it('runs non-interactively', () => {
|
||||
expect(buildInstallArgs(null)).toContain('--no-interaction');
|
||||
});
|
||||
});
|
||||
|
||||
describe('mysql', () => {
|
||||
@@ -173,4 +178,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 = [
|
||||
@@ -22,23 +24,32 @@ const BASE_ARGS = [
|
||||
'--admin-firstname=Admin',
|
||||
'--admin-lastname=User',
|
||||
'--backend-frontname=admin',
|
||||
'--no-interaction',
|
||||
];
|
||||
|
||||
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 +57,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.5.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.5.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"
|
||||
@@ -14,10 +14,11 @@ See the [action.yml](./action.yml)
|
||||
| Input | Description | Required | Default |
|
||||
|-----------------| ------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------- |-----------------------|
|
||||
| kind | The "kind" of support you're targeting for your package. See [Kinds](#kinds). | false | 'currently-supported' |
|
||||
| project | The project to return the supported versions for. Allowed values are `mage-os` and `magento-open-source` | false | 'magento-open-source' |
|
||||
| project | The project to return the supported versions for. Allowed values are `mage-os`, `mage-os-minimal`, and `magento-open-source` | false | 'magento-open-source' |
|
||||
| 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.
|
||||
@@ -30,8 +31,50 @@ See the [action.yml](./action.yml)
|
||||
|
||||
## Projects
|
||||
- `mage-os`
|
||||
- `mage-os-minimal`
|
||||
- `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.5.0 # x-release-please-version
|
||||
id: supported-version
|
||||
with:
|
||||
kind: currently-supported
|
||||
service_preferences: opensearch,valkey
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```yml
|
||||
@@ -51,7 +94,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.5.0 # x-release-please-version
|
||||
id: supported-version
|
||||
- run: echo ${{ steps.supported-version.outputs.matrix }}
|
||||
```
|
||||
|
||||
@@ -9,7 +9,7 @@ inputs:
|
||||
default: "currently-supported"
|
||||
project:
|
||||
required: false
|
||||
description: "The project to return the supported versions for. Allowed values are `mage-os` and `magento-open-source`"
|
||||
description: "The project to return the supported versions for. Allowed values are `mage-os`, `mage-os-minimal`, and `magento-open-source`"
|
||||
# The default value is what it is to keep backward compatibility
|
||||
default: "magento-open-source"
|
||||
custom_versions:
|
||||
@@ -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
+28
-24
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()
|
||||
|
||||
@@ -145,6 +145,12 @@ describe('getCurrentlySupportedVersions for mage-os', () => {
|
||||
['2026-04-15T00:00:01Z', 'Release of 2.2.2', [
|
||||
'mage-os/project-community-edition:2.2.2',
|
||||
]],
|
||||
['2026-05-13T00:00:01Z', 'Release of 2.3.0', [
|
||||
'mage-os/project-community-edition:2.3.0',
|
||||
]],
|
||||
['2026-05-19T00:00:01Z', 'Release of 3.0.0', [
|
||||
'mage-os/project-community-edition:3.0.0',
|
||||
]],
|
||||
])(
|
||||
'supportedVersions for %s',
|
||||
(date, description ,result) => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"mage-os": ["mage-os/project-community-edition"],
|
||||
"mage-os-minimal": ["mage-os/project-minimal-edition"],
|
||||
"magento-open-source": ["magento/project-community-edition"]
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"mage-os": ["mage-os/project-community-edition:next"],
|
||||
"mage-os-minimal": ["mage-os/project-minimal-edition:next"],
|
||||
"magento-open-source": ["magento/project-community-edition:next"]
|
||||
}
|
||||
@@ -3,6 +3,7 @@ export interface ServiceConfig {
|
||||
env?: Record<string, string>;
|
||||
ports?: string[];
|
||||
options?: string;
|
||||
volumes?: string[];
|
||||
}
|
||||
|
||||
export interface Services {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
*/
|
||||
export const KNOWN_PROJECTS = {
|
||||
"mage-os": true,
|
||||
"mage-os-minimal": true,
|
||||
"magento-open-source": true,
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ describe('validateProject', () => {
|
||||
it('returns `true` if its a valid project', () => {
|
||||
expect(validateProject("magento-open-source")).toBe(true);
|
||||
expect(validateProject("mage-os")).toBe(true);
|
||||
expect(validateProject("mage-os-minimal")).toBe(true);
|
||||
});
|
||||
|
||||
it('throws a helpful exception if it is an invalid project', () => {
|
||||
|
||||
@@ -4,6 +4,7 @@ import {Project} from "../projects";
|
||||
describe('isKnownProject', () => {
|
||||
it('returns `true` for known projects', () => {
|
||||
expect(isKnownProject("mage-os")).toBe(true)
|
||||
expect(isKnownProject("mage-os-minimal")).toBe(true)
|
||||
expect(isKnownProject("magento-open-source")).toBe(true)
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { buildServicesForEntry } from './build-services';
|
||||
import { PackageMatrixVersion } from '../matrix/matrix-type';
|
||||
import mageOsMinimalIndividual from '../versions/mage-os-minimal/individual.json';
|
||||
import mageOsMinimalComposite from '../versions/mage-os-minimal/composite.json';
|
||||
|
||||
const createTestEntry = (overrides: Partial<PackageMatrixVersion> = {}): PackageMatrixVersion => ({
|
||||
magento: 'magento/project-community-edition:2.4.7',
|
||||
@@ -223,13 +225,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 +245,136 @@ 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);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mage-os-minimal', () => {
|
||||
const minimalEntries: [string, PackageMatrixVersion][] = [
|
||||
...Object.entries(mageOsMinimalIndividual as unknown as Record<string, PackageMatrixVersion>),
|
||||
...Object.entries(mageOsMinimalComposite as unknown as Record<string, PackageMatrixVersion>)
|
||||
];
|
||||
|
||||
it.each(minimalEntries)('omits rabbitmq from services for %s', (_key, entry) => {
|
||||
const services = buildServicesForEntry(entry);
|
||||
|
||||
expect(services.rabbitmq).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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`]
|
||||
});
|
||||
|
||||
@@ -5,8 +5,9 @@ describe('getIndividialVersionsForProject', () => {
|
||||
it('returns individual versions matrix for magento-open-source', () => {
|
||||
expect(Object.keys(getIndividualVersionsForProject("magento-open-source")).length).toBeGreaterThan(0)
|
||||
expect(Object.keys(getIndividualVersionsForProject("mage-os")).length).toBeGreaterThan(0)
|
||||
expect(Object.keys(getIndividualVersionsForProject("mage-os-minimal")).length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
|
||||
it('throws error if no individual versions are specified for given project', () => {
|
||||
expect(() => getIndividualVersionsForProject(<Project>"ahsoka")).toThrowError()
|
||||
})
|
||||
@@ -16,6 +17,7 @@ describe('getCompositeVersionsForProject', () => {
|
||||
it('returns composite versions matrix for magento-open-source', () => {
|
||||
expect(Object.keys(getCompositeVersionsForProject("magento-open-source")).length).toBeGreaterThan(0)
|
||||
expect(Object.keys(getCompositeVersionsForProject("mage-os")).length).toBeGreaterThan(0)
|
||||
expect(Object.keys(getCompositeVersionsForProject("mage-os-minimal")).length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('throws error if no composite versions are specified for given project', () => {
|
||||
|
||||
@@ -3,11 +3,13 @@ import { PackageMatrixVersion } from "../matrix/matrix-type";
|
||||
|
||||
const individual = {
|
||||
'mage-os': require('./mage-os/individual.json'),
|
||||
'mage-os-minimal': require('./mage-os-minimal/individual.json'),
|
||||
'magento-open-source': require('./magento-open-source/individual.json')
|
||||
}
|
||||
|
||||
const composite = {
|
||||
'mage-os': require('./mage-os/composite.json'),
|
||||
'mage-os-minimal': require('./mage-os-minimal/composite.json'),
|
||||
'magento-open-source': require('./magento-open-source/composite.json')
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"mage-os/project-minimal-edition": {
|
||||
"magento": "mage-os/project-minimal-edition",
|
||||
"php": 8.5,
|
||||
"composer": "2.9.8",
|
||||
"mysql": "mysql:8.4",
|
||||
"opensearch": "opensearchproject/opensearch:3",
|
||||
"valkey": "valkey/valkey:9",
|
||||
"varnish": "varnish:8",
|
||||
"nginx": "nginx:1.28",
|
||||
"os": "ubuntu-latest",
|
||||
"release": "2026-05-19T00:00:00+0000",
|
||||
"eol": "2029-05-19T00:00:00+0000"
|
||||
},
|
||||
"mage-os/project-minimal-edition:next": {
|
||||
"magento": "mage-os/project-minimal-edition:next",
|
||||
"php": 8.5,
|
||||
"composer": "2.9.8",
|
||||
"mysql": "mysql:8.4",
|
||||
"opensearch": "opensearchproject/opensearch:3",
|
||||
"valkey": "valkey/valkey:9",
|
||||
"varnish": "varnish:8",
|
||||
"nginx": "nginx:1.28",
|
||||
"os": "ubuntu-latest",
|
||||
"release": "2026-05-19T00:00:00+0000",
|
||||
"eol": "2029-05-19T00:00:00+0000"
|
||||
},
|
||||
"mage-os/project-minimal-edition:>=3.0 <3.1": {
|
||||
"magento": "mage-os/project-minimal-edition:>=3.0 <3.1",
|
||||
"php": 8.5,
|
||||
"composer": "2.9.8",
|
||||
"mysql": "mysql:8.4",
|
||||
"opensearch": "opensearchproject/opensearch:3",
|
||||
"valkey": "valkey/valkey:9",
|
||||
"varnish": "varnish:8",
|
||||
"nginx": "nginx:1.28",
|
||||
"os": "ubuntu-latest",
|
||||
"release": "2026-05-19T00:00:00+0000",
|
||||
"eol": "2029-05-19T00:00:00+0000"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"mage-os/project-minimal-edition:3.0.0": {
|
||||
"magento": "mage-os/project-minimal-edition:3.0.0",
|
||||
"upstream": "2.4.9",
|
||||
"php": 8.5,
|
||||
"composer": "2.9.8",
|
||||
"mysql": "mysql:8.4",
|
||||
"opensearch": "opensearchproject/opensearch:3",
|
||||
"valkey": "valkey/valkey:9",
|
||||
"varnish": "varnish:8",
|
||||
"nginx": "nginx:1.28",
|
||||
"os": "ubuntu-latest",
|
||||
"release": "2026-05-19T00:00:00+0000",
|
||||
"eol": "2029-05-19T00:00:00+0000"
|
||||
}
|
||||
}
|
||||
@@ -6,13 +6,12 @@
|
||||
"mysql": "mysql:8.4",
|
||||
"opensearch": "opensearchproject/opensearch:3",
|
||||
"rabbitmq": "rabbitmq:4.1-management",
|
||||
"valkey": "valkey/valkey:8.0",
|
||||
"redis": "redis:7.2",
|
||||
"valkey": "valkey/valkey:8",
|
||||
"varnish": "varnish:7.7",
|
||||
"nginx": "nginx:1.28",
|
||||
"os": "ubuntu-latest",
|
||||
"release": "2026-04-15T00:00:00+0000",
|
||||
"eol": "2029-04-15T00:00:00+0000"
|
||||
"release": "2026-05-19T00:00:00+0000",
|
||||
"eol": "2029-05-19T00:00:00+0000"
|
||||
},
|
||||
"mage-os/project-community-edition:next": {
|
||||
"magento": "mage-os/project-community-edition:next",
|
||||
@@ -21,13 +20,12 @@
|
||||
"mysql": "mysql:8.4",
|
||||
"opensearch": "opensearchproject/opensearch:3",
|
||||
"rabbitmq": "rabbitmq:4.1-management",
|
||||
"valkey": "valkey/valkey:8.0",
|
||||
"redis": "redis:7.2",
|
||||
"valkey": "valkey/valkey:8",
|
||||
"varnish": "varnish:7.7",
|
||||
"nginx": "nginx:1.28",
|
||||
"os": "ubuntu-latest",
|
||||
"release": "2026-04-15T00:00:00+0000",
|
||||
"eol": "2029-04-15T00:00:00+0000"
|
||||
"release": "2026-05-19T00:00:00+0000",
|
||||
"eol": "2029-05-19T00:00:00+0000"
|
||||
},
|
||||
"mage-os/project-community-edition:>=1.0 <1.1": {
|
||||
"magento": "mage-os/project-community-edition:>=1.0 <1.1",
|
||||
@@ -126,6 +124,34 @@
|
||||
"nginx": "nginx:1.28",
|
||||
"os": "ubuntu-latest",
|
||||
"release": "2026-03-10T00:00:00+0000",
|
||||
"eol": "2029-04-15T00:00:00+0000"
|
||||
"eol": "2026-05-13T00:00:00+0000"
|
||||
},
|
||||
"mage-os/project-community-edition:>=2.3 <2.4": {
|
||||
"magento": "mage-os/project-community-edition:>=2.3 <2.4",
|
||||
"php": 8.4,
|
||||
"composer": "2.9.8",
|
||||
"mysql": "mysql:8.4",
|
||||
"opensearch": "opensearchproject/opensearch:2.19.1",
|
||||
"rabbitmq": "rabbitmq:4.0-management",
|
||||
"redis": "redis:7.2",
|
||||
"varnish": "varnish:7.6",
|
||||
"nginx": "nginx:1.26",
|
||||
"os": "ubuntu-latest",
|
||||
"release": "2026-05-13T00:00:00+0000",
|
||||
"eol": "2026-05-19T00:00:00+0000"
|
||||
},
|
||||
"mage-os/project-community-edition:>=3.0 <3.1": {
|
||||
"magento": "mage-os/project-community-edition:>=3.0 <3.1",
|
||||
"php": 8.5,
|
||||
"composer": "2.9.8",
|
||||
"mysql": "mysql:8.4",
|
||||
"opensearch": "opensearchproject/opensearch:3",
|
||||
"rabbitmq": "rabbitmq:4.2-management",
|
||||
"valkey": "valkey/valkey:9",
|
||||
"varnish": "varnish:8",
|
||||
"nginx": "nginx:1.28",
|
||||
"os": "ubuntu-latest",
|
||||
"release": "2026-05-19T00:00:00+0000",
|
||||
"eol": "2029-05-19T00:00:00+0000"
|
||||
}
|
||||
}
|
||||
@@ -253,6 +253,36 @@
|
||||
"nginx": "nginx:1.28",
|
||||
"os": "ubuntu-latest",
|
||||
"release": "2026-04-15T00:00:00+0000",
|
||||
"eol": "2029-04-15T00:00:00+0000"
|
||||
"eol": "2026-05-13T00:00:00+0000"
|
||||
},
|
||||
"mage-os/project-community-edition:2.3.0": {
|
||||
"magento": "mage-os/project-community-edition:2.3.0",
|
||||
"upstream": "2.4.8-p5",
|
||||
"php": 8.4,
|
||||
"composer": "2.9.8",
|
||||
"mysql": "mysql:8.4",
|
||||
"opensearch": "opensearchproject/opensearch:2.19.1",
|
||||
"rabbitmq": "rabbitmq:4.0-management",
|
||||
"redis": "redis:7.2",
|
||||
"varnish": "varnish:7.6",
|
||||
"nginx": "nginx:1.26",
|
||||
"os": "ubuntu-latest",
|
||||
"release": "2026-05-13T00:00:00+0000",
|
||||
"eol": "2026-05-19T00:00:00+0000"
|
||||
},
|
||||
"mage-os/project-community-edition:3.0.0": {
|
||||
"magento": "mage-os/project-community-edition:3.0.0",
|
||||
"upstream": "2.4.9",
|
||||
"php": 8.5,
|
||||
"composer": "2.9.8",
|
||||
"mysql": "mysql:8.4",
|
||||
"opensearch": "opensearchproject/opensearch:3",
|
||||
"rabbitmq": "rabbitmq:4.2-management",
|
||||
"valkey": "valkey/valkey:9",
|
||||
"varnish": "varnish:8",
|
||||
"nginx": "nginx:1.28",
|
||||
"os": "ubuntu-latest",
|
||||
"release": "2026-05-19T00:00:00+0000",
|
||||
"eol": "2029-05-19T00:00:00+0000"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user