Compare commits

...

38 Commits

Author SHA1 Message Date
GrayBot 36953b919c chore: release 8.4.0 (#282) 2026-05-25 16:14:12 -04:00
Damien Retzinger 83f9433da0 feat: remove rabbitmq from supported-version for mage-os/minimal 2026-05-25 16:09:31 -04:00
dependabot[bot] a7d48f567e build(deps): bump googleapis/release-please-action from 4 to 5 (#273)
Bumps [googleapis/release-please-action](https://github.com/googleapis/release-please-action) from 4 to 5.
- [Release notes](https://github.com/googleapis/release-please-action/releases)
- [Changelog](https://github.com/googleapis/release-please-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/googleapis/release-please-action/compare/v4...v5)

---
updated-dependencies:
- dependency-name: googleapis/release-please-action
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-25 09:34:12 -04:00
dependabot[bot] 76eb9064ff build(deps): bump actions/upload-artifact from 6 to 7 (#272)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 6 to 7.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-25 09:34:00 -04:00
GrayBot 147245e120 chore: restore internal action refs to @main (#281) 2026-05-25 09:33:37 -04:00
GrayBot 4df4b25e05 chore: release 8.3.0 (#280) 2026-05-25 09:31:20 -04:00
Damien Retzinger fa8e597365 feat(check-store): use the project when computing underlying version requirements 2026-05-25 09:27:28 -04:00
Damien Retzinger 1ea5a10ef6 feat(get-magento-version): emit supported-version project name as an output 2026-05-25 09:26:33 -04:00
Damien Retzinger 863444afbd feat(get-magento-version): add support for MageOS minimal distro 2026-05-25 09:15:50 -04:00
Damien Retzinger befe0807f7 feat(supported-version): add support for MageOS Minimal edition 2026-05-25 09:11:52 -04:00
Marcel 1e63c019ed feat(supported-version): add support for MageOS 3
Co-authored-by: Damien Retzinger <damienwebdev@gmail.com>
2026-05-25 08:57:42 -04:00
GrayBot ebfdeb0b73 chore: restore internal action refs to @main (#271) 2026-05-17 19:18:30 -04:00
GrayBot 8a0f197a13 chore: release 8.2.0 (#270) 2026-05-17 19:17:32 -04:00
Damien Retzinger 0bf08ef692 feat(check-extension): allow configuraton via .github/check-extension.json (#269) 2026-05-17 19:09:18 -04:00
Damien Retzinger 35c1ace2bc feat(resolve-check-config): defined required integration test services required (#269) 2026-05-17 19:09:13 -04:00
GrayBot d07afbacd0 chore: restore internal action refs to @main (#268) 2026-05-17 18:02:52 -04:00
GrayBot b71bb8b4aa chore: release 8.1.0 (#266) 2026-05-17 17:43:18 -04:00
Damien Retzinger e39dd46f9c feat(check-store): add smoke-test action and use resolve-check-config (#255) 2026-05-17 17:40:33 -04:00
Damien Retzinger b98313e100 feat(resolve-check-config): add ability to use a config file to adjust jobs (#255) 2026-05-17 17:18:50 -04:00
Damien Retzinger 0c7d14d885 feat(configure-service-nginx): add ability to adjust nginx conf after init (#255) 2026-05-17 17:18:48 -04:00
Damien Retzinger 6d4ca8d669 feat(setup-install): add a container_id input to run setup:install against a specific container (#255) 2026-05-17 17:18:46 -04:00
Damien Retzinger b790da1859 feat(smoke-test): add simple smoke test action (#255) 2026-05-17 17:18:43 -04:00
Damien Retzinger e89f6ad2e0 feat(supported-version): add service_preferences and support for php-fpm and nginx (#255) 2026-05-17 17:18:40 -04:00
Damien Retzinger 8e82fcc893 fix(check-extension): only run coding-standard on most recent version of Magento (#265) 2026-05-16 12:44:56 -04:00
Damien Retzinger 83ef32c838 ci: remove awkward commit requirements for graduating releases 2026-05-14 17:25:52 -04:00
GrayBot de7c47f07d chore: restore internal action refs to @main (#264) 2026-05-14 13:53:32 -04:00
GrayBot a2e3e7758b chore: release 8.0.0 (#263) 2026-05-14 13:52:22 -04:00
GrayBot e6bb7be524 chore: graduate 8.0.0-rc.2 to 8.0.0 (#262)
Release-As: 8.0.0
2026-05-14 13:51:09 -04:00
Damien Retzinger 3e9f95ee56 ci: add a graduation mode to release please 2026-05-14 13:49:47 -04:00
GrayBot 9c56da774b chore: restore internal action refs to @main (#260) 2026-05-13 19:58:46 -04:00
GrayBot 8c4fefd979 chore: release 8.0.0-rc.2 (#259) 2026-05-13 17:17:12 -04:00
Damien Retzinger d1a31d260d feat(supported-versions)!: forcibly bump all packages to the latest relevant release line of composer for LogLeak
This may break users who rely on older version of composer to work in their apps. Unfortunately, the risk is too high.
2026-05-13 08:13:34 -04:00
Damien Retzinger d37f001ab6 feat(supported-versions)!: updates for 2.4.9, 2.4.8-p5, 2.4.7-p19, 2.4.6-p15 (#258)
BREAKING CHANGE: This release brings support for the v2.4.9 version of Magento. This also brings backwards-incompatible infrastructure changes for the patch versions of Magento. See https://github.com/graycoreio/github-actions-magento2/pull/258 for more information.
2026-05-12 21:33:18 -04:00
Damien Retzinger c5221f0d68 feat(check-extension): pass along COMPOSER_AUTH where needed (#258) 2026-05-12 21:33:12 -04:00
Damien Retzinger 1fcb3618c0 ci: pass along COMPOSER_AUTH where needed (#258) 2026-05-12 21:33:09 -04:00
Damien Retzinger 4fc491bc1a fix(check-store): prevent error in phpunit 12 if no tests exists (#258) 2026-05-12 21:32:55 -04:00
Damien Retzinger 5df6c1a0bd docs: add new version skill 2026-05-12 11:51:26 -04:00
GrayBot ff5f76339b chore: restore internal action refs to @main (#257) 2026-05-10 18:40:04 -04:00
101 changed files with 3210 additions and 330 deletions
+112
View File
@@ -0,0 +1,112 @@
---
name: new-versions
description: Check magento/magento2 for newly published tags and add any missing entries to supported-version/src/versions/magento-open-source/{individual,composite}.json using Adobe's system requirements docs. Use when user wants to refresh Magento Open Source version data, mentions "new versions", or asks to check for new Magento releases.
---
Refresh the Magento Open Source version data in this repo by adding any tags that have shipped recently but are not yet recorded.
## 1. Find new tags
List tags from `magento/magento2` via the GitHub API:
```
gh api -X GET repos/magento/magento2/tags --paginate -q '.[].name' | head -50
```
Focus on tags from the last several weeks. Tags look like `2.4.8-p4`, `2.4.7-p9`, etc. Ignore preview/RC tags unless the user asks otherwise.
For each candidate tag, get its publish date (needed for the `release` field):
```
gh api repos/magento/magento2/git/refs/tags/<tag> -q '.object.url' | xargs -I{} gh api {} -q '.tagger.date // .author.date'
```
## 2. Diff against the JSON files
The two files to check:
- `supported-version/src/versions/magento-open-source/individual.json` — one entry per concrete tag, keyed `magento/project-community-edition:<tag>`
- `supported-version/src/versions/magento-open-source/composite.json` — range entries keyed `magento/project-community-edition:>=X.Y.Z <X.Y.(Z+1)`, plus the rolling entries `magento/project-community-edition` and `magento/project-community-edition:next`
A tag is "missing" if the `magento/project-community-edition:<tag>` key is absent from `individual.json`.
## 3. Fetch system requirements
For the minor version that the missing tag belongs to (e.g. `2.4.8` for `2.4.8-p4`), pull Adobe's system requirements page:
- https://experienceleague.adobe.com/en/docs/commerce-operations/installation-guide/system-requirements
Use `WebFetch` and extract: PHP, Composer, MySQL/MariaDB, Elasticsearch/OpenSearch, RabbitMQ, Redis/Valkey, Varnish, Nginx, supported OS. If a component (e.g. `elasticsearch`) is no longer listed for that minor version, omit the field from the new entry — do not carry it over from the previous patch. Compare the new entry against the most recent prior patch in `individual.json` to sanity-check which fields should be present.
For each service field, pin the **latest currently-published tag within the line Adobe lists**, derived from Docker Hub — not whatever the prior patch happened to carry.
- Adobe lists a major+minor (e.g. "Elasticsearch 8.19"): use the highest published `8.19.x` tag.
- Adobe lists only a major (e.g. "Elasticsearch 8"): use the highest published `8.y.z` across all `8.x` minors (today: `8.19.15`).
- Adobe lists multiple majors/lines (e.g. "OpenSearch 2.19, 3"): pick the newest line (`3`).
Query Docker Hub for the latest patch:
```
curl -s "https://hub.docker.com/v2/repositories/library/elasticsearch/tags?page_size=100&name=8.19" \
| python3 -c "import json,sys; d=json.load(sys.stdin); tags=[t['name'] for t in d['results'] if t['name'].startswith('8.19.') and t['name'].split('.')[2].isdigit()]; print(max(tags, key=lambda t:[int(x) for x in t.split('.')]))"
```
For OpenSearch swap `library/elasticsearch``opensearchproject/opensearch`. Services already using rolling minor tags (`redis:7.2`, `varnish:7.7`, `nginx:1.28`, `rabbitmq:4.1-management`) are already "latest of the line" and need no bump.
Also fetch Adobe's lifecycle policy page for the line's EOL date:
- https://experienceleague.adobe.com/en/docs/commerce-operations/release/planning/lifecycle-policy
## 4. Write the entries
### individual.json
Append the new patch entry preserving file ordering (group by minor version, ascending patch number). Schema:
```json
"magento/project-community-edition:<tag>": {
"magento": "magento/project-community-edition:<tag>",
"php": <number>,
"composer": "<string>",
"mysql": "mysql:<ver>" | "mariadb:<ver>",
"opensearch": "opensearchproject/opensearch:<ver>",
"elasticsearch": "elasticsearch:<ver>",
"rabbitmq": "rabbitmq:<ver>-management",
"redis": "redis:<ver>",
"valkey": "valkey/valkey:<ver>",
"varnish": "varnish:<ver>",
"nginx": "nginx:<ver>",
"os": "ubuntu-latest",
"release": "<ISO8601 from tag date>",
"eol": "<ISO8601 — see EOL rules below>"
}
```
### EOL rules
- The newest patch on a line gets `eol` set to the line's EOL date from Adobe's lifecycle policy page.
- When a newer patch on the same line releases, overwrite the previous patch's `eol` with the newer patch's `release` date. So when adding a new patch, also update the prior patch's `eol` accordingly.
- Net effect: at any moment only the latest patch on a line carries the line's lifecycle EOL; every older patch's `eol` equals the release date of its successor.
### composite.json
The composite range entry for the affected minor (e.g. `magento/project-community-edition:>=2.4.8 <2.4.9`) should reflect the highest patch's stack. Update its fields to match the new entry if the new tag is now the highest in that minor.
The rolling entries `magento/project-community-edition` and `magento/project-community-edition:next` must always mirror the system requirements of the highest tag across all minors (i.e. the absolute newest patch you just added, if it is the newest overall). Update PHP, Composer, MySQL, OpenSearch, RabbitMQ, Valkey, Varnish, Nginx, OS, release, eol on both. The `magento` field on `:next` stays `magento/project-community-edition:@alpha`.
## 5. Verify
After edits:
```
cd supported-version && npm test
```
All tests must pass before declaring done.
## Notes
- Do not remove existing entries — only add or update.
- Use the tag's actual publish timestamp from the GitHub API for `release`, not today's date.
- If Adobe's docs are ambiguous for a given component, ask the user before guessing.
- Preserve the file's existing key ordering and indentation (4 spaces).
+8 -1
View File
@@ -50,9 +50,12 @@ jobs:
tools: composer:v${{ matrix.composer }}
magento_version: ${{ matrix.magento }}
mode: extension
composer_auth: ${{ secrets.COMPOSER_AUTH }}
- run: composer update --no-install
working-directory: ${{ steps.setup-magento.outputs.path }}
env:
COMPOSER_AUTH: ${{ secrets.COMPOSER_AUTH }}
- uses: ./cache-magento
with:
@@ -79,6 +82,8 @@ jobs:
- run: composer install
working-directory: ${{ steps.setup-magento.outputs.path }}
env:
COMPOSER_AUTH: ${{ secrets.COMPOSER_AUTH }}
- uses: actions/upload-artifact@v7
with:
@@ -98,4 +103,6 @@ jobs:
store_artifact_name: store-fixture-${{ matrix.version }}
path: "_ghamagento/"
composer_cache_key: ${{ matrix.magento }}
stamp: true
stamp: true
secrets:
composer_auth: ${{ secrets.COMPOSER_AUTH }}
@@ -30,7 +30,9 @@ jobs:
- run: composer create-project --repository-url="https://mirror.mage-os.org" "magento/project-community-edition:2.4.5-p1" ../magento2 --no-install
shell: bash
name: Create Magento ${{ matrix.magento }} Project
name: Create Magento ${{ matrix.magento }} Project
env:
COMPOSER_AUTH: ${{ secrets.COMPOSER_AUTH }}
- uses: ./get-magento-version
id: magento-version
@@ -58,6 +58,8 @@ jobs:
- run: composer update --no-install
working-directory: ${{ steps.setup-magento.outputs.path }}
env:
COMPOSER_AUTH: ${{ secrets.COMPOSER_AUTH }}
- uses: ./cache-magento
with:
@@ -57,6 +57,8 @@ jobs:
- run: composer update --no-install
working-directory: ${{ steps.setup-magento.outputs.path }}
env:
COMPOSER_AUTH: ${{ secrets.COMPOSER_AUTH }}
- uses: ./cache-magento
with:
@@ -78,6 +78,8 @@ jobs:
- run: composer update --no-install
working-directory: ${{ env.magento_folder }}
env:
COMPOSER_AUTH: ${{ secrets.COMPOSER_AUTH }}
- uses: ./cache-magento
with:
@@ -218,13 +220,15 @@ jobs:
- run: composer update --no-install
working-directory: ${{ steps.setup-magento.outputs.path }}
env:
COMPOSER_AUTH: ${{ secrets.COMPOSER_AUTH }}
- uses: ./cache-magento
with:
composer_cache_key: ${{ matrix.magento }}
working-directory: ${{ steps.setup-magento.outputs.path }}
stamp: true
- run: composer install
name: Composer install
shell: bash
@@ -40,3 +40,5 @@ jobs:
path: _test/demo-package
matrix: ${{ needs.compute_matrix.outputs.matrix }}
stamp: true
secrets:
composer_auth: ${{ secrets.COMPOSER_AUTH }}
+53 -17
View File
@@ -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@v8.4.0
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-rc.1
- uses: graycoreio/github-actions-magento2/setup-magento@v8.4.0
id: setup-magento
with:
php-version: ${{ matrix.php }}
@@ -71,11 +84,15 @@ jobs:
- name: Require extension
working-directory: ${{ steps.setup-magento.outputs.path }}
run: composer require "${{ steps.package.outputs.name }}:@dev" --no-install
env:
COMPOSER_AUTH: ${{ secrets.composer_auth }}
- run: composer update --no-install
working-directory: ${{ steps.setup-magento.outputs.path }}
env:
COMPOSER_AUTH: ${{ secrets.composer_auth }}
- uses: graycoreio/github-actions-magento2/cache-magento@v8.0.0-rc.1
- uses: graycoreio/github-actions-magento2/cache-magento@v8.4.0
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 }}
@@ -109,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-rc.1
- uses: graycoreio/github-actions-magento2/setup-magento@v8.4.0
id: setup-magento
with:
php-version: ${{ matrix.php }}
@@ -141,8 +160,10 @@ jobs:
- run: composer update --no-install
working-directory: ${{ steps.setup-magento.outputs.path }}
- uses: graycoreio/github-actions-magento2/cache-magento@v8.0.0-rc.1
env:
COMPOSER_AUTH: ${{ secrets.composer_auth }}
- uses: graycoreio/github-actions-magento2/cache-magento@v8.4.0
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 }}
@@ -155,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-rc.1
- uses: graycoreio/github-actions-magento2/setup-di-compile@v8.4.0
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@v8.4.0
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
@@ -173,25 +205,27 @@ jobs:
tools: composer:v${{ matrix.composer }}
coverage: none
- uses: graycoreio/github-actions-magento2/cache-magento@v8.0.0-rc.1
- uses: graycoreio/github-actions-magento2/cache-magento@v8.4.0
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-rc.1
- uses: graycoreio/github-actions-magento2/coding-standard@v8.4.0
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-rc.1
- uses: graycoreio/github-actions-magento2/setup-magento@v8.4.0
id: setup-magento
with:
php-version: ${{ matrix.php }}
@@ -217,8 +251,10 @@ jobs:
- run: composer update --no-install
working-directory: ${{ steps.setup-magento.outputs.path }}
- uses: graycoreio/github-actions-magento2/cache-magento@v8.0.0-rc.1
env:
COMPOSER_AUTH: ${{ secrets.composer_auth }}
- uses: graycoreio/github-actions-magento2/cache-magento@v8.4.0
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 }}
@@ -231,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-rc.1
- uses: graycoreio/github-actions-magento2/get-magento-version@v8.4.0
id: magento-version
with:
working-directory: ${{ steps.setup-magento.outputs.path }}
@@ -265,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 }}
+86 -11
View File
@@ -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-rc.1
- uses: graycoreio/github-actions-magento2/get-magento-version@v8.4.0
id: get-magento-version
with:
working-directory: ${{ inputs.path }}
- uses: graycoreio/github-actions-magento2/supported-version@v8.0.0-rc.1
- uses: graycoreio/github-actions-magento2/supported-version@v8.4.0
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@v8.4.0
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-rc.1
- uses: graycoreio/github-actions-magento2/setup-magento@v8.4.0
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-rc.1
- uses: graycoreio/github-actions-magento2/cache-magento@v8.4.0
with:
composer_cache_key: ${{ inputs.composer_cache_key }}
working-directory: ${{ steps.setup-magento.outputs.path }}
@@ -106,6 +114,12 @@ jobs:
EOF
sed -i '/<testsuites>/r /tmp/testsuite.xml' dev/tests/unit/phpunit.xml.dist
## PHPUnit 12 (Magento 2.4.9) implicitly enables failOnEmptyTestSuite when --testsuite is passed.
## Default it off only when the consumer hasn't set it themselves, so we don't clobber explicit configuration.
if ! grep -q 'failOnEmptyTestSuite=' dev/tests/unit/phpunit.xml.dist; then
sed -i 's|<phpunit |<phpunit failOnEmptyTestSuite="false" |' dev/tests/unit/phpunit.xml.dist
fi
## Disable allure (See https://github.com/magento/magento2/issues/36702 )
sed -i '/<extensions>/,/<\/extensions>/d' dev/tests/unit/phpunit.xml.dist
@@ -116,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
@@ -129,7 +144,7 @@ jobs:
name: ${{ inputs.store_artifact_name }}
path: ${{ inputs.path }}
- uses: graycoreio/github-actions-magento2/setup-magento@v8.0.0-rc.1
- uses: graycoreio/github-actions-magento2/setup-magento@v8.4.0
id: setup-magento
with:
php-version: ${{ matrix.php }}
@@ -138,7 +153,7 @@ jobs:
working-directory: ${{ inputs.path }}
composer_auth: ${{ secrets.composer_auth }}
- uses: graycoreio/github-actions-magento2/cache-magento@v8.0.0-rc.1
- uses: graycoreio/github-actions-magento2/cache-magento@v8.4.0
with:
composer_cache_key: ${{ inputs.composer_cache_key }}
working-directory: ${{ steps.setup-magento.outputs.path }}
@@ -163,7 +178,67 @@ jobs:
EOF
fi
- uses: graycoreio/github-actions-magento2/coding-standard@v8.0.0-rc.1
- uses: graycoreio/github-actions-magento2/coding-standard@v8.4.0
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@v8.4.0
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@v8.4.0
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@v8.4.0
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@v8.4.0
with:
container_id: ${{ job.services.nginx.id }}
magento_path: ${{ inputs.path }}
- uses: graycoreio/github-actions-magento2/smoke-test@v8.4.0
with:
kind: page
- uses: graycoreio/github-actions-magento2/smoke-test@v8.4.0
with:
kind: graphql
+2 -2
View File
@@ -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-rc.1
- uses: graycoreio/github-actions-magento2/get-magento-version@v8.4.0
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 }}
+10 -6
View File
@@ -6,11 +6,15 @@ on:
- main
workflow_dispatch:
inputs:
release-candidate:
description: 'Cut a release-candidate (prerelease) instead of a normal release.'
type: boolean
release-mode:
description: 'auto = follow conventional commits; rc = bump the rc suffix; graduate = graduate the current rc to a stable release.'
type: choice
required: false
default: false
default: auto
options:
- auto
- rc
- graduate
env:
RELEASE_BRANCH: release-please--branches--main--components--github-actions-magento2
@@ -24,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-candidate && '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
+1 -1
View File
@@ -1 +1 @@
{".":"8.0.0-rc.1"}
{".":"8.4.0"}
+69
View File
@@ -2,6 +2,75 @@
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.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)
### Miscellaneous Chores
* graduate 8.0.0-rc.2 to 8.0.0 ([#262](https://github.com/graycoreio/github-actions-magento2/issues/262)) ([e6bb7be](https://github.com/graycoreio/github-actions-magento2/commit/e6bb7be5248a1431f06c07986066ab154c9d8531))
## [8.0.0-rc.2](https://github.com/graycoreio/github-actions-magento2/compare/v8.0.0-rc.1...v8.0.0-rc.2) (2026-05-13)
### ⚠ BREAKING CHANGES
* **supported-versions:** forcibly bump all packages to the latest relevant release line of composer for LogLeak
* **supported-versions:** This release brings support for the v2.4.9 version of Magento. This also brings backwards-incompatible infrastructure changes for the patch versions of Magento. See https://github.com/graycoreio/github-actions-magento2/pull/258 for more information.
### Features
* **check-extension:** pass along COMPOSER_AUTH where needed ([#258](https://github.com/graycoreio/github-actions-magento2/issues/258)) ([c5221f0](https://github.com/graycoreio/github-actions-magento2/commit/c5221f0d68b7ecc892b7718326eabc6f093c108f))
* **supported-versions:** forcibly bump all packages to the latest relevant release line of composer for LogLeak ([d1a31d2](https://github.com/graycoreio/github-actions-magento2/commit/d1a31d260dc54556ebd1ea4fb2e1764ad637694a))
* **supported-versions:** updates for 2.4.9, 2.4.8-p5, 2.4.7-p19, 2.4.6-p15 ([#258](https://github.com/graycoreio/github-actions-magento2/issues/258)) ([d37f001](https://github.com/graycoreio/github-actions-magento2/commit/d37f001ab6607d2c23751db12d21a7a9e69543f3))
### Bug Fixes
* **check-store:** prevent error in phpunit 12 if no tests exists ([#258](https://github.com/graycoreio/github-actions-magento2/issues/258)) ([4fc491b](https://github.com/graycoreio/github-actions-magento2/commit/4fc491bc1a26b7b7089b562db5d4e4a89b6d0744))
## [8.0.0-rc.1](https://github.com/graycoreio/github-actions-magento2/compare/v8.0.0-rc.0...v8.0.0-rc.1) (2026-05-10)
+4 -4
View File
@@ -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@v7.0.0 # x-release-please-version
- uses: graycoreio/github-actions-magento2/cache-magento@v8.4.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@v7.0.0 # x-release-please-version
- uses: graycoreio/github-actions-magento2/setup-magento@v8.4.0 # x-release-please-version
id: setup-magento
with:
mode: extension # or store
# ...
- uses: graycoreio/github-actions-magento2/cache-magento@v7.0.0 # x-release-please-version
- uses: graycoreio/github-actions-magento2/cache-magento@v8.4.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@v7.0.0 # x-release-please-version
> - uses: graycoreio/github-actions-magento2/cache-magento@v8.4.0 # x-release-please-version
> with:
> stamp: ${{ github.actor != 'dependabot[bot]' }}
> ```
+1 -1
View File
@@ -62,7 +62,7 @@ runs:
exit 1
fi
- uses: graycoreio/github-actions-magento2/get-magento-version@v8.0.0-rc.1
- uses: graycoreio/github-actions-magento2/get-magento-version@v8.4.0
id: cache-magento-get-magento-version
with:
working-directory: ${{ inputs.working-directory }}
+1 -1
View File
@@ -36,7 +36,7 @@ jobs:
tools: composer:v2
coverage: none
- uses: graycoreio/github-actions-magento2/coding-standard@v7.0.0 # x-release-please-version
- uses: graycoreio/github-actions-magento2/coding-standard@v8.4.0 # x-release-please-version
with:
path: app/code # Optional, defaults to .
version: 25 # Optional, will use the latest if omitted.
+2 -2
View File
@@ -52,12 +52,12 @@ runs:
fi
- name: Get Composer Version
uses: graycoreio/github-actions-magento2/get-composer-version@v8.0.0-rc.1
uses: graycoreio/github-actions-magento2/get-composer-version@v8.4.0
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-rc.1
uses: graycoreio/github-actions-magento2/semver-compare@v8.4.0
id: is-allow-plugins-available
if: steps.check-installed.outputs.installed != 'true'
with:
+71
View File
@@ -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
```
+67
View File
@@ -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*;
}
+19 -2
View File
@@ -18,6 +18,23 @@ See the [check-extension.yaml](../../.github/workflows/check-extension.yaml)
The Magento matrix format outlined by the [supported versions action.](https://github.com/graycoreio/github-actions-magento2/tree/main/supported-version/supported.json)
## Configuration
Each check can be toggled on/off through an optional `.github/check-extension.json` file in the repo that calls this workflow.
You can learn more about this file here in the [`resolve-check-config` action.](../../resolve-check-config/README.md):
Reference the published JSON Schema with `$schema` to get autocompletion and inline validation in editors that support it:
```json
{
"$schema": "https://raw.githubusercontent.com/graycoreio/github-actions-magento2/main/resolve-check-config/check-extension.schema.json",
"jobs": {
"integration_test": false
}
}
```
## Usage
```yml
@@ -38,12 +55,12 @@ jobs:
matrix: ${{ steps.supported-version.outputs.matrix }}
steps:
- uses: actions/checkout@v6
- uses: graycoreio/github-actions-magento2/supported-version@v7.0.0 # x-release-please-version
- uses: graycoreio/github-actions-magento2/supported-version@v8.4.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@v7.0.0 # x-release-please-version
uses: graycoreio/github-actions-magento2/.github/workflows/check-extension.yaml@v8.4.0 # x-release-please-version
with:
matrix: ${{ needs.compute_matrix.outputs.matrix }}
```
+20 -2
View File
@@ -24,6 +24,24 @@ See the [check-store.yaml](../../.github/workflows/check-store.yaml)
- **Unit Tests** — runs PHPUnit against custom code in `app/code`. Skipped automatically if no test files are found.
- **Coding Standard** — runs the Magento Coding Standard against `app/code`. Uses your `phpcs.xml` (or `.phpcs.xml`, `phpcs.xml.dist`, `.phpcs.xml.dist`) if one exists, otherwise a sensible default is generated.
- **Smoke Test** — boots your store against the supported-version service set (mysql, search, queue, cache, nginx, php-fpm) and runs the smoke probes against it.
## Configuration
Each check can be toggled on/off through an optional `.github/check-store.json` file in the repo that calls this workflow.
You can learn more about this file here in the [`resolve-check-config` action.](../../resolve-check-config/README.md):
Reference the published JSON Schema with `$schema` to get autocompletion and inline validation in editors that support it:
```json
{
"$schema": "https://raw.githubusercontent.com/graycoreio/github-actions-magento2/main/resolve-check-config/check-store.schema.json",
"jobs": {
"coding-standard": false
}
}
```
## Usage
@@ -40,7 +58,7 @@ on:
jobs:
check-store:
uses: graycoreio/github-actions-magento2/.github/workflows/check-store.yaml@v7.0.0 # x-release-please-version
uses: graycoreio/github-actions-magento2/.github/workflows/check-store.yaml@v8.4.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@v7.0.0 # x-release-please-version
uses: graycoreio/github-actions-magento2/.github/workflows/check-store.yaml@v8.4.0 # x-release-please-version
secrets:
composer_auth: ${{ secrets.COMPOSER_AUTH }}
```
+2 -2
View File
@@ -50,13 +50,13 @@ jobs:
matrix: ${{ steps.supported-version.outputs.matrix }}
steps:
- uses: actions/checkout@v6
- uses: graycoreio/github-actions-magento2/supported-version@v7.0.0 # x-release-please-version
- uses: graycoreio/github-actions-magento2/supported-version@v8.4.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@v7.0.0 # x-release-please-version
uses: graycoreio/github-actions-magento2/.github/workflows/integration.yaml@v8.4.0 # x-release-please-version
with:
package_name: my-vendor/package
matrix: ${{ needs.compute_matrix.outputs.matrix }}
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: graycoreio/github-actions-magento2/fix-magento-install@v7.0.0 # x-release-please-version
- uses: graycoreio/github-actions-magento2/fix-magento-install@v8.4.0 # x-release-please-version
with:
magento_directory: path/to/magento
```
+1 -1
View File
@@ -9,7 +9,7 @@ inputs:
runs:
using: "composite"
steps:
- uses: graycoreio/github-actions-magento2/get-magento-version@v8.0.0-rc.1
- uses: graycoreio/github-actions-magento2/get-magento-version@v8.4.0
id: init-magento-get-magento-version
with:
working-directory: ${{ inputs.magento_directory }}
+1 -1
View File
@@ -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@v7.0.0 # x-release-please-version
- uses: graycoreio/github-actions-magento2/get-composer-version@v8.4.0 # x-release-please-version
id: get-composer-version
- run: echo version ${{ steps.get-composer-version.outputs.version }}
shell: bash
+1 -1
View File
@@ -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@v7.0.0 # x-release-please-version
- uses: graycoreio/github-actions-magento2/get-magento-version@v8.4.0 # x-release-please-version
id: get-magento-version
- run: echo version ${{ steps.get-magento-version.outputs.version }}
shell: bash
+3
View File
@@ -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": []
}
+9 -1
View File
@@ -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
View File
@@ -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"
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "@graycoreio/github-actions-magento2",
"version": "8.0.0-rc.1",
"version": "8.4.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@graycoreio/github-actions-magento2",
"version": "8.0.0-rc.1",
"version": "8.4.0",
"license": "MIT",
"dependencies": {
"@actions/core": "^1.11.1",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@graycoreio/github-actions-magento2",
"version": "8.0.0-rc.1",
"version": "8.4.0",
"description": "Github Actions for Magento 2",
"scripts": {
"test": "cd supported-version && npm run test && cd - && cd setup-install && npm run test && cd -",
+19
View File
@@ -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 }
]
}
}
}
+55
View File
@@ -0,0 +1,55 @@
# "Resolve Check Config" Action
Reads `.github/check-<kind>.json` (or a path you specify), validates job names against the known list for the selected workflow kind, and emits a per-job filtered version of the `supported-version` matrix. Each job in the output carries an `enabled` flag and its own `matrix`, where every entry's `services` map has been narrowed to the tiers that job actually needs. Consumers gate each job with `fromJSON(...)['<job>'].enabled != false` and feed `fromJSON(...)['<job>'].matrix` into `strategy.matrix`.
A missing config file is fine — every known job is emitted with its default tier list.
## Inputs
| Input | Description | Required | Default |
|---------------|--------------------------------------------------------------------------------------------------------------------------------------------------------|----------|-------------------------------|
| `kind` | Which reusable workflow this config belongs to: `store` or `extension`. Selects the default `config_path`, the known-job list, and the per-job tier defaults. | true | |
| `matrix` | The matrix JSON emitted by the `supported-version` action. Each entry's `services` map is filtered per-job based on the resolved tier list. | true | |
| `config_path` | Path to the check-config JSON file, relative to the runner workspace. | false | `.github/check-<kind>.json` |
## Usage
```yml
jobs:
compute_matrix:
runs-on: ubuntu-latest
outputs:
resolved: ${{ steps.resolve.outputs.resolved }}
steps:
- uses: graycoreio/github-actions-magento2/supported-version@v8.4.0 # x-release-please-version
id: supported-version
with:
kind: currently-supported
- uses: graycoreio/github-actions-magento2/resolve-check-config@v8.4.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
}
}
```
+27
View File
@@ -0,0 +1,27 @@
name: "Resolve check config"
author: "Graycore"
description: "Reads .github/check-<kind>.json (or a path you specify), validates job names against the known list for that workflow kind, and emits a per-job filtered version of the supported-version matrix. Missing config file is treated as 'all jobs enabled with their default tier list.'"
inputs:
kind:
required: true
description: "Which reusable workflow this config belongs to: `store` or `extension`. Selects the default `config_path`, the known-job list used for validation, and the per-job default tier list."
matrix:
required: true
description: "The matrix JSON emitted by the `supported-version` action. Each entry's `services` map is filtered per-job based on the resolved tier list and embedded in the per-job `matrix` output."
config_path:
required: false
default: ""
description: "Path to the check-config JSON file, relative to the runner workspace. Defaults to `.github/check-<kind>.json`. Missing file is fine — every known job is emitted with its default tier list."
outputs:
resolved:
description: "The per-job resolved configuration as a JSON object. Each top-level key is a known job name for the selected kind; values are objects with `enabled` (boolean) and `matrix` (a copy of the supported-version matrix where every entry's `services` is filtered to the tiers the job needs). Consumers default-enable omitted jobs via `fromJSON(...)['<job>'].enabled != false` and use `fromJSON(...)['<job>'].matrix` for `strategy.matrix`."
runs:
using: "node24"
main: "dist/index.js"
branding:
icon: "check-square"
color: "green"
@@ -0,0 +1,49 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://raw.githubusercontent.com/graycoreio/github-actions-magento2/main/resolve-check-config/check-extension.schema.json",
"title": "graycoreio check-extension config",
"description": "Configuration consumed by the check-extension reusable workflow. Per-job toggles and settings live under `jobs`. Top-level remains open for future global keys.",
"type": "object",
"properties": {
"jobs": {
"type": "object",
"description": "Per-job configuration. Each key is a job name declared by check-extension; unknown keys are rejected.",
"properties": {
"unit-test-extension": { "$ref": "#/$defs/jobConfig" },
"compile-extension": { "$ref": "#/$defs/jobConfig" },
"coding-standard": { "$ref": "#/$defs/jobConfig" },
"integration_test": { "$ref": "#/$defs/jobConfig" }
},
"additionalProperties": false
}
},
"additionalProperties": true,
"$defs": {
"jobConfig": {
"description": "How a single job should be configured. Boolean form is shorthand for { enabled: <bool> }; object form allows extra per-job keys.",
"oneOf": [
{ "type": "boolean" },
{
"type": "object",
"properties": {
"enabled": {
"type": "boolean",
"description": "Whether the job should run. Defaults to true when the key is present.",
"default": true
},
"services": {
"type": "array",
"description": "Tier names this job needs as GitHub Actions service containers. mysql is always implicit.",
"items": {
"type": "string",
"enum": ["search", "queue", "cache", "web"]
},
"uniqueItems": true
}
},
"additionalProperties": true
}
]
}
}
}
@@ -0,0 +1,39 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://raw.githubusercontent.com/graycoreio/github-actions-magento2/main/resolve-check-config/check-store.schema.json",
"title": "graycoreio check-store config",
"description": "Configuration consumed by the check-store reusable workflow. Per-job toggles and settings live under `jobs`. Top-level remains open for future global keys.",
"type": "object",
"properties": {
"jobs": {
"type": "object",
"description": "Per-job configuration. Each key is a job name declared by check-store; unknown keys are rejected.",
"properties": {
"unit-test": { "$ref": "#/$defs/jobConfig" },
"coding-standard": { "$ref": "#/$defs/jobConfig" },
"smoke-test": { "$ref": "#/$defs/jobConfig" }
},
"additionalProperties": false
}
},
"additionalProperties": true,
"$defs": {
"jobConfig": {
"description": "How a single job should be configured. Boolean form is shorthand for { enabled: <bool> }; object form allows extra per-job keys.",
"oneOf": [
{ "type": "boolean" },
{
"type": "object",
"properties": {
"enabled": {
"type": "boolean",
"description": "Whether the job should run. Defaults to true when the key is present.",
"default": true
}
},
"additionalProperties": true
}
]
}
}
}
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
}
}
+9
View File
@@ -0,0 +1,9 @@
module.exports = {
clearMocks: true,
moduleFileExtensions: ['js', 'ts'],
testMatch: ['**/*.spec.ts'],
transform: {
'^.+\\.ts$': 'ts-jest'
},
verbose: true
}
+21
View File
@@ -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"
}
}
+33
View File
@@ -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();
+26
View File
@@ -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'/);
});
});
+22
View File
@@ -0,0 +1,22 @@
import { Kind } from './types';
/**
* Type guard for the `kind` input. Use this when you have an
* `unknown` value (e.g. from `core.getInput`) and want to narrow it
* without throwing.
*/
export const isKind = (value: unknown): value is Kind =>
value === 'store' || value === 'extension';
/**
* Narrows an `unknown` (typically the raw action input) to `Kind` or
* throws a user-facing error naming the accepted values. Prefer this
* at the action boundary so a bad `kind` fails fast with a clear
* message rather than later as an obscure dispatch miss.
*/
export const assertKind = (value: unknown): Kind => {
if (!isKind(value)) {
throw new Error(`check-config: \`kind\` must be 'store' or 'extension' (got ${JSON.stringify(value)})`);
}
return value;
}
@@ -0,0 +1,101 @@
import { EXTENSION_JOBS, KNOWN_JOBS_EXTENSION, resolveExtensionConfig } from './extension';
import { Matrix } from '../types';
const FULL_SERVICES = {
mysql: { image: 'mysql:8' },
opensearch: { image: 'opensearchproject/opensearch:2' },
rabbitmq: { image: 'rabbitmq:3' },
valkey: { image: 'valkey:8' },
nginx: { image: 'nginx:1.27' },
'php-fpm': { image: 'php:8.3-fpm' },
};
const MATRIX: Matrix = {
include: [{ php: '8.3', services: { ...FULL_SERVICES } }],
};
describe('EXTENSION_JOBS', () => {
it('declares the check-extension jobs', () => {
expect(Object.keys(EXTENSION_JOBS).sort()).toEqual([
'coding-standard',
'compile-extension',
'integration_test',
'unit-test-extension',
]);
});
it('keeps KNOWN_JOBS_EXTENSION in sync with the map keys', () => {
expect([...KNOWN_JOBS_EXTENSION].sort()).toEqual(Object.keys(EXTENSION_JOBS).sort());
});
it('declares integration_test required tiers (no web)', () => {
expect(EXTENSION_JOBS['integration_test'].services).toEqual([]);
expect([...EXTENSION_JOBS['integration_test'].requiredServices!].sort()).toEqual([
'cache',
'db',
'queue',
'search',
]);
});
it('leaves the non-service jobs with empty defaults', () => {
for (const name of ['unit-test-extension', 'compile-extension', 'coding-standard']) {
expect(EXTENSION_JOBS[name].services).toEqual([]);
expect(EXTENSION_JOBS[name].requiredServices).toBeUndefined();
}
});
});
describe('resolveExtensionConfig', () => {
it('emits every known job', () => {
const resolved = resolveExtensionConfig({}, MATRIX);
expect(Object.keys(resolved).sort()).toEqual([
'coding-standard',
'compile-extension',
'integration_test',
'unit-test-extension',
]);
});
it('emits services={} for the non-service jobs', () => {
const resolved = resolveExtensionConfig({}, MATRIX);
for (const name of ['unit-test-extension', 'compile-extension', 'coding-standard']) {
expect(resolved[name].matrix.include[0].services).toEqual({});
}
});
it('integration_test includes mysql/search/queue/cache but NOT nginx/php-fpm', () => {
const resolved = resolveExtensionConfig({}, MATRIX);
expect(Object.keys(resolved['integration_test'].matrix.include[0].services!).sort()).toEqual([
'mysql',
'opensearch',
'rabbitmq',
'valkey',
]);
});
it('keeps integration_test required tiers even when caller overrides services to []', () => {
const resolved = resolveExtensionConfig(
{ jobs: { integration_test: { services: [] } } },
MATRIX,
);
expect(Object.keys(resolved['integration_test'].matrix.include[0].services!).sort()).toEqual([
'mysql',
'opensearch',
'rabbitmq',
'valkey',
]);
});
it('throws on a typo in the job name', () => {
expect(() => resolveExtensionConfig({ jobs: { 'inteegration_test': false } }, MATRIX)).toThrowError(
/unknown job "inteegration_test" for kind "extension"/
);
});
it('throws when a store-only job name is used', () => {
expect(() => resolveExtensionConfig({ jobs: { 'smoke-test': false } }, MATRIX)).toThrowError(
/unknown job "smoke-test" for kind "extension"/
);
});
});
@@ -0,0 +1,29 @@
import { resolveJobs } from '../parse';
import { JobDefaults, Matrix, RawConfig, ResolvedConfig } from '../types';
/**
* Per-job defaults for the `check-extension.yaml` reusable workflow.
* Edit this map when a job is added, removed, or renamed in that
* workflow — keys are validated against caller config and the values
* supply the default tier list used when the caller doesn't override
* `services` themselves.
*/
export const EXTENSION_JOBS: Record<string, JobDefaults> = {
'unit-test-extension': { services: [] },
'compile-extension': { services: [] },
'coding-standard': { services: [] },
'integration_test': {
services: [],
requiredServices: ['db', 'search', 'queue', 'cache'],
},
};
export const KNOWN_JOBS_EXTENSION: readonly string[] = Object.keys(EXTENSION_JOBS);
/**
* Resolves a parsed config file + supported-version matrix against
* the check-extension job list. Thin wrapper that binds the kind and
* the per-job defaults so callers don't repeat the wiring.
*/
export const resolveExtensionConfig = (raw: RawConfig, matrix: Matrix): ResolvedConfig =>
resolveJobs(raw, 'extension', EXTENSION_JOBS, matrix);
@@ -0,0 +1,93 @@
import { KNOWN_JOBS_STORE, resolveStoreConfig, STORE_JOBS } from './store';
import { Matrix } from '../types';
const MATRIX: Matrix = {
include: [{
php: '8.3',
services: {
mysql: { image: 'mysql:8' },
opensearch: { image: 'opensearchproject/opensearch:2' },
rabbitmq: { image: 'rabbitmq:3' },
valkey: { image: 'valkey:8' },
nginx: { image: 'nginx:1.27' },
'php-fpm': { image: 'php:8.3-fpm' },
},
}],
};
describe('STORE_JOBS', () => {
it('declares the check-store jobs', () => {
expect(Object.keys(STORE_JOBS).sort()).toEqual(['coding-standard', 'smoke-test', 'unit-test']);
});
it('declares smoke-test required tiers (end-user cannot toggle)', () => {
expect(STORE_JOBS['smoke-test'].services).toEqual([]);
expect([...STORE_JOBS['smoke-test'].requiredServices!].sort()).toEqual([
'cache',
'db',
'queue',
'search',
'web',
]);
});
it('exposes empty service defaults for unit-test and coding-standard', () => {
expect(STORE_JOBS['unit-test'].services).toEqual([]);
expect(STORE_JOBS['coding-standard'].services).toEqual([]);
});
it('keeps KNOWN_JOBS_STORE in sync with the map keys', () => {
expect([...KNOWN_JOBS_STORE].sort()).toEqual(Object.keys(STORE_JOBS).sort());
});
});
describe('resolveStoreConfig', () => {
it('emits every known job with default tier expansion, always including mysql for smoke-test', () => {
const resolved = resolveStoreConfig({}, MATRIX);
expect(Object.keys(resolved).sort()).toEqual(['coding-standard', 'smoke-test', 'unit-test']);
expect(resolved['unit-test'].matrix.include[0].services).toEqual({});
expect(Object.keys(resolved['smoke-test'].matrix.include[0].services!).sort()).toEqual([
'mysql',
'nginx',
'opensearch',
'php-fpm',
'rabbitmq',
'valkey',
]);
});
it('keeps every required service even when caller overrides smoke-test services to []', () => {
const resolved = resolveStoreConfig(
{ jobs: { 'smoke-test': { services: [] } } },
MATRIX,
);
expect(Object.keys(resolved['smoke-test'].matrix.include[0].services!).sort()).toEqual([
'mysql',
'nginx',
'opensearch',
'php-fpm',
'rabbitmq',
'valkey',
]);
});
it('honors enabled=false for a job', () => {
const resolved = resolveStoreConfig(
{ jobs: { 'smoke-test': false } },
MATRIX,
);
expect(resolved['smoke-test'].enabled).toBe(false);
});
it('throws on a typo in the job name', () => {
expect(() => resolveStoreConfig({ jobs: { 'smkoe-test': false } }, MATRIX)).toThrowError(
/unknown job "smkoe-test" for kind "store"/
);
});
it('throws when an extension-only job name is used', () => {
expect(() => resolveStoreConfig({ jobs: { 'unit-test-extension': false } }, MATRIX)).toThrowError(
/unknown job "unit-test-extension" for kind "store"/
);
});
});
+28
View File
@@ -0,0 +1,28 @@
import { resolveJobs } from '../parse';
import { JobDefaults, Matrix, RawConfig, ResolvedConfig } from '../types';
/**
* Per-job defaults for the `check-store.yaml` reusable workflow.
* Edit this map when a job is added, removed, or renamed in that
* workflow — keys are validated against caller config and the values
* supply the default tier list used when the caller doesn't override
* `services` themselves.
*/
export const STORE_JOBS: Record<string, JobDefaults> = {
'unit-test': { services: [] },
'coding-standard': { services: [] },
'smoke-test': {
services: [],
requiredServices: ['db', 'search', 'queue', 'cache', 'web'],
},
};
export const KNOWN_JOBS_STORE: readonly string[] = Object.keys(STORE_JOBS);
/**
* Resolves a parsed config file + supported-version matrix against
* the check-store job list. Thin wrapper that binds the kind and the
* per-job defaults so callers don't repeat the wiring.
*/
export const resolveStoreConfig = (raw: RawConfig, matrix: Matrix): ResolvedConfig =>
resolveJobs(raw, 'store', STORE_JOBS, matrix);
+305
View File
@@ -0,0 +1,305 @@
import {
filterEntryServices,
filterMatrixForJob,
mergeRequiredTiers,
normalizeJobEntry,
parseMatrixInput,
parseRawConfig,
resolveJobs,
} from './parse';
import { JobDefaults, Matrix } from './types';
const FULL_SERVICES = {
mysql: { image: 'mysql:8' },
opensearch: { image: 'opensearchproject/opensearch:2' },
rabbitmq: { image: 'rabbitmq:3' },
valkey: { image: 'valkey:8' },
nginx: { image: 'nginx:1.27' },
'php-fpm': { image: 'php:8.3-fpm' },
};
const MATRIX: Matrix = {
include: [{ php: '8.3', services: { ...FULL_SERVICES } }],
};
const noDefaults: JobDefaults = { services: [] };
const smokeDefaults: JobDefaults = { services: ['search', 'queue', 'cache', 'web'] };
describe('normalizeJobEntry', () => {
it('defaults enabled=true and uses the default tiers when entry is undefined', () => {
expect(normalizeJobEntry('smoke-test', undefined, smokeDefaults)).toEqual({
enabled: true,
tiers: smokeDefaults.services,
});
});
it('treats true shorthand as enabled with defaults', () => {
expect(normalizeJobEntry('smoke-test', true, smokeDefaults)).toEqual({
enabled: true,
tiers: smokeDefaults.services,
});
});
it('treats false shorthand as disabled with defaults', () => {
expect(normalizeJobEntry('smoke-test', false, smokeDefaults)).toEqual({
enabled: false,
tiers: smokeDefaults.services,
});
});
it('empty object is enabled with defaults', () => {
expect(normalizeJobEntry('smoke-test', {}, smokeDefaults)).toEqual({
enabled: true,
tiers: smokeDefaults.services,
});
});
it('preserves enabled when explicitly set', () => {
expect(normalizeJobEntry('smoke-test', { enabled: false }, smokeDefaults)).toEqual({
enabled: false,
tiers: smokeDefaults.services,
});
});
it('overrides the default tiers when services is set', () => {
expect(normalizeJobEntry('smoke-test', { services: ['cache', 'web'] }, smokeDefaults)).toEqual({
enabled: true,
tiers: ['cache', 'web'],
});
});
it('accepts an empty services array as "no services"', () => {
expect(normalizeJobEntry('smoke-test', { services: [] }, smokeDefaults)).toEqual({
enabled: true,
tiers: [],
});
});
it('throws when entry is a non-array primitive other than boolean', () => {
expect(() => normalizeJobEntry('unit-test', 'true' as never, noDefaults)).toThrowError(
/must be a boolean or an object/
);
});
it('throws when entry is an array', () => {
expect(() => normalizeJobEntry('unit-test', [] as never, noDefaults)).toThrowError(/got array/);
});
it('throws when services is not an array', () => {
expect(() => normalizeJobEntry('smoke-test', { services: 'search' } as never, smokeDefaults)).toThrowError(
/services must be an array of tier names/
);
});
it('throws when services contains an unknown tier', () => {
expect(() => normalizeJobEntry('smoke-test', { services: ['llm'] } as never, smokeDefaults)).toThrowError(
/services contains unknown tier "llm"/
);
});
});
describe('mergeRequiredTiers', () => {
it('returns the input list when required is undefined', () => {
expect(mergeRequiredTiers(['cache'], undefined)).toEqual(['cache']);
});
it('returns the input list when required is empty', () => {
expect(mergeRequiredTiers(['cache'], [])).toEqual(['cache']);
});
it('prepends required tiers ahead of the input tiers', () => {
expect(mergeRequiredTiers(['cache', 'web'], ['db'])).toEqual(['db', 'cache', 'web']);
});
it('deduplicates when a required tier already appears in the input', () => {
expect(mergeRequiredTiers(['db', 'cache'], ['db'])).toEqual(['db', 'cache']);
});
it('deduplicates within required itself', () => {
expect(mergeRequiredTiers(['cache'], ['db', 'db'])).toEqual(['db', 'cache']);
});
});
describe('filterEntryServices', () => {
it('returns services={} for an empty tier list', () => {
const out = filterEntryServices({ php: '8.3', services: FULL_SERVICES }, []);
expect(out.services).toEqual({});
expect(out.php).toBe('8.3');
});
it('keeps only services in the requested tiers', () => {
const out = filterEntryServices({ php: '8.3', services: FULL_SERVICES }, ['cache', 'web']);
expect(Object.keys(out.services!).sort()).toEqual(['nginx', 'php-fpm', 'valkey']);
});
it('drops services that the matrix doesn\'t carry (elasticsearch absent)', () => {
const out = filterEntryServices({ services: { opensearch: FULL_SERVICES.opensearch } }, ['search']);
expect(Object.keys(out.services!)).toEqual(['opensearch']);
});
it('emits services={} when the entry has no services map', () => {
const out = filterEntryServices({ php: '8.3' }, ['cache']);
expect(out.services).toEqual({});
});
});
describe('filterMatrixForJob', () => {
it('preserves matrix shape, mapping every entry through filterEntryServices', () => {
const out = filterMatrixForJob(MATRIX, ['queue']);
expect(out.include).toHaveLength(1);
expect(Object.keys(out.include[0].services!)).toEqual(['rabbitmq']);
});
it('passes through unrelated top-level matrix keys', () => {
const out = filterMatrixForJob({ ...MATRIX, magento: ['2.4.7'] } as Matrix, []);
expect((out as Matrix).magento).toEqual(['2.4.7']);
});
});
describe('resolveJobs', () => {
const jobs: Record<string, JobDefaults> = {
'unit-test': noDefaults,
'smoke-test': smokeDefaults,
};
it('emits every known job, defaulted-enabled, when raw is empty', () => {
const out = resolveJobs({}, 'store', jobs, MATRIX);
expect(Object.keys(out).sort()).toEqual(['smoke-test', 'unit-test']);
expect(out['unit-test'].enabled).toBe(true);
expect(out['smoke-test'].enabled).toBe(true);
});
it('emits services={} on entries for a no-default job', () => {
const out = resolveJobs({}, 'store', jobs, MATRIX);
expect(out['unit-test'].matrix.include[0].services).toEqual({});
});
it('expands the smoke-test default tiers across the matrix entry', () => {
const out = resolveJobs({}, 'store', jobs, MATRIX);
expect(Object.keys(out['smoke-test'].matrix.include[0].services!).sort()).toEqual([
'nginx',
'opensearch',
'php-fpm',
'rabbitmq',
'valkey',
]);
});
it('applies a caller-supplied services override', () => {
const out = resolveJobs(
{ jobs: { 'smoke-test': { services: ['cache'] } } },
'store',
jobs,
MATRIX,
);
expect(Object.keys(out['smoke-test'].matrix.include[0].services!)).toEqual(['valkey']);
});
it('always merges requiredServices into the matrix even when caller overrides services', () => {
const withRequired: Record<string, JobDefaults> = {
'smoke-test': { ...smokeDefaults, requiredServices: ['db'] },
};
const out = resolveJobs(
{ jobs: { 'smoke-test': { services: ['cache'] } } },
'store',
withRequired,
MATRIX,
);
expect(Object.keys(out['smoke-test'].matrix.include[0].services!).sort()).toEqual(['mysql', 'valkey']);
});
it('keeps requiredServices even when caller overrides services to []', () => {
const withRequired: Record<string, JobDefaults> = {
'smoke-test': { ...smokeDefaults, requiredServices: ['db'] },
};
const out = resolveJobs(
{ jobs: { 'smoke-test': { services: [] } } },
'store',
withRequired,
MATRIX,
);
expect(Object.keys(out['smoke-test'].matrix.include[0].services!)).toEqual(['mysql']);
});
it('honors caller enabled=false but still emits a filtered matrix', () => {
const out = resolveJobs(
{ jobs: { 'smoke-test': false } },
'store',
jobs,
MATRIX,
);
expect(out['smoke-test'].enabled).toBe(false);
expect(out['smoke-test'].matrix.include[0].services).toBeDefined();
});
it('throws on unknown job names with the kind in the message', () => {
expect(() => resolveJobs({ jobs: { taco: false } }, 'store', jobs, MATRIX)).toThrowError(
/unknown job "taco" for kind "store"/
);
});
it('throws when `jobs` is not an object', () => {
expect(() => resolveJobs({ jobs: 'oops' } as never, 'store', jobs, MATRIX)).toThrowError(
/`jobs` must be an object/
);
});
});
describe('parseRawConfig', () => {
it('returns an empty object for empty input', () => {
expect(parseRawConfig('')).toEqual({});
});
it('returns an empty object for whitespace input', () => {
expect(parseRawConfig(' \n ')).toEqual({});
});
it('parses a valid object', () => {
expect(parseRawConfig('{"jobs": {"unit-test": true}}')).toEqual({
jobs: { 'unit-test': true },
});
});
it('throws on syntactically invalid JSON', () => {
expect(() => parseRawConfig('{not json}')).toThrowError(/failed to parse JSON/);
});
it('throws when top level is an array', () => {
expect(() => parseRawConfig('[]')).toThrowError(/top-level value must be an object/);
});
it('throws when top level is a primitive', () => {
expect(() => parseRawConfig('"hello"')).toThrowError(/top-level value must be an object/);
});
it('throws when top level is null', () => {
expect(() => parseRawConfig('null')).toThrowError(/top-level value must be an object/);
});
});
describe('parseMatrixInput', () => {
it('parses a valid matrix', () => {
const out = parseMatrixInput('{"include": [{"php": "8.3"}]}');
expect(out.include).toEqual([{ php: '8.3' }]);
});
it('throws on empty input', () => {
expect(() => parseMatrixInput('')).toThrowError(/`matrix` input is required/);
});
it('throws on invalid JSON', () => {
expect(() => parseMatrixInput('{nope}')).toThrowError(/failed to parse `matrix` input/);
});
it('throws when top level is an array', () => {
expect(() => parseMatrixInput('[]')).toThrowError(/`matrix` must be a JSON object/);
});
it('throws when include is missing', () => {
expect(() => parseMatrixInput('{}')).toThrowError(/`matrix.include` must be an array/);
});
it('throws when include is not an array', () => {
expect(() => parseMatrixInput('{"include": "nope"}')).toThrowError(/`matrix.include` must be an array/);
});
});
+174
View File
@@ -0,0 +1,174 @@
import { JobDefaults, Kind, Matrix, MatrixEntry, RawConfig, RawJobConfig, ResolvedConfig, ResolvedJobConfig, Services } from './types';
import { isTier, servicesForTiers, Tier } from './tier-map';
/**
* Normalizes a single raw job entry to (enabled, tiers). Accepts
* the boolean shorthand and the object form. Validates the shape
* and the `services` tier list; throws on unexpected input. The
* caller supplies the per-job default tiers, used when `services`
* is omitted from the entry.
*/
export const normalizeJobEntry = (
jobName: string,
raw: RawJobConfig | undefined,
defaults: JobDefaults,
): { enabled: boolean; tiers: readonly Tier[] } => {
if (raw === undefined) {
return { enabled: true, tiers: defaults.services };
}
if (typeof raw === 'boolean') {
return { enabled: raw, tiers: defaults.services };
}
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
throw new Error(
`check-config: job "${jobName}" must be a boolean or an object (got ${Array.isArray(raw) ? 'array' : typeof raw})`
);
}
const { enabled, services } = raw as { enabled?: unknown; services?: unknown };
const enabledValue = enabled === undefined ? true : Boolean(enabled);
if (services === undefined) {
return { enabled: enabledValue, tiers: defaults.services };
}
if (!Array.isArray(services)) {
throw new Error(`check-config: job "${jobName}".services must be an array of tier names`);
}
const tiers: Tier[] = [];
for (const value of services) {
if (!isTier(value)) {
throw new Error(`check-config: job "${jobName}".services contains unknown tier "${String(value)}"`);
}
tiers.push(value);
}
return { enabled: enabledValue, tiers };
}
/**
* Merges a job's `requiredServices` into the resolved tier list,
* deduplicating while preserving order (required tiers first, then
* the caller/default tiers in their original order).
*/
export const mergeRequiredTiers = (
tiers: readonly Tier[],
required: readonly Tier[] | undefined,
): readonly Tier[] => {
if (!required || required.length === 0) return tiers;
const seen = new Set<Tier>();
const merged: Tier[] = [];
for (const tier of required) {
if (!seen.has(tier)) { seen.add(tier); merged.push(tier); }
}
for (const tier of tiers) {
if (!seen.has(tier)) { seen.add(tier); merged.push(tier); }
}
return merged;
}
/**
* Returns a copy of `entry` with `services` filtered to the concrete
* names produced by expanding `tiers` through the tier-map. An empty
* tier list yields `services: {}`.
*/
export const filterEntryServices = (entry: MatrixEntry, tiers: readonly Tier[]): MatrixEntry => {
const keep = servicesForTiers(tiers);
const original = entry.services ?? {};
const filtered: Services = {};
for (const [name, config] of Object.entries(original)) {
if (keep.has(name)) filtered[name] = config;
}
return { ...entry, services: filtered };
}
/**
* Per-job filter applied to the supported-version matrix: returns a
* shallow clone with every entry's `services` narrowed to the tiers
* the job needs.
*/
export const filterMatrixForJob = (matrix: Matrix, tiers: readonly Tier[]): Matrix => ({
...matrix,
include: matrix.include.map(entry => filterEntryServices(entry, tiers)),
});
/**
* Shared per-kind resolver: walks the per-kind job map and emits one
* `ResolvedJobConfig` per known job. Caller-supplied jobs override
* the defaults; jobs the caller omitted still appear, carrying the
* default `enabled: true` and the default tier list. Rejects unknown
* job names from the config so typos surface in CI.
*/
export const resolveJobs = (
raw: RawConfig,
kind: Kind,
jobs: Record<string, JobDefaults>,
matrix: Matrix,
): ResolvedConfig => {
const rawJobs = raw.jobs ?? {};
if (rawJobs === null || typeof rawJobs !== 'object' || Array.isArray(rawJobs)) {
throw new Error(`check-config: \`jobs\` must be an object`);
}
for (const name of Object.keys(rawJobs)) {
if (!(name in jobs)) {
throw new Error(
`check-config: unknown job "${name}" for kind "${kind}". Known jobs: ${Object.keys(jobs).join(', ')}`
);
}
}
const resolved: ResolvedConfig = {};
for (const [name, defaults] of Object.entries(jobs)) {
const entry = (rawJobs as Record<string, RawJobConfig>)[name];
const { enabled, tiers } = normalizeJobEntry(name, entry, defaults);
const finalTiers = mergeRequiredTiers(tiers, defaults.requiredServices);
resolved[name] = {
enabled,
matrix: filterMatrixForJob(matrix, finalTiers),
} as ResolvedJobConfig;
}
return resolved;
}
/**
* Parses a JSON string into a RawConfig with shape validation
* (must be an object, not an array or primitive). Empty/whitespace
* input yields an empty config.
*/
export const parseRawConfig = (jsonText: string): RawConfig => {
const trimmed = jsonText.trim();
if (trimmed === '') return {};
let parsed: unknown;
try {
parsed = JSON.parse(trimmed);
} catch (e) {
throw new Error(`check-config: failed to parse JSON: ${(e as Error).message}`);
}
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
throw new Error(`check-config: top-level value must be an object`);
}
return parsed as RawConfig;
}
/**
* Parses the `matrix` action input. Validates the top-level shape
* (must be an object with an `include` array) so a malformed input
* fails with a clear message at the boundary.
*/
export const parseMatrixInput = (jsonText: string): Matrix => {
const trimmed = jsonText.trim();
if (trimmed === '') {
throw new Error('check-config: `matrix` input is required');
}
let parsed: unknown;
try {
parsed = JSON.parse(trimmed);
} catch (e) {
throw new Error(`check-config: failed to parse \`matrix\` input as JSON: ${(e as Error).message}`);
}
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
throw new Error('check-config: `matrix` must be a JSON object');
}
const include = (parsed as Record<string, unknown>).include;
if (!Array.isArray(include)) {
throw new Error('check-config: `matrix.include` must be an array');
}
return parsed as Matrix;
}
+39
View File
@@ -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"/
);
});
});
+13
View File
@@ -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);
}
+46
View File
@@ -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']);
});
});
+39
View File
@@ -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;
}
+100
View File
@@ -0,0 +1,100 @@
import { Tier } from './tier-map';
/**
* Which reusable workflow a config belongs to. Selects the known-job
* list used for validation and the default config path.
*/
export type Kind = 'store' | 'extension';
/**
* A single service container definition from supported-version's
* matrix output. We don't model the inner shape here — we just
* preserve unknown keys when filtering.
*/
export interface ServiceConfig {
image: string;
env?: Record<string, string>;
ports?: string[];
options?: string;
volumes?: string[];
}
export interface Services {
[serviceName: string]: ServiceConfig;
}
/**
* One row of supported-version's matrix. Carries the PHP/Composer/etc
* coordinates plus the concrete `services` map this job should bring
* up. We type the known fields supported-version emits and allow
* extras to pass through untouched.
*/
export interface MatrixEntry {
services?: Services;
[key: string]: unknown;
}
/**
* The matrix shape emitted by supported-version, suitable for
* `strategy.matrix` in GitHub Actions.
*/
export interface Matrix {
include: MatrixEntry[];
[key: string]: unknown;
}
/**
* Per-job static defaults declared by each kind module.
*
* `services` is the tier list used when the caller's config does not
* override it — these tiers are user-toggleable through the schema.
*
* `requiredServices` is always merged in on top of the resolved list,
* regardless of caller overrides. Use it for tiers a job structurally
* cannot run without (e.g. mysql for a running store smoke-test) and
* which therefore should not appear in the user-facing schema enum.
*/
export interface JobDefaults {
services: readonly Tier[];
requiredServices?: readonly Tier[];
}
/**
* Resolved per-job output. `enabled` mirrors the input boolean (or
* `enabled` key); `matrix` is supported-version's matrix with each
* entry's `services` filtered to the tiers this job needs.
*/
export interface ResolvedJobConfig {
enabled: boolean;
matrix: Matrix;
[key: string]: unknown;
}
/**
* Map of job-name -> resolved config. Keys are exactly the job names
* declared by the kind module (omitted-by-caller jobs still appear,
* carrying defaults so the consumer's `if:` guard works uniformly).
*/
export interface ResolvedConfig {
[jobName: string]: ResolvedJobConfig;
}
/**
* Shape of a single per-job entry in the user's JSON config file.
* - `true` / `false`: shorthand for `{ enabled: true|false }`
* - object: explicit enabled flag plus an optional tier list under
* `services` (validated against the per-kind schema).
*/
export type RawJobConfig =
| boolean
| { enabled?: boolean; services?: string[]; [key: string]: unknown };
/**
* Top-level shape of the user's JSON config file. Job toggles live
* under `jobs`; the rest of the top level is reserved for future
* global keys.
*/
export interface RawConfig {
jobs?: { [jobName: string]: RawJobConfig };
[key: string]: unknown;
}
+8
View File
@@ -0,0 +1,8 @@
{
"compilerOptions": {
"resolveJsonModule": true,
"esModuleInterop": true,
"typeRoots": ["../node_modules/@types"],
"types": ["jest"]
}
}
+1 -1
View File
@@ -28,7 +28,7 @@ jobs:
steps:
- uses: actions/checkout@v6
- uses: graycoreio/github-actions-magento2/sansec-ecomscan@v7.0.0 # x-release-please-version
- uses: graycoreio/github-actions-magento2/sansec-ecomscan@v8.4.0 # x-release-please-version
with:
license: ${{ secrets.SANSEC_LICENSE_KEY }}
```
+1 -1
View File
@@ -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@v7.0.0 # x-release-please-version
- uses: graycoreio/github-actions-magento2/semver-compare@v8.4.0 # x-release-please-version
with:
version: 2.1.0
compare_against: 2.2.3
+4 -4
View File
@@ -28,7 +28,7 @@ jobs:
matrix: ${{ steps.supported-version.outputs.matrix }}
steps:
- uses: actions/checkout@v6
- uses: graycoreio/github-actions-magento2/supported-version@v7.0.0 # x-release-please-version
- uses: graycoreio/github-actions-magento2/supported-version@v8.4.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@v7.0.0 # x-release-please-version
- uses: graycoreio/github-actions-magento2/setup-magento@v8.4.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@v7.0.0 # x-release-please-version
- uses: graycoreio/github-actions-magento2/cache-magento@v8.4.0 # x-release-please-version
- run: composer install
env:
COMPOSER_AUTH: ${{ secrets.COMPOSER_AUTH }}
- uses: graycoreio/github-actions-magento2/setup-di-compile@v7.0.0 # x-release-please-version
- uses: graycoreio/github-actions-magento2/setup-di-compile@v8.4.0 # x-release-please-version
with:
path: ${{ steps.setup-magento.outputs.path }}
```
+3 -3
View File
@@ -36,7 +36,7 @@ jobs:
matrix: ${{ steps.supported-version.outputs.matrix }}
steps:
- uses: actions/checkout@v6
- uses: graycoreio/github-actions-magento2/supported-version@v7.0.0 # x-release-please-version
- uses: graycoreio/github-actions-magento2/supported-version@v8.4.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@v7.0.0 # x-release-please-version
- uses: graycoreio/github-actions-magento2/setup-magento@v8.4.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@v7.0.0 # x-release-please-version
- uses: graycoreio/github-actions-magento2/setup-install@v8.4.0 # x-release-please-version
with:
services: ${{ toJSON(matrix.services) }}
path: ${{ steps.setup-magento.outputs.path }}
+5
View File
@@ -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."
+28 -28
View File
File diff suppressed because one or more lines are too long
+96
View File
@@ -173,4 +173,100 @@ describe('buildInstallArgs', () => {
]);
});
});
describe('container mode', () => {
it('uses the mysql network alias and the internal port from ports[0]', () => {
const services: Services = {
mysql: { ...MYSQL_SERVICE, ports: ['33060:3306'] },
};
expect(buildInstallArgs(services, true)).toEqual([
...BASE_ARGS,
'--db-host=mysql:3306',
'--db-name=magento_integration_tests',
'--db-user=user',
'--db-password=password',
]);
});
it('uses the opensearch alias and parses the internal port', () => {
const services: Services = {
opensearch: { image: 'opensearchproject/opensearch:2.19.1', ports: ['19200:9200'] },
};
expect(buildInstallArgs(services, true)).toEqual([
...BASE_ARGS,
'--search-engine=opensearch',
'--opensearch-host=opensearch',
'--opensearch-port=9200',
]);
});
it('uses the elasticsearch alias and parses the internal port', () => {
const services: Services = {
elasticsearch: { image: 'elasticsearch:8.11.4', ports: ['19200:9200'] },
};
expect(buildInstallArgs(services, true)).toEqual([
...BASE_ARGS,
'--search-engine=elasticsearch8',
'--elasticsearch-host=elasticsearch',
'--elasticsearch-port=9200',
]);
});
it('uses the rabbitmq alias and parses the internal port', () => {
const services: Services = {
rabbitmq: { image: 'rabbitmq:4.0-management', ports: ['15672:5672'] },
};
expect(buildInstallArgs(services, true)).toEqual([
...BASE_ARGS,
'--amqp-host=rabbitmq',
'--amqp-port=5672',
'--amqp-user=guest',
'--amqp-password=guest',
]);
});
it('uses the valkey alias when valkey is the cache service', () => {
const services: Services = {
valkey: { image: 'valkey:8.0', ports: ['16379:6379'] },
};
expect(buildInstallArgs(services, true)).toEqual([
...BASE_ARGS,
'--session-save=redis',
'--session-save-redis-host=valkey',
'--session-save-redis-port=6379',
'--cache-backend=redis',
'--cache-backend-redis-server=valkey',
'--cache-backend-redis-port=6379',
]);
});
it('uses the redis alias when redis is the cache service', () => {
const services: Services = {
redis: { image: 'redis:7.2', ports: ['16379:6379'] },
};
expect(buildInstallArgs(services, true)).toEqual([
...BASE_ARGS,
'--session-save=redis',
'--session-save-redis-host=redis',
'--session-save-redis-port=6379',
'--cache-backend=redis',
'--cache-backend-redis-server=redis',
'--cache-backend-redis-port=6379',
]);
});
it('falls back to default internal ports when ports are absent', () => {
const services: Services = {
mysql: { ...MYSQL_SERVICE, ports: undefined },
opensearch: { image: 'opensearchproject/opensearch:2.19.1' },
rabbitmq: { image: 'rabbitmq:4.0-management' },
valkey: { image: 'valkey:8.0' },
};
const args = buildInstallArgs(services, true);
expect(args).toContain('--db-host=mysql:3306');
expect(args).toContain('--opensearch-port=9200');
expect(args).toContain('--amqp-port=5672');
expect(args).toContain('--session-save-redis-port=6379');
});
});
});
+32 -15
View File
@@ -3,6 +3,7 @@ export interface ServiceConfig {
env?: Record<string, string>;
ports?: string[];
options?: string;
volumes?: string[];
}
export interface Services {
@@ -12,6 +13,7 @@ export interface Services {
rabbitmq?: ServiceConfig;
redis?: ServiceConfig;
valkey?: ServiceConfig;
'php-fpm'?: ServiceConfig;
}
const BASE_ARGS = [
@@ -24,21 +26,29 @@ const BASE_ARGS = [
'--backend-frontname=admin',
];
const parsePort = (svc: ServiceConfig | undefined, index: 0 | 1, fallback: string): string => {
return svc?.ports?.[0]?.split(':')[index] ?? fallback;
};
export const buildMysqlPrepArgs = (mysql: ServiceConfig): string[] => {
const rootPassword = mysql.env?.MYSQL_ROOT_PASSWORD ?? 'rootpassword';
const port = mysql.ports?.[0]?.split(':')[0] ?? '3306';
const port = parsePort(mysql, 0, '3306');
return ['-h127.0.0.1', `--port=${port}`, '-uroot', `-p${rootPassword}`, '-e', 'SET GLOBAL log_bin_trust_function_creators = 1;'];
};
export const buildInstallArgs = (services: Services | null): string[] => {
export const buildInstallArgs = (services: Services | null, containerMode = false): string[] => {
const args = [...BASE_ARGS];
if (!services) return args;
const portIdx: 0 | 1 = containerMode ? 1 : 0;
const hostFor = (alias: string): string => containerMode ? alias : 'localhost';
if (services.mysql) {
const dbPort = services.mysql.ports?.[0]?.split(':')[0] ?? '3306';
const dbPort = parsePort(services.mysql, portIdx, '3306');
const dbHost = containerMode ? `mysql:${dbPort}` : `127.0.0.1:${dbPort}`;
args.push(
`--db-host=127.0.0.1:${dbPort}`,
`--db-host=${dbHost}`,
`--db-name=${services.mysql.env?.MYSQL_DATABASE ?? 'magento'}`,
`--db-user=${services.mysql.env?.MYSQL_USER ?? 'magento'}`,
`--db-password=${services.mysql.env?.MYSQL_PASSWORD ?? 'magento'}`,
@@ -46,39 +56,46 @@ export const buildInstallArgs = (services: Services | null): string[] => {
}
if (services.opensearch) {
const port = parsePort(services.opensearch, portIdx, '9200');
args.push(
'--search-engine=opensearch',
'--opensearch-host=localhost',
'--opensearch-port=9200',
`--opensearch-host=${hostFor('opensearch')}`,
`--opensearch-port=${port}`,
);
} else if (services.elasticsearch) {
const majorVersion = services.elasticsearch.image.split(':')[1]?.split('.')[0];
const port = parsePort(services.elasticsearch, portIdx, '9200');
args.push(
`--search-engine=elasticsearch${majorVersion}`,
'--elasticsearch-host=localhost',
'--elasticsearch-port=9200',
`--elasticsearch-host=${hostFor('elasticsearch')}`,
`--elasticsearch-port=${port}`,
);
}
if (services.rabbitmq) {
const port = parsePort(services.rabbitmq, portIdx, '5672');
args.push(
'--amqp-host=localhost',
'--amqp-port=5672',
`--amqp-host=${hostFor('rabbitmq')}`,
`--amqp-port=${port}`,
'--amqp-user=guest',
'--amqp-password=guest',
);
}
if (services.valkey || services.redis) {
const cacheKey: 'valkey' | 'redis' = services.valkey ? 'valkey' : 'redis';
const cache = services[cacheKey]!;
const port = parsePort(cache, portIdx, '6379');
const host = hostFor(cacheKey);
args.push(
'--session-save=redis',
'--session-save-redis-host=localhost',
'--session-save-redis-port=6379',
`--session-save-redis-host=${host}`,
`--session-save-redis-port=${port}`,
'--cache-backend=redis',
'--cache-backend-redis-server=localhost',
'--cache-backend-redis-port=6379',
`--cache-backend-redis-server=${host}`,
`--cache-backend-redis-port=${port}`,
);
}
return args;
};
};
+40 -5
View File
@@ -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();
+2 -2
View File
@@ -51,7 +51,7 @@ jobs:
steps:
- uses: actions/checkout@v6
- uses: graycoreio/github-actions-magento2/setup-magento@v7.0.0 # x-release-please-version
- uses: graycoreio/github-actions-magento2/setup-magento@v8.4.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@v7.0.0 # x-release-please-version
- uses: graycoreio/github-actions-magento2/setup-magento@v8.4.0 # x-release-please-version
id: setup-magento
with:
php-version: "8.3"
+1 -1
View File
@@ -111,7 +111,7 @@ runs:
fi
printf '%s\n' "$line" >> .gitattributes
- uses: graycoreio/github-actions-magento2/fix-magento-install@v8.0.0-rc.1
- uses: graycoreio/github-actions-magento2/fix-magento-install@v8.4.0
name: Fix Magento Out of Box Install Issues
with:
magento_directory: ${{ steps.setup-magento-compute-directory.outputs.MAGENTO_DIRECTORY }}
+77
View File
@@ -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"
+45 -2
View File
@@ -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.4.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@v7.0.0 # x-release-please-version
- uses: graycoreio/github-actions-magento2/supported-version@v8.4.0 # x-release-please-version
id: supported-version
- run: echo ${{ steps.supported-version.outputs.matrix }}
```
+6 -1
View File
@@ -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."
+29 -25
View File
File diff suppressed because one or more lines are too long
+17 -2
View File
@@ -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()
@@ -88,12 +88,14 @@ describe('getCurrentlySupportedVersions for magento-open-source', () => {
'magento/project-community-edition:2.4.7-p9',
'magento/project-community-edition:2.4.8-p4',
]],
['2027-04-09T00:00:00Z', 'Day of v2.4.7 EoL', [
'magento/project-community-edition:2.4.7-p9',
'magento/project-community-edition:2.4.8-p4',
['2027-05-31T00:00:00Z', 'Day of v2.4.7 EoL', [
'magento/project-community-edition:2.4.7-p10',
'magento/project-community-edition:2.4.8-p5',
'magento/project-community-edition:2.4.9',
]],
['2027-04-10T00:00:00Z', 'Day after v2.4.7 EoL', [
'magento/project-community-edition:2.4.8-p4',
['2027-06-01T00:00:00Z', 'Day after v2.4.7 EoL', [
'magento/project-community-edition:2.4.8-p5',
'magento/project-community-edition:2.4.9',
]],
])(
'supportedVersions for %s',
@@ -143,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];
}
@@ -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"
}
}
@@ -2,37 +2,35 @@
"mage-os/project-community-edition": {
"magento": "mage-os/project-community-edition",
"php": 8.4,
"composer": "2.9.7",
"composer": "2.9.8",
"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",
"php": 8.4,
"composer": "2.9.7",
"composer": "2.9.8",
"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",
"php": 8.3,
"composer": "2.9.5",
"composer": "2.9.8",
"mysql": "mariadb:10.6",
"elasticsearch": "elasticsearch:8.11.4",
"rabbitmq": "rabbitmq:3.13-management",
@@ -46,7 +44,7 @@
"mage-os/project-community-edition:>=1.1 <1.2": {
"magento": "mage-os/project-community-edition:>=1.1 <1.2",
"php": 8.4,
"composer": "2.9.5",
"composer": "2.9.8",
"mysql": "mysql:8.4",
"opensearch": "opensearchproject/opensearch:2.19.1",
"rabbitmq": "rabbitmq:4.0-management",
@@ -60,7 +58,7 @@
"mage-os/project-community-edition:>=1.2 <1.3": {
"magento": "mage-os/project-community-edition:>=1.2 <1.3",
"php": 8.4,
"composer": "2.9.5",
"composer": "2.9.8",
"mysql": "mysql:8.4",
"opensearch": "opensearchproject/opensearch:2.19.1",
"rabbitmq": "rabbitmq:4.0-management",
@@ -74,7 +72,7 @@
"mage-os/project-community-edition:>=1.3 <1.4": {
"magento": "mage-os/project-community-edition:>=1.2 <1.3",
"php": 8.4,
"composer": "2.9.5",
"composer": "2.9.8",
"mysql": "mysql:8.4",
"opensearch": "opensearchproject/opensearch:2.19.1",
"rabbitmq": "rabbitmq:4.0-management",
@@ -88,7 +86,7 @@
"mage-os/project-community-edition:>=2.0 <2.1": {
"magento": "mage-os/project-community-edition:>=2.0 <2.1",
"php": 8.4,
"composer": "2.9.5",
"composer": "2.9.8",
"mysql": "mysql:8.4",
"opensearch": "opensearchproject/opensearch:2.19.1",
"rabbitmq": "rabbitmq:4.0-management",
@@ -102,7 +100,7 @@
"mage-os/project-community-edition:>=2.1 <2.2": {
"magento": "mage-os/project-community-edition:>=2.1 <2.2",
"php": 8.4,
"composer": "2.9.5",
"composer": "2.9.8",
"mysql": "mysql:8.4",
"opensearch": "opensearchproject/opensearch:2.19.1",
"rabbitmq": "rabbitmq:4.0-management",
@@ -116,7 +114,7 @@
"mage-os/project-community-edition:>=2.2 <2.3": {
"magento": "mage-os/project-community-edition:>=2.2 <2.3",
"php": 8.4,
"composer": "2.9.3",
"composer": "2.9.8",
"mysql": "mysql:8.4",
"opensearch": "opensearchproject/opensearch:3",
"rabbitmq": "rabbitmq:4.1-management",
@@ -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"
}
}
@@ -3,7 +3,7 @@
"magento": "mage-os/project-community-edition:1.0.0",
"upstream": "2.4.6-p3",
"php": 8.1,
"composer": "2.2.21",
"composer": "2.2.28",
"mysql": "mysql:8.0",
"elasticsearch": "elasticsearch:8.5.3",
"rabbitmq": "rabbitmq:3.9-management",
@@ -18,7 +18,7 @@
"magento": "mage-os/project-community-edition:1.0.1",
"upstream": "2.4.6-p3",
"php": 8.1,
"composer": "2.2.21",
"composer": "2.2.28",
"mysql": "mysql:8.0",
"elasticsearch": "elasticsearch:8.5.3",
"rabbitmq": "rabbitmq:3.9-management",
@@ -33,7 +33,7 @@
"magento": "mage-os/project-community-edition:1.0.2",
"upstream": "2.4.7-p1",
"php": 8.3,
"composer": "2.7.4",
"composer": "2.9.8",
"mysql": "mariadb:10.6",
"elasticsearch": "elasticsearch:8.11.4",
"rabbitmq": "rabbitmq:3.13-management",
@@ -48,7 +48,7 @@
"magento": "mage-os/project-community-edition:1.0.3",
"upstream": "2.4.7-p1",
"php": 8.3,
"composer": "2.7.4",
"composer": "2.9.8",
"mysql": "mariadb:10.6",
"elasticsearch": "elasticsearch:8.11.4",
"rabbitmq": "rabbitmq:3.13-management",
@@ -63,7 +63,7 @@
"magento": "mage-os/project-community-edition:1.0.4",
"upstream": "2.4.7-p2",
"php": 8.3,
"composer": "2.7.4",
"composer": "2.9.8",
"mysql": "mariadb:10.6",
"elasticsearch": "elasticsearch:8.11.4",
"rabbitmq": "rabbitmq:3.13-management",
@@ -78,7 +78,7 @@
"magento": "mage-os/project-community-edition:1.0.5",
"upstream": "2.4.7-p3",
"php": 8.3,
"composer": "2.7.4",
"composer": "2.9.8",
"mysql": "mariadb:10.6",
"elasticsearch": "elasticsearch:8.11.4",
"rabbitmq": "rabbitmq:3.13-management",
@@ -93,7 +93,7 @@
"magento": "mage-os/project-community-edition:1.0.6",
"upstream": "2.4.7-p4",
"php": 8.3,
"composer": "2.7.4",
"composer": "2.9.8",
"mysql": "mariadb:10.6",
"elasticsearch": "elasticsearch:8.11.4",
"rabbitmq": "rabbitmq:3.13-management",
@@ -108,7 +108,7 @@
"magento": "mage-os/project-community-edition:1.1.0",
"upstream": "2.4.8",
"php": 8.4,
"composer": "2.9.5",
"composer": "2.9.8",
"mysql": "mysql:8.4",
"opensearch": "opensearchproject/opensearch:2.19.1",
"rabbitmq": "rabbitmq:4.0-management",
@@ -123,7 +123,7 @@
"magento": "mage-os/project-community-edition:1.1.1",
"upstream": "2.4.8",
"php": 8.4,
"composer": "2.9.5",
"composer": "2.9.8",
"mysql": "mysql:8.4",
"opensearch": "opensearchproject/opensearch:2.19.1",
"rabbitmq": "rabbitmq:4.0-management",
@@ -138,7 +138,7 @@
"magento": "mage-os/project-community-edition:1.2.0",
"upstream": "2.4.8-p1",
"php": 8.4,
"composer": "2.9.5",
"composer": "2.9.8",
"mysql": "mysql:8.4",
"opensearch": "opensearchproject/opensearch:2.19.1",
"rabbitmq": "rabbitmq:4.0-management",
@@ -153,7 +153,7 @@
"magento": "mage-os/project-community-edition:1.3.0",
"upstream": "2.4.8-p2",
"php": 8.4,
"composer": "2.9.5",
"composer": "2.9.8",
"mysql": "mysql:8.4",
"opensearch": "opensearchproject/opensearch:2.19.1",
"rabbitmq": "rabbitmq:4.0-management",
@@ -168,7 +168,7 @@
"magento": "mage-os/project-community-edition:1.3.1",
"upstream": "2.4.8-p2",
"php": 8.4,
"composer": "2.9.5",
"composer": "2.9.8",
"mysql": "mysql:8.4",
"opensearch": "opensearchproject/opensearch:2.19.1",
"rabbitmq": "rabbitmq:4.0-management",
@@ -183,7 +183,7 @@
"magento": "mage-os/project-community-edition:2.0.0",
"upstream": "2.4.8-p3",
"php": 8.4,
"composer": "2.9.5",
"composer": "2.9.8",
"mysql": "mysql:8.4",
"opensearch": "opensearchproject/opensearch:2.19.1",
"rabbitmq": "rabbitmq:4.0-management",
@@ -198,7 +198,7 @@
"magento": "mage-os/project-community-edition:2.1.0",
"upstream": "2.4.8-p3",
"php": 8.4,
"composer": "2.9.5",
"composer": "2.9.8",
"mysql": "mysql:8.4",
"opensearch": "opensearchproject/opensearch:2.19.1",
"rabbitmq": "rabbitmq:4.0-management",
@@ -213,7 +213,7 @@
"magento": "mage-os/project-community-edition:2.2.0",
"upstream": "2.4.8-p4",
"php": 8.4,
"composer": "2.9.3",
"composer": "2.9.8",
"mysql": "mysql:8.4",
"opensearch": "opensearchproject/opensearch:3",
"rabbitmq": "rabbitmq:4.1-management",
@@ -228,7 +228,7 @@
"magento": "mage-os/project-community-edition:2.2.1",
"upstream": "2.4.8-p4",
"php": 8.4,
"composer": "2.9.5",
"composer": "2.9.8",
"mysql": "mysql:8.4",
"opensearch": "opensearchproject/opensearch:3",
"rabbitmq": "rabbitmq:4.1-management",
@@ -243,7 +243,7 @@
"magento": "mage-os/project-community-edition:2.2.2",
"upstream": "2.4.8-p4",
"php": 8.4,
"composer": "2.9.7",
"composer": "2.9.8",
"mysql": "mysql:8.4",
"opensearch": "opensearchproject/opensearch:3",
"rabbitmq": "rabbitmq:4.1-management",
@@ -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"
}
}
@@ -44,7 +44,7 @@
"magento/project-community-edition:>=2.4.2 <2.4.3": {
"magento": "magento/project-community-edition:>=2.4.2 <2.4.3",
"php": 7.4,
"composer": "2.2.21",
"composer": "2.2.28",
"mysql": "mysql:8.0",
"elasticsearch": "elasticsearch:7.9.3",
"rabbitmq": "rabbitmq:3.8-management",
@@ -58,7 +58,7 @@
"magento/project-community-edition:>=2.4.3 <2.4.4": {
"magento": "magento/project-community-edition:>=2.4.3 <2.4.4",
"php": 7.4,
"composer": "2.2.21",
"composer": "2.2.28",
"mysql": "mysql:8.0",
"elasticsearch": "elasticsearch:7.16.3",
"rabbitmq": "rabbitmq:3.8-management",
@@ -72,7 +72,7 @@
"magento/project-community-edition:>=2.4.4 <2.4.5": {
"magento": "magento/project-community-edition:>=2.4.4 <2.4.5",
"php": 8.1,
"composer": "2.2.25",
"composer": "2.2.28",
"mysql": "mysql:8.0",
"elasticsearch": "elasticsearch:7.17.5",
"rabbitmq": "rabbitmq:3.9-management",
@@ -86,7 +86,7 @@
"magento/project-community-edition:>=2.4.5 <2.4.6": {
"magento": "magento/project-community-edition:>=2.4.5 <2.4.6",
"php": 8.1,
"composer": "2.2.25",
"composer": "2.2.28",
"mysql": "mysql:8.0",
"elasticsearch": "elasticsearch:7.17.28",
"rabbitmq": "rabbitmq:3.13-management",
@@ -100,12 +100,12 @@
"magento/project-community-edition:>=2.4.6 <2.4.7": {
"magento": "magento/project-community-edition:>=2.4.6 <2.4.7",
"php": 8.2,
"composer": "2.2.26",
"composer": "2.2.28",
"mysql": "mysql:8.0",
"elasticsearch": "elasticsearch:8.19.2",
"rabbitmq": "rabbitmq:4.1-management",
"redis": "redis:7.2",
"varnish": "varnish:7.7",
"opensearch": "opensearchproject/opensearch:3",
"rabbitmq": "rabbitmq:4.2-management",
"valkey": "valkey/valkey:8.1",
"varnish": "varnish:8",
"nginx": "nginx:1.28",
"os": "ubuntu-latest",
"release": "2025-04-08T00:00:00+0000",
@@ -114,59 +114,72 @@
"magento/project-community-edition:>=2.4.7 <2.4.8": {
"magento": "magento/project-community-edition:>=2.4.7 <2.4.8",
"php": 8.3,
"composer": "2.9.5",
"composer": "2.9.8",
"mysql": "mysql:8.0",
"elasticsearch": "elasticsearch:8.17.4",
"opensearch": "opensearchproject/opensearch:2.19.5",
"rabbitmq": "rabbitmq:4.1-management",
"redis": "redis:7.2",
"valkey": "valkey/valkey:8.0",
"varnish": "varnish:7.7",
"elasticsearch": "elasticsearch:8.19.15",
"opensearch": "opensearchproject/opensearch:3",
"rabbitmq": "rabbitmq:4.2-management",
"valkey": "valkey/valkey:8.1",
"varnish": "varnish:8",
"nginx": "nginx:1.28",
"os": "ubuntu-latest",
"release": "2025-04-08T00:00:00+0000",
"eol": "2027-04-09T00:00:00+0000"
"eol": "2027-05-31T00:00:00+0000"
},
"magento/project-community-edition:>=2.4.8 <2.4.9": {
"magento": "magento/project-community-edition:>=2.4.8 <2.4.9",
"php": 8.4,
"composer": "2.9.5",
"composer": "2.9.8",
"mysql": "mysql:8.4",
"opensearch": "opensearchproject/opensearch:3",
"rabbitmq": "rabbitmq:4.1-management",
"valkey": "valkey/valkey:8.0",
"varnish": "varnish:7.7",
"rabbitmq": "rabbitmq:4.2-management",
"valkey": "valkey/valkey:8.1",
"varnish": "varnish:8",
"nginx": "nginx:1.28",
"os": "ubuntu-latest",
"release": "2025-04-08T00:00:00+0000",
"eol": "2028-04-09T00:00:00+0000"
"eol": "2028-05-31T00:00:00+0000"
},
"magento/project-community-edition:>=2.4.9 <2.4.10": {
"magento": "magento/project-community-edition:>=2.4.9 <2.4.10",
"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-07T00:00:00+0000",
"eol": "2029-05-31T00:00:00+0000"
},
"magento/project-community-edition": {
"magento": "magento/project-community-edition",
"php": 8.4,
"composer": "2.9.5",
"php": 8.5,
"composer": "2.9.8",
"mysql": "mysql:8.4",
"opensearch": "opensearchproject/opensearch:3",
"rabbitmq": "rabbitmq:4.1-management",
"valkey": "valkey/valkey:8.0",
"varnish": "varnish:7.7",
"rabbitmq": "rabbitmq:4.2-management",
"valkey": "valkey/valkey:9",
"varnish": "varnish:8",
"nginx": "nginx:1.28",
"os": "ubuntu-latest",
"release": "2025-04-08T00:00:00+0000",
"eol": "2028-04-09T00:00:00+0000"
"release": "2026-05-07T00:00:00+0000",
"eol": "2029-05-31T00:00:00+0000"
},
"magento/project-community-edition:next": {
"magento": "magento/project-community-edition:@alpha",
"php": 8.4,
"composer": "2.9.5",
"php": 8.5,
"composer": "2.9.8",
"mysql": "mysql:8.4",
"valkey": "valkey/valkey:8.0",
"valkey": "valkey/valkey:9",
"opensearch": "opensearchproject/opensearch:3",
"rabbitmq": "rabbitmq:4.1-management",
"varnish": "varnish:7.7",
"rabbitmq": "rabbitmq:4.2-management",
"varnish": "varnish:8",
"nginx": "nginx:1.28",
"os": "ubuntu-latest",
"release": "2025-04-08T00:00:00+0000",
"eol": "2028-04-09T00:00:00+0000"
"release": "2026-05-07T00:00:00+0000",
"eol": "2029-05-31T00:00:00+0000"
}
}

Some files were not shown because too many files have changed in this diff Show More