Compare commits

...

43 Commits

Author SHA1 Message Date
Damien Retzinger a097371e37 feat(resolve-check-config): graphql smoke test opt-out by default 2026-05-31 21:26:17 -04:00
GrayBot 5ee0768610 chore: restore internal action refs to @main (#285) 2026-05-27 16:20:58 -04:00
GrayBot 3c51e99538 chore: release 8.5.0 (#284) 2026-05-27 16:18:40 -04:00
Damien Retzinger 32a5fd2bad feat(setup-install): run with --no-interaction 2026-05-27 15:39:46 -04:00
GrayBot 91bd008e62 chore: restore internal action refs to @main (#283) 2026-05-25 16:15:12 -04:00
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
102 changed files with 3408 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 }} tools: composer:v${{ matrix.composer }}
magento_version: ${{ matrix.magento }} magento_version: ${{ matrix.magento }}
mode: extension mode: extension
composer_auth: ${{ secrets.COMPOSER_AUTH }}
- run: composer update --no-install - run: composer update --no-install
working-directory: ${{ steps.setup-magento.outputs.path }} working-directory: ${{ steps.setup-magento.outputs.path }}
env:
COMPOSER_AUTH: ${{ secrets.COMPOSER_AUTH }}
- uses: ./cache-magento - uses: ./cache-magento
with: with:
@@ -79,6 +82,8 @@ jobs:
- run: composer install - run: composer install
working-directory: ${{ steps.setup-magento.outputs.path }} working-directory: ${{ steps.setup-magento.outputs.path }}
env:
COMPOSER_AUTH: ${{ secrets.COMPOSER_AUTH }}
- uses: actions/upload-artifact@v7 - uses: actions/upload-artifact@v7
with: with:
@@ -98,4 +103,6 @@ jobs:
store_artifact_name: store-fixture-${{ matrix.version }} store_artifact_name: store-fixture-${{ matrix.version }}
path: "_ghamagento/" path: "_ghamagento/"
composer_cache_key: ${{ matrix.magento }} 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 - run: composer create-project --repository-url="https://mirror.mage-os.org" "magento/project-community-edition:2.4.5-p1" ../magento2 --no-install
shell: bash shell: bash
name: Create Magento ${{ matrix.magento }} Project name: Create Magento ${{ matrix.magento }} Project
env:
COMPOSER_AUTH: ${{ secrets.COMPOSER_AUTH }}
- uses: ./get-magento-version - uses: ./get-magento-version
id: magento-version id: magento-version
@@ -58,6 +58,8 @@ jobs:
- run: composer update --no-install - run: composer update --no-install
working-directory: ${{ steps.setup-magento.outputs.path }} working-directory: ${{ steps.setup-magento.outputs.path }}
env:
COMPOSER_AUTH: ${{ secrets.COMPOSER_AUTH }}
- uses: ./cache-magento - uses: ./cache-magento
with: with:
@@ -57,6 +57,8 @@ jobs:
- run: composer update --no-install - run: composer update --no-install
working-directory: ${{ steps.setup-magento.outputs.path }} working-directory: ${{ steps.setup-magento.outputs.path }}
env:
COMPOSER_AUTH: ${{ secrets.COMPOSER_AUTH }}
- uses: ./cache-magento - uses: ./cache-magento
with: with:
@@ -78,6 +78,8 @@ jobs:
- run: composer update --no-install - run: composer update --no-install
working-directory: ${{ env.magento_folder }} working-directory: ${{ env.magento_folder }}
env:
COMPOSER_AUTH: ${{ secrets.COMPOSER_AUTH }}
- uses: ./cache-magento - uses: ./cache-magento
with: with:
@@ -218,13 +220,15 @@ jobs:
- run: composer update --no-install - run: composer update --no-install
working-directory: ${{ steps.setup-magento.outputs.path }} working-directory: ${{ steps.setup-magento.outputs.path }}
env:
COMPOSER_AUTH: ${{ secrets.COMPOSER_AUTH }}
- uses: ./cache-magento - uses: ./cache-magento
with: with:
composer_cache_key: ${{ matrix.magento }} composer_cache_key: ${{ matrix.magento }}
working-directory: ${{ steps.setup-magento.outputs.path }} working-directory: ${{ steps.setup-magento.outputs.path }}
stamp: true stamp: true
- run: composer install - run: composer install
name: Composer install name: Composer install
shell: bash shell: bash
@@ -40,3 +40,5 @@ jobs:
path: _test/demo-package path: _test/demo-package
matrix: ${{ needs.compute_matrix.outputs.matrix }} matrix: ${{ needs.compute_matrix.outputs.matrix }}
stamp: true 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)" description: "Your composer credentials (typically a stringified json object of the contents of your auth.json)"
jobs: jobs:
compute_resolved:
runs-on: ubuntu-latest
outputs:
resolved: ${{ steps.resolve.outputs.resolved }}
steps:
- uses: graycoreio/github-actions-magento2/resolve-check-config@main
id: resolve
with:
kind: extension
matrix: ${{ inputs.matrix }}
unit-test-extension: unit-test-extension:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
needs: compute_resolved
if: ${{ fromJSON(needs.compute_resolved.outputs.resolved)['unit-test-extension'].enabled != false }}
strategy: strategy:
matrix: ${{ fromJSON(inputs.matrix) }} matrix: ${{ fromJSON(needs.compute_resolved.outputs.resolved)['unit-test-extension'].matrix }}
fail-fast: ${{ inputs.fail-fast }} fail-fast: ${{ inputs.fail-fast }}
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v6
- uses: graycoreio/github-actions-magento2/setup-magento@v8.0.0-rc.1 - uses: graycoreio/github-actions-magento2/setup-magento@main
id: setup-magento id: setup-magento
with: with:
php-version: ${{ matrix.php }} php-version: ${{ matrix.php }}
@@ -71,11 +84,15 @@ jobs:
- name: Require extension - name: Require extension
working-directory: ${{ steps.setup-magento.outputs.path }} working-directory: ${{ steps.setup-magento.outputs.path }}
run: composer require "${{ steps.package.outputs.name }}:@dev" --no-install run: composer require "${{ steps.package.outputs.name }}:@dev" --no-install
env:
COMPOSER_AUTH: ${{ secrets.composer_auth }}
- run: composer update --no-install - run: composer update --no-install
working-directory: ${{ steps.setup-magento.outputs.path }} 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@main
with: with:
composer_cache_key: ${{ inputs.composer_cache_key && format('{0} | {1}', inputs.composer_cache_key, matrix.magento) || matrix.magento }} 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 }} working-directory: ${{ steps.setup-magento.outputs.path }}
@@ -109,13 +126,15 @@ jobs:
compile-extension: compile-extension:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
needs: compute_resolved
if: ${{ fromJSON(needs.compute_resolved.outputs.resolved)['compile-extension'].enabled != false }}
strategy: strategy:
matrix: ${{ fromJSON(inputs.matrix) }} matrix: ${{ fromJSON(needs.compute_resolved.outputs.resolved)['compile-extension'].matrix }}
fail-fast: ${{ inputs.fail-fast }} fail-fast: ${{ inputs.fail-fast }}
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v6
- uses: graycoreio/github-actions-magento2/setup-magento@v8.0.0-rc.1 - uses: graycoreio/github-actions-magento2/setup-magento@main
id: setup-magento id: setup-magento
with: with:
php-version: ${{ matrix.php }} php-version: ${{ matrix.php }}
@@ -141,8 +160,10 @@ jobs:
- run: composer update --no-install - run: composer update --no-install
working-directory: ${{ steps.setup-magento.outputs.path }} working-directory: ${{ steps.setup-magento.outputs.path }}
env:
- uses: graycoreio/github-actions-magento2/cache-magento@v8.0.0-rc.1 COMPOSER_AUTH: ${{ secrets.composer_auth }}
- uses: graycoreio/github-actions-magento2/cache-magento@main
with: with:
composer_cache_key: ${{ inputs.composer_cache_key && format('{0} | {1}', inputs.composer_cache_key, matrix.magento) || matrix.magento }} 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 }} working-directory: ${{ steps.setup-magento.outputs.path }}
@@ -155,14 +176,25 @@ jobs:
COMPOSER_AUTH: ${{ secrets.composer_auth }} COMPOSER_AUTH: ${{ secrets.composer_auth }}
COMPOSER_MIRROR_PATH_REPOS: 1 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@main
with: with:
path: ${{ steps.setup-magento.outputs.path }} path: ${{ steps.setup-magento.outputs.path }}
compute_latest_matrix:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.supported-version.outputs.matrix }}
steps:
- uses: graycoreio/github-actions-magento2/supported-version@main
id: supported-version
with:
kind: latest
coding-standard: coding-standard:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
needs: compute_latest_matrix
strategy: strategy:
matrix: ${{ fromJSON(inputs.matrix) }} matrix: ${{ fromJSON(needs.compute_latest_matrix.outputs.matrix) }}
fail-fast: ${{ inputs.fail-fast }} fail-fast: ${{ inputs.fail-fast }}
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v6
@@ -173,25 +205,27 @@ jobs:
tools: composer:v${{ matrix.composer }} tools: composer:v${{ matrix.composer }}
coverage: none coverage: none
- uses: graycoreio/github-actions-magento2/cache-magento@v8.0.0-rc.1 - uses: graycoreio/github-actions-magento2/cache-magento@main
with: with:
composer_cache_key: ${{ inputs.composer_cache_key && format('{0} | {1}', inputs.composer_cache_key, matrix.magento) || matrix.magento }} 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@main
with: with:
path: ${{ inputs.path }} path: ${{ inputs.path }}
composer_auth: ${{ secrets.composer_auth }} composer_auth: ${{ secrets.composer_auth }}
integration_test: integration_test:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
needs: compute_resolved
if: ${{ fromJSON(needs.compute_resolved.outputs.resolved)['integration_test'].enabled != false }}
strategy: strategy:
matrix: ${{ fromJSON(inputs.matrix) }} matrix: ${{ fromJSON(needs.compute_resolved.outputs.resolved)['integration_test'].matrix }}
fail-fast: ${{ inputs.fail-fast }} fail-fast: ${{ inputs.fail-fast }}
services: ${{ matrix.services }} services: ${{ matrix.services }}
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v6
- uses: graycoreio/github-actions-magento2/setup-magento@v8.0.0-rc.1 - uses: graycoreio/github-actions-magento2/setup-magento@main
id: setup-magento id: setup-magento
with: with:
php-version: ${{ matrix.php }} php-version: ${{ matrix.php }}
@@ -217,8 +251,10 @@ jobs:
- run: composer update --no-install - run: composer update --no-install
working-directory: ${{ steps.setup-magento.outputs.path }} working-directory: ${{ steps.setup-magento.outputs.path }}
env:
- uses: graycoreio/github-actions-magento2/cache-magento@v8.0.0-rc.1 COMPOSER_AUTH: ${{ secrets.composer_auth }}
- uses: graycoreio/github-actions-magento2/cache-magento@main
with: with:
composer_cache_key: ${{ inputs.composer_cache_key && format('{0} | {1}', inputs.composer_cache_key, matrix.magento) || matrix.magento }} 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 }} working-directory: ${{ steps.setup-magento.outputs.path }}
@@ -231,7 +267,7 @@ jobs:
COMPOSER_AUTH: ${{ secrets.composer_auth }} COMPOSER_AUTH: ${{ secrets.composer_auth }}
COMPOSER_MIRROR_PATH_REPOS: 1 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@main
id: magento-version id: magento-version
with: with:
working-directory: ${{ steps.setup-magento.outputs.path }} working-directory: ${{ steps.setup-magento.outputs.path }}
@@ -265,7 +301,7 @@ jobs:
run: ../../../vendor/bin/phpunit -c phpunit.xml.dist --testsuite Extension_Integration_Tests run: ../../../vendor/bin/phpunit -c phpunit.xml.dist --testsuite Extension_Integration_Tests
- name: Upload test sandbox dir - name: Upload test sandbox dir
uses: actions/upload-artifact@v6 uses: actions/upload-artifact@v7
if: failure() if: failure()
with: with:
name: sandbox-data-${{ steps.magento-version.outputs.version }} name: sandbox-data-${{ steps.magento-version.outputs.version }}
+91 -11
View File
@@ -35,7 +35,7 @@ jobs:
compute_matrix: compute_matrix:
runs-on: ubuntu-latest runs-on: ubuntu-latest
outputs: outputs:
matrix: ${{ steps.supported-version.outputs.matrix }} resolved: ${{ steps.resolve.outputs.resolved }}
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v6
if: inputs.store_artifact_name == '' if: inputs.store_artifact_name == ''
@@ -46,22 +46,30 @@ jobs:
name: ${{ inputs.store_artifact_name }} name: ${{ inputs.store_artifact_name }}
path: ${{ inputs.path }} path: ${{ inputs.path }}
- uses: graycoreio/github-actions-magento2/get-magento-version@v8.0.0-rc.1 - uses: graycoreio/github-actions-magento2/get-magento-version@main
id: get-magento-version id: get-magento-version
with: with:
working-directory: ${{ inputs.path }} working-directory: ${{ inputs.path }}
- uses: graycoreio/github-actions-magento2/supported-version@v8.0.0-rc.1 - uses: graycoreio/github-actions-magento2/supported-version@main
id: supported-version id: supported-version
with: with:
project: ${{ steps.get-magento-version.outputs.supported_version_project }}
kind: custom kind: custom
custom_versions: ${{ steps.get-magento-version.outputs.project }}:${{ fromJSON(steps.get-magento-version.outputs.version) }} custom_versions: ${{ steps.get-magento-version.outputs.project }}:${{ fromJSON(steps.get-magento-version.outputs.version) }}
- uses: graycoreio/github-actions-magento2/resolve-check-config@main
id: resolve
with:
kind: store
matrix: ${{ steps.supported-version.outputs.matrix }}
unit-test: unit-test:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
needs: compute_matrix needs: compute_matrix
if: ${{ fromJSON(needs.compute_matrix.outputs.resolved)['unit-test'].enabled != false }}
strategy: strategy:
matrix: ${{ fromJSON(needs.compute_matrix.outputs.matrix) }} matrix: ${{ fromJSON(needs.compute_matrix.outputs.resolved)['unit-test'].matrix }}
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v6
@@ -73,7 +81,7 @@ jobs:
name: ${{ inputs.store_artifact_name }} name: ${{ inputs.store_artifact_name }}
path: ${{ inputs.path }} path: ${{ inputs.path }}
- uses: graycoreio/github-actions-magento2/setup-magento@v8.0.0-rc.1 - uses: graycoreio/github-actions-magento2/setup-magento@main
id: setup-magento id: setup-magento
with: with:
php-version: ${{ matrix.php }} php-version: ${{ matrix.php }}
@@ -82,7 +90,7 @@ jobs:
working-directory: ${{ inputs.path }} working-directory: ${{ inputs.path }}
composer_auth: ${{ secrets.composer_auth }} composer_auth: ${{ secrets.composer_auth }}
- uses: graycoreio/github-actions-magento2/cache-magento@v8.0.0-rc.1 - uses: graycoreio/github-actions-magento2/cache-magento@main
with: with:
composer_cache_key: ${{ inputs.composer_cache_key }} composer_cache_key: ${{ inputs.composer_cache_key }}
working-directory: ${{ steps.setup-magento.outputs.path }} working-directory: ${{ steps.setup-magento.outputs.path }}
@@ -106,6 +114,12 @@ jobs:
EOF EOF
sed -i '/<testsuites>/r /tmp/testsuite.xml' dev/tests/unit/phpunit.xml.dist 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 ) ## Disable allure (See https://github.com/magento/magento2/issues/36702 )
sed -i '/<extensions>/,/<\/extensions>/d' dev/tests/unit/phpunit.xml.dist sed -i '/<extensions>/,/<\/extensions>/d' dev/tests/unit/phpunit.xml.dist
@@ -116,8 +130,9 @@ jobs:
coding-standard: coding-standard:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
needs: compute_matrix needs: compute_matrix
if: ${{ fromJSON(needs.compute_matrix.outputs.resolved)['coding-standard'].enabled != false }}
strategy: strategy:
matrix: ${{ fromJSON(needs.compute_matrix.outputs.matrix) }} matrix: ${{ fromJSON(needs.compute_matrix.outputs.resolved)['coding-standard'].matrix }}
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v6
@@ -129,7 +144,7 @@ jobs:
name: ${{ inputs.store_artifact_name }} name: ${{ inputs.store_artifact_name }}
path: ${{ inputs.path }} path: ${{ inputs.path }}
- uses: graycoreio/github-actions-magento2/setup-magento@v8.0.0-rc.1 - uses: graycoreio/github-actions-magento2/setup-magento@main
id: setup-magento id: setup-magento
with: with:
php-version: ${{ matrix.php }} php-version: ${{ matrix.php }}
@@ -138,7 +153,7 @@ jobs:
working-directory: ${{ inputs.path }} working-directory: ${{ inputs.path }}
composer_auth: ${{ secrets.composer_auth }} composer_auth: ${{ secrets.composer_auth }}
- uses: graycoreio/github-actions-magento2/cache-magento@v8.0.0-rc.1 - uses: graycoreio/github-actions-magento2/cache-magento@main
with: with:
composer_cache_key: ${{ inputs.composer_cache_key }} composer_cache_key: ${{ inputs.composer_cache_key }}
working-directory: ${{ steps.setup-magento.outputs.path }} working-directory: ${{ steps.setup-magento.outputs.path }}
@@ -163,7 +178,72 @@ jobs:
EOF EOF
fi fi
- uses: graycoreio/github-actions-magento2/coding-standard@v8.0.0-rc.1 - uses: graycoreio/github-actions-magento2/coding-standard@main
with: with:
path: ${{ steps.setup-magento.outputs.path }} path: ${{ steps.setup-magento.outputs.path }}
composer_auth: ${{ secrets.composer_auth }} composer_auth: ${{ secrets.composer_auth }}
smoke-test:
runs-on: ${{ matrix.os }}
needs: compute_matrix
if: ${{ fromJSON(needs.compute_matrix.outputs.resolved)['smoke-test'].enabled != false }}
services: ${{ matrix.services }}
strategy:
matrix: ${{ fromJSON(needs.compute_matrix.outputs.resolved)['smoke-test'].matrix }}
steps:
- uses: actions/checkout@v6
if: inputs.store_artifact_name == ''
- uses: actions/download-artifact@v8
if: inputs.store_artifact_name != ''
with:
name: ${{ inputs.store_artifact_name }}
path: ${{ inputs.path }}
- uses: graycoreio/github-actions-magento2/setup-magento@main
id: setup-magento
with:
php-version: ${{ matrix.php }}
tools: composer:v${{ matrix.composer }}
mode: store
working-directory: ${{ inputs.path }}
composer_auth: ${{ secrets.composer_auth }}
- uses: graycoreio/github-actions-magento2/cache-magento@main
with:
composer_cache_key: ${{ inputs.composer_cache_key }}
working-directory: ${{ steps.setup-magento.outputs.path }}
stamp: ${{ inputs.stamp }}
- name: Composer install
working-directory: ${{ steps.setup-magento.outputs.path }}
run: composer install
env:
COMPOSER_AUTH: ${{ secrets.composer_auth }}
- uses: graycoreio/github-actions-magento2/setup-install@main
id: setup-install
with:
services: ${{ toJSON(matrix.services) }}
path: ${{ steps.setup-magento.outputs.path }}
container_id: ${{ job.services['php-fpm'].id }}
extra_args: --magento-init-params=MAGE_MODE=developer
- uses: graycoreio/github-actions-magento2/configure-service-nginx@main
with:
container_id: ${{ job.services.nginx.id }}
magento_path: ${{ inputs.path }}
- uses: graycoreio/github-actions-magento2/smoke-test@main
if: contains(fromJSON(needs.compute_matrix.outputs.resolved)['smoke-test'].probes, 'page')
with:
kind: page
## graphql is opt-in: editions without GraphQL modules (e.g. mage-os
## minimal) have no /graphql endpoint. Enable it per store via
## `.github/check-store.json` -> jobs.smoke-test.probes: ["page", "graphql"].
- uses: graycoreio/github-actions-magento2/smoke-test@main
if: contains(fromJSON(needs.compute_matrix.outputs.resolved)['smoke-test'].probes, 'graphql')
with:
kind: graphql
+2 -2
View File
@@ -82,7 +82,7 @@ jobs:
COMPOSER_AUTH: ${{ secrets.composer_auth }} COMPOSER_AUTH: ${{ secrets.composer_auth }}
name: Create Magento ${{ matrix.magento }} Project 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@main
id: magento-version id: magento-version
with: with:
working-directory: ${{ inputs.magento_directory }} working-directory: ${{ inputs.magento_directory }}
@@ -182,7 +182,7 @@ jobs:
name: Run Integration Tests name: Run Integration Tests
- name: Upload test sandbox dir - name: Upload test sandbox dir
uses: actions/upload-artifact@v6 uses: actions/upload-artifact@v7
if: failure() if: failure()
with: with:
name: sandbox-data-${{ steps.magento-version.outputs.version }} name: sandbox-data-${{ steps.magento-version.outputs.version }}
+10 -6
View File
@@ -6,11 +6,15 @@ on:
- main - main
workflow_dispatch: workflow_dispatch:
inputs: inputs:
release-candidate: release-mode:
description: 'Cut a release-candidate (prerelease) instead of a normal release.' description: 'auto = follow conventional commits; rc = bump the rc suffix; graduate = graduate the current rc to a stable release.'
type: boolean type: choice
required: false required: false
default: false default: auto
options:
- auto
- rc
- graduate
env: env:
RELEASE_BRANCH: release-please--branches--main--components--github-actions-magento2 RELEASE_BRANCH: release-please--branches--main--components--github-actions-magento2
@@ -24,10 +28,10 @@ jobs:
releases_created: ${{ steps.release.outputs.releases_created }} releases_created: ${{ steps.release.outputs.releases_created }}
steps: steps:
- id: release - id: release
uses: googleapis/release-please-action@v4 uses: googleapis/release-please-action@v5
with: with:
token: ${{ secrets.GRAYCORE_GITHUB_TOKEN }} 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 - name: Check if release branch exists
id: branch-check id: branch-check
+1 -1
View File
@@ -1 +1 @@
{".":"8.0.0-rc.1"} {".":"8.5.0"}
+76
View File
@@ -2,6 +2,82 @@
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. All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
## [8.5.0](https://github.com/graycoreio/github-actions-magento2/compare/v8.4.0...v8.5.0) (2026-05-27)
### Features
* **setup-install:** run with --no-interaction ([32a5fd2](https://github.com/graycoreio/github-actions-magento2/commit/32a5fd2badfe558e7dced9606765d0d44632c6f0))
## [8.4.0](https://github.com/graycoreio/github-actions-magento2/compare/v8.3.0...v8.4.0) (2026-05-25)
### Features
* remove rabbitmq from supported-version for mage-os/minimal ([83f9433](https://github.com/graycoreio/github-actions-magento2/commit/83f9433da0d7f20efbf090fd8ed75a0a39000797))
## [8.3.0](https://github.com/graycoreio/github-actions-magento2/compare/v8.2.0...v8.3.0) (2026-05-25)
### Features
* **check-store:** use the project when computing underlying version requirements ([fa8e597](https://github.com/graycoreio/github-actions-magento2/commit/fa8e59736563d5969f5c8ebaccd23c48f0628721))
* **get-magento-version:** add support for MageOS minimal distro ([863444a](https://github.com/graycoreio/github-actions-magento2/commit/863444afbd137d32157392b964f06503f021ee6c))
* **get-magento-version:** emit supported-version project name as an output ([1ea5a10](https://github.com/graycoreio/github-actions-magento2/commit/1ea5a10ef67d6fda8d10e078895adc9bea434477))
* **supported-version:** add support for MageOS 3 ([1e63c01](https://github.com/graycoreio/github-actions-magento2/commit/1e63c019edb63ee0bcd4576b4125b73520ca8864))
* **supported-version:** add support for MageOS Minimal edition ([befe080](https://github.com/graycoreio/github-actions-magento2/commit/befe0807f7636c125d7e650f2d08012b28554a54))
## [8.2.0](https://github.com/graycoreio/github-actions-magento2/compare/v8.1.0...v8.2.0) (2026-05-17)
### Features
* **check-extension:** allow configuraton via .github/check-extension.json ([#269](https://github.com/graycoreio/github-actions-magento2/issues/269)) ([0bf08ef](https://github.com/graycoreio/github-actions-magento2/commit/0bf08ef69291090e5fe3e3d47cb432c6c9107f30))
* **resolve-check-config:** defined required integration test services required ([#269](https://github.com/graycoreio/github-actions-magento2/issues/269)) ([35c1ace](https://github.com/graycoreio/github-actions-magento2/commit/35c1ace2bc68be1356dc6565a8a05ff02e33d75d))
## [8.1.0](https://github.com/graycoreio/github-actions-magento2/compare/v8.0.0...v8.1.0) (2026-05-17)
### Features
* **check-store:** add smoke-test action and use resolve-check-config ([#255](https://github.com/graycoreio/github-actions-magento2/issues/255)) ([e39dd46](https://github.com/graycoreio/github-actions-magento2/commit/e39dd46f9c53a0d2625cd5d19ad1cf18565b8c5c))
* **configure-service-nginx:** add ability to adjust nginx conf after init ([#255](https://github.com/graycoreio/github-actions-magento2/issues/255)) ([0c7d14d](https://github.com/graycoreio/github-actions-magento2/commit/0c7d14d88573d92c81654b1107ef6a9e4d918cff))
* **resolve-check-config:** add ability to use a config file to adjust jobs ([#255](https://github.com/graycoreio/github-actions-magento2/issues/255)) ([b98313e](https://github.com/graycoreio/github-actions-magento2/commit/b98313e10044a0a6a04546d3ff8ebe3a3f284f5b))
* **setup-install:** add a container_id input to run setup:install against a specific container ([#255](https://github.com/graycoreio/github-actions-magento2/issues/255)) ([6d4ca8d](https://github.com/graycoreio/github-actions-magento2/commit/6d4ca8d669164d840d99e8af721309abb9f204ea))
* **smoke-test:** add simple smoke test action ([#255](https://github.com/graycoreio/github-actions-magento2/issues/255)) ([b790da1](https://github.com/graycoreio/github-actions-magento2/commit/b790da18597e58a9013cc0f7e2c923f08c82f813))
* **supported-version:** add service_preferences and support for php-fpm and nginx ([#255](https://github.com/graycoreio/github-actions-magento2/issues/255)) ([e89f6ad](https://github.com/graycoreio/github-actions-magento2/commit/e89f6ad2e08fcaa03cba92c8371e60ba67b3cf62))
### Bug Fixes
* **check-extension:** only run coding-standard on most recent version of Magento ([#265](https://github.com/graycoreio/github-actions-magento2/issues/265)) ([8e82fcc](https://github.com/graycoreio/github-actions-magento2/commit/8e82fcc89354c83523781c1f5fd4622dec19ca7b))
## [8.0.0](https://github.com/graycoreio/github-actions-magento2/compare/v8.0.0-rc.2...v8.0.0) (2026-05-14)
### 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) ## [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) ### Extension (download cache only)
```yml ```yml
- uses: graycoreio/github-actions-magento2/cache-magento@v7.0.0 # x-release-please-version - uses: graycoreio/github-actions-magento2/cache-magento@v8.5.0 # x-release-please-version
with: with:
composer_cache_key: ${{ inputs.composer_cache_key }} 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) ### Extension or store (download + vendor stamp cache)
```yml ```yml
- uses: graycoreio/github-actions-magento2/setup-magento@v7.0.0 # x-release-please-version - uses: graycoreio/github-actions-magento2/setup-magento@v8.5.0 # x-release-please-version
id: setup-magento id: setup-magento
with: with:
mode: extension # or store 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.5.0 # x-release-please-version
with: with:
composer_cache_key: ${{ inputs.composer_cache_key }} composer_cache_key: ${{ inputs.composer_cache_key }}
working-directory: ${{ steps.setup-magento.outputs.path }} 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: > **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 > ```yml
> - uses: graycoreio/github-actions-magento2/cache-magento@v7.0.0 # x-release-please-version > - uses: graycoreio/github-actions-magento2/cache-magento@v8.5.0 # x-release-please-version
> with: > with:
> stamp: ${{ github.actor != 'dependabot[bot]' }} > stamp: ${{ github.actor != 'dependabot[bot]' }}
> ``` > ```
+1 -1
View File
@@ -62,7 +62,7 @@ runs:
exit 1 exit 1
fi fi
- uses: graycoreio/github-actions-magento2/get-magento-version@v8.0.0-rc.1 - uses: graycoreio/github-actions-magento2/get-magento-version@main
id: cache-magento-get-magento-version id: cache-magento-get-magento-version
with: with:
working-directory: ${{ inputs.working-directory }} working-directory: ${{ inputs.working-directory }}
+1 -1
View File
@@ -36,7 +36,7 @@ jobs:
tools: composer:v2 tools: composer:v2
coverage: none coverage: none
- uses: graycoreio/github-actions-magento2/coding-standard@v7.0.0 # x-release-please-version - uses: graycoreio/github-actions-magento2/coding-standard@v8.5.0 # x-release-please-version
with: with:
path: app/code # Optional, defaults to . path: app/code # Optional, defaults to .
version: 25 # Optional, will use the latest if omitted. version: 25 # Optional, will use the latest if omitted.
+2 -2
View File
@@ -52,12 +52,12 @@ runs:
fi fi
- name: Get Composer Version - 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@main
id: get-composer-version id: get-composer-version
if: steps.check-installed.outputs.installed != 'true' if: steps.check-installed.outputs.installed != 'true'
- name: Check if allow-plugins option is available for this version of composer - 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@main
id: is-allow-plugins-available id: is-allow-plugins-available
if: steps.check-installed.outputs.installed != 'true' if: steps.check-installed.outputs.installed != 'true'
with: 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) The Magento matrix format outlined by the [supported versions action.](https://github.com/graycoreio/github-actions-magento2/tree/main/supported-version/supported.json)
## Configuration
Each check can be toggled on/off through an optional `.github/check-extension.json` file in the repo that calls this workflow.
You can learn more about this file here in the [`resolve-check-config` action.](../../resolve-check-config/README.md):
Reference the published JSON Schema with `$schema` to get autocompletion and inline validation in editors that support it — see [`check-extension.schema.json`](../../resolve-check-config/check-extension.schema.json):
```json
{
"$schema": "https://raw.githubusercontent.com/graycoreio/github-actions-magento2/main/resolve-check-config/check-extension.schema.json",
"jobs": {
"integration_test": false
}
}
```
## Usage ## Usage
```yml ```yml
@@ -38,12 +55,12 @@ jobs:
matrix: ${{ steps.supported-version.outputs.matrix }} matrix: ${{ steps.supported-version.outputs.matrix }}
steps: steps:
- uses: actions/checkout@v6 - 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.5.0 # x-release-please-version
id: supported-version id: supported-version
- run: echo ${{ steps.supported-version.outputs.matrix }} - run: echo ${{ steps.supported-version.outputs.matrix }}
check-extension: check-extension:
needs: compute_matrix 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.5.0 # x-release-please-version
with: with:
matrix: ${{ needs.compute_matrix.outputs.matrix }} 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. - **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. - **Coding Standard** — runs the Magento Coding Standard against `app/code`. Uses your `phpcs.xml` (or `.phpcs.xml`, `phpcs.xml.dist`, `.phpcs.xml.dist`) if one exists, otherwise a sensible default is generated.
- **Smoke Test** — boots your store against the supported-version service set (mysql, search, queue, cache, nginx, php-fpm) and runs the smoke probes against it.
## Configuration
Each check can be toggled on/off through an optional `.github/check-store.json` file in the repo that calls this workflow.
You can learn more about this file here in the [`resolve-check-config` action.](../../resolve-check-config/README.md):
Reference the published JSON Schema with `$schema` to get autocompletion and inline validation in editors that support it — see [`check-store.schema.json`](../../resolve-check-config/check-store.schema.json):
```json
{
"$schema": "https://raw.githubusercontent.com/graycoreio/github-actions-magento2/main/resolve-check-config/check-store.schema.json",
"jobs": {
"coding-standard": false
}
}
```
## Usage ## Usage
@@ -40,7 +58,7 @@ on:
jobs: jobs:
check-store: 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.5.0 # x-release-please-version
secrets: secrets:
composer_auth: ${{ secrets.COMPOSER_AUTH }} 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 ```yml
jobs: jobs:
check-store: 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.5.0 # x-release-please-version
secrets: secrets:
composer_auth: ${{ secrets.COMPOSER_AUTH }} composer_auth: ${{ secrets.COMPOSER_AUTH }}
``` ```
+2 -2
View File
@@ -50,13 +50,13 @@ jobs:
matrix: ${{ steps.supported-version.outputs.matrix }} matrix: ${{ steps.supported-version.outputs.matrix }}
steps: steps:
- uses: actions/checkout@v6 - 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.5.0 # x-release-please-version
with: with:
include_services: true include_services: true
id: supported-version id: supported-version
integration-workflow: integration-workflow:
needs: compute_matrix 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.5.0 # x-release-please-version
with: with:
package_name: my-vendor/package package_name: my-vendor/package
matrix: ${{ needs.compute_matrix.outputs.matrix }} matrix: ${{ needs.compute_matrix.outputs.matrix }}
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - 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.5.0 # x-release-please-version
with: with:
magento_directory: path/to/magento magento_directory: path/to/magento
``` ```
+1 -1
View File
@@ -9,7 +9,7 @@ inputs:
runs: runs:
using: "composite" using: "composite"
steps: steps:
- uses: graycoreio/github-actions-magento2/get-magento-version@v8.0.0-rc.1 - uses: graycoreio/github-actions-magento2/get-magento-version@main
id: init-magento-get-magento-version id: init-magento-get-magento-version
with: with:
working-directory: ${{ inputs.magento_directory }} working-directory: ${{ inputs.magento_directory }}
+1 -1
View File
@@ -25,7 +25,7 @@ jobs:
name: A job to compute an installed Composer version. name: A job to compute an installed Composer version.
steps: steps:
- uses: actions/checkout@v6 - 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.5.0 # x-release-please-version
id: get-composer-version id: get-composer-version
- run: echo version ${{ steps.get-composer-version.outputs.version }} - run: echo version ${{ steps.get-composer-version.outputs.version }}
shell: bash shell: bash
+1 -1
View File
@@ -25,7 +25,7 @@ jobs:
name: A job to compute an installed Magento version. name: A job to compute an installed Magento version.
steps: steps:
- uses: actions/checkout@v6 - 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.5.0 # x-release-please-version
id: get-magento-version id: get-magento-version
- run: echo version ${{ steps.get-magento-version.outputs.version }} - run: echo version ${{ steps.get-magento-version.outputs.version }}
shell: bash shell: bash
+3
View File
@@ -15,6 +15,9 @@ outputs:
project: project:
description: 'The Magento project package name (e.g. magento/project-community-edition)' description: 'The Magento project package name (e.g. magento/project-community-edition)'
value: ${{ steps.get-magento-version.outputs.project }} 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: runs:
using: "composite" 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 set -uo pipefail
WORKING_DIR="${1:-.}" 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" cd "$WORKING_DIR"
@@ -16,5 +16,13 @@ PRODUCT=$(echo "${RESULT:-}" | awk '{print $1}')
VERSION=$(echo "${RESULT:-}" | awk '{print $2}' | sed 's/^v//') VERSION=$(echo "${RESULT:-}" | awk '{print $2}' | sed 's/^v//')
PROJECT=$(echo "$PRODUCT" | sed 's/product-/project-/') 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 "version=\"$VERSION\""
echo "project=$PROJECT" 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") OUT=$(bash "$SCRIPT" "$FIXTURES/store-lock")
assert_eq "store lock: version" '"2.4.7"' "$(field "$OUT" version)" 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: 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") OUT=$(bash "$SCRIPT" "$FIXTURES/store-v-prefix")
assert_eq "store v-prefix: version" '"2.4.6"' "$(field "$OUT" version)" assert_eq "store v-prefix: version" '"2.4.6"' "$(field "$OUT" version)"
OUT=$(bash "$SCRIPT" "$FIXTURES/enterprise") OUT=$(bash "$SCRIPT" "$FIXTURES/enterprise")
assert_eq "enterprise: version" '"2.4.7-p1"' "$(field "$OUT" version)" 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: 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") OUT=$(bash "$SCRIPT" "$FIXTURES/mage-os")
assert_eq "mage-os: version" '"1.0.0"' "$(field "$OUT" version)" 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: 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") OUT=$(bash "$SCRIPT" "$FIXTURES/store-json")
assert_eq "store json: version" '"2.4.6-p1"' "$(field "$OUT" version)" 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: 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") OUT=$(bash "$SCRIPT" "$FIXTURES/extension")
assert_eq "extension: version" '""' "$(field "$OUT" version)" assert_eq "extension: version" '""' "$(field "$OUT" version)"
assert_eq "extension: project" "" "$(field "$OUT" project)" assert_eq "extension: project" "" "$(field "$OUT" project)"
assert_eq "extension: supported_version_project" "" "$(field "$OUT" supported_version_project)"
OUT=$(bash "$SCRIPT" "$FIXTURES/empty") OUT=$(bash "$SCRIPT" "$FIXTURES/empty")
assert_eq "empty dir: version" '""' "$(field "$OUT" version)" assert_eq "empty dir: version" '""' "$(field "$OUT" version)"
assert_eq "empty dir: project" "" "$(field "$OUT" project)" assert_eq "empty dir: project" "" "$(field "$OUT" project)"
assert_eq "empty dir: supported_version_project" "" "$(field "$OUT" supported_version_project)"
echo "" echo ""
echo "$PASS passed, $FAIL failed" echo "$PASS passed, $FAIL failed"
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "@graycoreio/github-actions-magento2", "name": "@graycoreio/github-actions-magento2",
"version": "8.0.0-rc.1", "version": "8.5.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@graycoreio/github-actions-magento2", "name": "@graycoreio/github-actions-magento2",
"version": "8.0.0-rc.1", "version": "8.5.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@actions/core": "^1.11.1", "@actions/core": "^1.11.1",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@graycoreio/github-actions-magento2", "name": "@graycoreio/github-actions-magento2",
"version": "8.0.0-rc.1", "version": "8.5.0",
"description": "Github Actions for Magento 2", "description": "Github Actions for Magento 2",
"scripts": { "scripts": {
"test": "cd supported-version && npm run test && cd - && cd setup-install && npm run test && cd -", "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 }
]
}
}
}
+62
View File
@@ -0,0 +1,62 @@
# "Resolve Check Config" Action
Reads `.github/check-<kind>.json` (or a path you specify), validates job names against the known list for the selected workflow kind, and emits a per-job filtered version of the `supported-version` matrix. Each job in the output carries an `enabled` flag and its own `matrix`, where every entry's `services` map has been narrowed to the tiers that job actually needs. Consumers gate each job with `fromJSON(...)['<job>'].enabled != false` and feed `fromJSON(...)['<job>'].matrix` into `strategy.matrix`.
A missing config file is fine — every known job is emitted with its default tier list.
## Schemas
Reference the published JSON Schema from your config's `$schema` key for autocompletion and inline validation in editors that support it:
- [`check-store.schema.json`](./check-store.schema.json) — config for the [MageCheck Store](../docs/workflows/check-store.md) workflow
- [`check-extension.schema.json`](./check-extension.schema.json) — config for the [MageCheck Extension](../docs/workflows/check-extension.md) workflow
## Inputs
| Input | Description | Required | Default |
|---------------|--------------------------------------------------------------------------------------------------------------------------------------------------------|----------|-------------------------------|
| `kind` | Which reusable workflow this config belongs to: `store` or `extension`. Selects the default `config_path`, the known-job list, and the per-job tier defaults. | true | |
| `matrix` | The matrix JSON emitted by the `supported-version` action. Each entry's `services` map is filtered per-job based on the resolved tier list. | true | |
| `config_path` | Path to the check-config JSON file, relative to the runner workspace. | false | `.github/check-<kind>.json` |
## Usage
```yml
jobs:
compute_matrix:
runs-on: ubuntu-latest
outputs:
resolved: ${{ steps.resolve.outputs.resolved }}
steps:
- uses: graycoreio/github-actions-magento2/supported-version@v8.5.0 # x-release-please-version
id: supported-version
with:
kind: currently-supported
- uses: graycoreio/github-actions-magento2/resolve-check-config@v8.5.0 # x-release-please-version
id: resolve
with:
kind: store
matrix: ${{ steps.supported-version.outputs.matrix }}
smoke-test:
runs-on: ${{ matrix.os }}
needs: compute_matrix
if: ${{ fromJSON(needs.compute_matrix.outputs.resolved)['smoke-test'].enabled != false }}
services: ${{ matrix.services }}
strategy:
matrix: ${{ fromJSON(needs.compute_matrix.outputs.resolved)['smoke-test'].matrix }}
steps:
- run: echo "running with ${{ toJSON(matrix.services) }}"
```
Example `.github/check-store.json` for opting out of a specific job:
```json
{
"$schema": "https://raw.githubusercontent.com/graycoreio/github-actions-magento2/main/resolve-check-config/check-store.schema.json",
"jobs": {
"coding-standard": false
}
}
```
+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,62 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://raw.githubusercontent.com/graycoreio/github-actions-magento2/main/resolve-check-config/check-store.schema.json",
"title": "graycoreio check-store config",
"description": "Configuration consumed by the check-store reusable workflow. Per-job toggles and settings live under `jobs`. Top-level remains open for future global keys.",
"type": "object",
"properties": {
"jobs": {
"type": "object",
"description": "Per-job configuration. Each key is a job name declared by check-store; unknown keys are rejected.",
"properties": {
"unit-test": { "$ref": "#/$defs/jobConfig" },
"coding-standard": { "$ref": "#/$defs/jobConfig" },
"smoke-test": { "$ref": "#/$defs/smokeJobConfig" }
},
"additionalProperties": false
}
},
"additionalProperties": true,
"$defs": {
"jobConfig": {
"description": "How a single job should be configured. Boolean form is shorthand for { enabled: <bool> }; object form allows extra per-job keys.",
"oneOf": [
{ "type": "boolean" },
{
"type": "object",
"properties": {
"enabled": {
"type": "boolean",
"description": "Whether the job should run. Defaults to true when the key is present.",
"default": true
}
},
"additionalProperties": true
}
]
},
"smokeJobConfig": {
"description": "How the smoke-test job should be configured. Boolean form is shorthand for { enabled: <bool> }; object form adds a `probes` list on top of `enabled`.",
"oneOf": [
{ "type": "boolean" },
{
"type": "object",
"properties": {
"enabled": {
"type": "boolean",
"description": "Whether the job should run. Defaults to true when the key is present.",
"default": true
},
"probes": {
"type": "array",
"description": "Which smoke-test probes to run. Defaults to [\"page\"]. Add \"graphql\" to also probe the GraphQL endpoint — only for editions that ship GraphQL modules (the mage-os minimal edition does not, so /graphql 404s there).",
"items": { "enum": ["page", "graphql"] },
"default": ["page"]
}
},
"additionalProperties": true
}
]
}
}
}
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,122 @@
import { KNOWN_JOBS_STORE, resolveStoreConfig, STORE_JOBS } from './store';
import { Matrix } from '../types';
const MATRIX: Matrix = {
include: [{
php: '8.3',
services: {
mysql: { image: 'mysql:8' },
opensearch: { image: 'opensearchproject/opensearch:2' },
rabbitmq: { image: 'rabbitmq:3' },
valkey: { image: 'valkey:8' },
nginx: { image: 'nginx:1.27' },
'php-fpm': { image: 'php:8.3-fpm' },
},
}],
};
describe('STORE_JOBS', () => {
it('declares the check-store jobs', () => {
expect(Object.keys(STORE_JOBS).sort()).toEqual(['coding-standard', 'smoke-test', 'unit-test']);
});
it('declares smoke-test required tiers (end-user cannot toggle)', () => {
expect(STORE_JOBS['smoke-test'].services).toEqual([]);
expect([...STORE_JOBS['smoke-test'].requiredServices!].sort()).toEqual([
'cache',
'db',
'queue',
'search',
'web',
]);
});
it('defaults smoke-test to the page probe only (graphql is opt-in)', () => {
expect(STORE_JOBS['smoke-test'].probes).toEqual(['page']);
});
it('exposes empty service defaults for unit-test and coding-standard', () => {
expect(STORE_JOBS['unit-test'].services).toEqual([]);
expect(STORE_JOBS['coding-standard'].services).toEqual([]);
});
it('keeps KNOWN_JOBS_STORE in sync with the map keys', () => {
expect([...KNOWN_JOBS_STORE].sort()).toEqual(Object.keys(STORE_JOBS).sort());
});
});
describe('resolveStoreConfig', () => {
it('emits every known job with default tier expansion, always including mysql for smoke-test', () => {
const resolved = resolveStoreConfig({}, MATRIX);
expect(Object.keys(resolved).sort()).toEqual(['coding-standard', 'smoke-test', 'unit-test']);
expect(resolved['unit-test'].matrix.include[0].services).toEqual({});
expect(Object.keys(resolved['smoke-test'].matrix.include[0].services!).sort()).toEqual([
'mysql',
'nginx',
'opensearch',
'php-fpm',
'rabbitmq',
'valkey',
]);
});
it('keeps every required service even when caller overrides smoke-test services to []', () => {
const resolved = resolveStoreConfig(
{ jobs: { 'smoke-test': { services: [] } } },
MATRIX,
);
expect(Object.keys(resolved['smoke-test'].matrix.include[0].services!).sort()).toEqual([
'mysql',
'nginx',
'opensearch',
'php-fpm',
'rabbitmq',
'valkey',
]);
});
it('honors enabled=false for a job', () => {
const resolved = resolveStoreConfig(
{ jobs: { 'smoke-test': false } },
MATRIX,
);
expect(resolved['smoke-test'].enabled).toBe(false);
});
it('emits the default page-only probe list for smoke-test', () => {
const resolved = resolveStoreConfig({}, MATRIX);
expect(resolved['smoke-test'].probes).toEqual(['page']);
});
it('honors a smoke-test probes override', () => {
const resolved = resolveStoreConfig(
{ jobs: { 'smoke-test': { probes: ['page', 'graphql'] } } },
MATRIX,
);
expect(resolved['smoke-test'].probes).toEqual(['page', 'graphql']);
});
it('does not emit probes on jobs without a probe concept', () => {
const resolved = resolveStoreConfig({}, MATRIX);
expect(resolved['unit-test'].probes).toBeUndefined();
expect(resolved['coding-standard'].probes).toBeUndefined();
});
it('rejects probes on a job that does not support it', () => {
expect(() => resolveStoreConfig({ jobs: { 'unit-test': { probes: ['page'] } } }, MATRIX)).toThrowError(
/job "unit-test" does not support "probes"/
);
});
it('throws on a typo in the job name', () => {
expect(() => resolveStoreConfig({ jobs: { 'smkoe-test': false } }, MATRIX)).toThrowError(
/unknown job "smkoe-test" for kind "store"/
);
});
it('throws when an extension-only job name is used', () => {
expect(() => resolveStoreConfig({ jobs: { 'unit-test-extension': false } }, MATRIX)).toThrowError(
/unknown job "unit-test-extension" for kind "store"/
);
});
});
+29
View File
@@ -0,0 +1,29 @@
import { resolveJobs } from '../parse';
import { JobDefaults, Matrix, RawConfig, ResolvedConfig } from '../types';
/**
* Per-job defaults for the `check-store.yaml` reusable workflow.
* Edit this map when a job is added, removed, or renamed in that
* workflow — keys are validated against caller config and the values
* supply the default tier list used when the caller doesn't override
* `services` themselves.
*/
export const STORE_JOBS: Record<string, JobDefaults> = {
'unit-test': { services: [] },
'coding-standard': { services: [] },
'smoke-test': {
services: [],
requiredServices: ['db', 'search', 'queue', 'cache', 'web'],
probes: ['page'],
},
};
export const KNOWN_JOBS_STORE: readonly string[] = Object.keys(STORE_JOBS);
/**
* Resolves a parsed config file + supported-version matrix against
* the check-store job list. Thin wrapper that binds the kind and the
* per-job defaults so callers don't repeat the wiring.
*/
export const resolveStoreConfig = (raw: RawConfig, matrix: Matrix): ResolvedConfig =>
resolveJobs(raw, 'store', STORE_JOBS, matrix);
+367
View File
@@ -0,0 +1,367 @@
import {
filterEntryServices,
filterMatrixForJob,
mergeRequiredTiers,
normalizeJobEntry,
normalizeProbes,
parseMatrixInput,
parseRawConfig,
resolveJobs,
} from './parse';
import { JobDefaults, Matrix } from './types';
const FULL_SERVICES = {
mysql: { image: 'mysql:8' },
opensearch: { image: 'opensearchproject/opensearch:2' },
rabbitmq: { image: 'rabbitmq:3' },
valkey: { image: 'valkey:8' },
nginx: { image: 'nginx:1.27' },
'php-fpm': { image: 'php:8.3-fpm' },
};
const MATRIX: Matrix = {
include: [{ php: '8.3', services: { ...FULL_SERVICES } }],
};
const noDefaults: JobDefaults = { services: [] };
const smokeDefaults: JobDefaults = { services: ['search', 'queue', 'cache', 'web'] };
const probeDefaults: JobDefaults = { services: [], probes: ['page'] };
describe('normalizeJobEntry', () => {
it('defaults enabled=true and uses the default tiers when entry is undefined', () => {
expect(normalizeJobEntry('smoke-test', undefined, smokeDefaults)).toEqual({
enabled: true,
tiers: smokeDefaults.services,
});
});
it('treats true shorthand as enabled with defaults', () => {
expect(normalizeJobEntry('smoke-test', true, smokeDefaults)).toEqual({
enabled: true,
tiers: smokeDefaults.services,
});
});
it('treats false shorthand as disabled with defaults', () => {
expect(normalizeJobEntry('smoke-test', false, smokeDefaults)).toEqual({
enabled: false,
tiers: smokeDefaults.services,
});
});
it('empty object is enabled with defaults', () => {
expect(normalizeJobEntry('smoke-test', {}, smokeDefaults)).toEqual({
enabled: true,
tiers: smokeDefaults.services,
});
});
it('preserves enabled when explicitly set', () => {
expect(normalizeJobEntry('smoke-test', { enabled: false }, smokeDefaults)).toEqual({
enabled: false,
tiers: smokeDefaults.services,
});
});
it('overrides the default tiers when services is set', () => {
expect(normalizeJobEntry('smoke-test', { services: ['cache', 'web'] }, smokeDefaults)).toEqual({
enabled: true,
tiers: ['cache', 'web'],
});
});
it('accepts an empty services array as "no services"', () => {
expect(normalizeJobEntry('smoke-test', { services: [] }, smokeDefaults)).toEqual({
enabled: true,
tiers: [],
});
});
it('throws when entry is a non-array primitive other than boolean', () => {
expect(() => normalizeJobEntry('unit-test', 'true' as never, noDefaults)).toThrowError(
/must be a boolean or an object/
);
});
it('throws when entry is an array', () => {
expect(() => normalizeJobEntry('unit-test', [] as never, noDefaults)).toThrowError(/got array/);
});
it('throws when services is not an array', () => {
expect(() => normalizeJobEntry('smoke-test', { services: 'search' } as never, smokeDefaults)).toThrowError(
/services must be an array of tier names/
);
});
it('throws when services contains an unknown tier', () => {
expect(() => normalizeJobEntry('smoke-test', { services: ['llm'] } as never, smokeDefaults)).toThrowError(
/services contains unknown tier "llm"/
);
});
it('carries the default probes when the entry omits them', () => {
expect(normalizeJobEntry('smoke-test', { services: [] }, probeDefaults)).toEqual({
enabled: true,
tiers: [],
probes: ['page'],
});
});
it('carries the default probes for the boolean shorthand', () => {
expect(normalizeJobEntry('smoke-test', true, probeDefaults)).toEqual({
enabled: true,
tiers: [],
probes: ['page'],
});
});
it('overrides the default probes when probes is set', () => {
expect(normalizeJobEntry('smoke-test', { probes: ['page', 'graphql'] }, probeDefaults)).toEqual({
enabled: true,
tiers: [],
probes: ['page', 'graphql'],
});
});
it('omits probes for a job that declares no probe defaults', () => {
expect(normalizeJobEntry('unit-test', { services: [] }, noDefaults).probes).toBeUndefined();
});
});
describe('normalizeProbes', () => {
it('returns the defaults when probes is omitted', () => {
expect(normalizeProbes('smoke-test', undefined, ['page'])).toEqual(['page']);
});
it('returns undefined for a job with no probe defaults when omitted', () => {
expect(normalizeProbes('unit-test', undefined, undefined)).toBeUndefined();
});
it('throws when probes is set on a job that does not support it', () => {
expect(() => normalizeProbes('unit-test', ['page'], undefined)).toThrowError(
/job "unit-test" does not support "probes"/
);
});
it('throws when probes is not an array', () => {
expect(() => normalizeProbes('smoke-test', 'page', ['page'])).toThrowError(
/probes must be an array of probe names/
);
});
it('throws when probes contains an unknown probe', () => {
expect(() => normalizeProbes('smoke-test', ['rest'], ['page'])).toThrowError(
/probes contains unknown probe "rest"/
);
});
it('accepts an empty probes array', () => {
expect(normalizeProbes('smoke-test', [], ['page'])).toEqual([]);
});
});
describe('mergeRequiredTiers', () => {
it('returns the input list when required is undefined', () => {
expect(mergeRequiredTiers(['cache'], undefined)).toEqual(['cache']);
});
it('returns the input list when required is empty', () => {
expect(mergeRequiredTiers(['cache'], [])).toEqual(['cache']);
});
it('prepends required tiers ahead of the input tiers', () => {
expect(mergeRequiredTiers(['cache', 'web'], ['db'])).toEqual(['db', 'cache', 'web']);
});
it('deduplicates when a required tier already appears in the input', () => {
expect(mergeRequiredTiers(['db', 'cache'], ['db'])).toEqual(['db', 'cache']);
});
it('deduplicates within required itself', () => {
expect(mergeRequiredTiers(['cache'], ['db', 'db'])).toEqual(['db', 'cache']);
});
});
describe('filterEntryServices', () => {
it('returns services={} for an empty tier list', () => {
const out = filterEntryServices({ php: '8.3', services: FULL_SERVICES }, []);
expect(out.services).toEqual({});
expect(out.php).toBe('8.3');
});
it('keeps only services in the requested tiers', () => {
const out = filterEntryServices({ php: '8.3', services: FULL_SERVICES }, ['cache', 'web']);
expect(Object.keys(out.services!).sort()).toEqual(['nginx', 'php-fpm', 'valkey']);
});
it('drops services that the matrix doesn\'t carry (elasticsearch absent)', () => {
const out = filterEntryServices({ services: { opensearch: FULL_SERVICES.opensearch } }, ['search']);
expect(Object.keys(out.services!)).toEqual(['opensearch']);
});
it('emits services={} when the entry has no services map', () => {
const out = filterEntryServices({ php: '8.3' }, ['cache']);
expect(out.services).toEqual({});
});
});
describe('filterMatrixForJob', () => {
it('preserves matrix shape, mapping every entry through filterEntryServices', () => {
const out = filterMatrixForJob(MATRIX, ['queue']);
expect(out.include).toHaveLength(1);
expect(Object.keys(out.include[0].services!)).toEqual(['rabbitmq']);
});
it('passes through unrelated top-level matrix keys', () => {
const out = filterMatrixForJob({ ...MATRIX, magento: ['2.4.7'] } as Matrix, []);
expect((out as Matrix).magento).toEqual(['2.4.7']);
});
});
describe('resolveJobs', () => {
const jobs: Record<string, JobDefaults> = {
'unit-test': noDefaults,
'smoke-test': smokeDefaults,
};
it('emits every known job, defaulted-enabled, when raw is empty', () => {
const out = resolveJobs({}, 'store', jobs, MATRIX);
expect(Object.keys(out).sort()).toEqual(['smoke-test', 'unit-test']);
expect(out['unit-test'].enabled).toBe(true);
expect(out['smoke-test'].enabled).toBe(true);
});
it('emits services={} on entries for a no-default job', () => {
const out = resolveJobs({}, 'store', jobs, MATRIX);
expect(out['unit-test'].matrix.include[0].services).toEqual({});
});
it('expands the smoke-test default tiers across the matrix entry', () => {
const out = resolveJobs({}, 'store', jobs, MATRIX);
expect(Object.keys(out['smoke-test'].matrix.include[0].services!).sort()).toEqual([
'nginx',
'opensearch',
'php-fpm',
'rabbitmq',
'valkey',
]);
});
it('applies a caller-supplied services override', () => {
const out = resolveJobs(
{ jobs: { 'smoke-test': { services: ['cache'] } } },
'store',
jobs,
MATRIX,
);
expect(Object.keys(out['smoke-test'].matrix.include[0].services!)).toEqual(['valkey']);
});
it('always merges requiredServices into the matrix even when caller overrides services', () => {
const withRequired: Record<string, JobDefaults> = {
'smoke-test': { ...smokeDefaults, requiredServices: ['db'] },
};
const out = resolveJobs(
{ jobs: { 'smoke-test': { services: ['cache'] } } },
'store',
withRequired,
MATRIX,
);
expect(Object.keys(out['smoke-test'].matrix.include[0].services!).sort()).toEqual(['mysql', 'valkey']);
});
it('keeps requiredServices even when caller overrides services to []', () => {
const withRequired: Record<string, JobDefaults> = {
'smoke-test': { ...smokeDefaults, requiredServices: ['db'] },
};
const out = resolveJobs(
{ jobs: { 'smoke-test': { services: [] } } },
'store',
withRequired,
MATRIX,
);
expect(Object.keys(out['smoke-test'].matrix.include[0].services!)).toEqual(['mysql']);
});
it('honors caller enabled=false but still emits a filtered matrix', () => {
const out = resolveJobs(
{ jobs: { 'smoke-test': false } },
'store',
jobs,
MATRIX,
);
expect(out['smoke-test'].enabled).toBe(false);
expect(out['smoke-test'].matrix.include[0].services).toBeDefined();
});
it('throws on unknown job names with the kind in the message', () => {
expect(() => resolveJobs({ jobs: { taco: false } }, 'store', jobs, MATRIX)).toThrowError(
/unknown job "taco" for kind "store"/
);
});
it('throws when `jobs` is not an object', () => {
expect(() => resolveJobs({ jobs: 'oops' } as never, 'store', jobs, MATRIX)).toThrowError(
/`jobs` must be an object/
);
});
});
describe('parseRawConfig', () => {
it('returns an empty object for empty input', () => {
expect(parseRawConfig('')).toEqual({});
});
it('returns an empty object for whitespace input', () => {
expect(parseRawConfig(' \n ')).toEqual({});
});
it('parses a valid object', () => {
expect(parseRawConfig('{"jobs": {"unit-test": true}}')).toEqual({
jobs: { 'unit-test': true },
});
});
it('throws on syntactically invalid JSON', () => {
expect(() => parseRawConfig('{not json}')).toThrowError(/failed to parse JSON/);
});
it('throws when top level is an array', () => {
expect(() => parseRawConfig('[]')).toThrowError(/top-level value must be an object/);
});
it('throws when top level is a primitive', () => {
expect(() => parseRawConfig('"hello"')).toThrowError(/top-level value must be an object/);
});
it('throws when top level is null', () => {
expect(() => parseRawConfig('null')).toThrowError(/top-level value must be an object/);
});
});
describe('parseMatrixInput', () => {
it('parses a valid matrix', () => {
const out = parseMatrixInput('{"include": [{"php": "8.3"}]}');
expect(out.include).toEqual([{ php: '8.3' }]);
});
it('throws on empty input', () => {
expect(() => parseMatrixInput('')).toThrowError(/`matrix` input is required/);
});
it('throws on invalid JSON', () => {
expect(() => parseMatrixInput('{nope}')).toThrowError(/failed to parse `matrix` input/);
});
it('throws when top level is an array', () => {
expect(() => parseMatrixInput('[]')).toThrowError(/`matrix` must be a JSON object/);
});
it('throws when include is missing', () => {
expect(() => parseMatrixInput('{}')).toThrowError(/`matrix.include` must be an array/);
});
it('throws when include is not an array', () => {
expect(() => parseMatrixInput('{"include": "nope"}')).toThrowError(/`matrix.include` must be an array/);
});
});
+211
View File
@@ -0,0 +1,211 @@
import { JobDefaults, Kind, Matrix, MatrixEntry, RawConfig, RawJobConfig, ResolvedConfig, ResolvedJobConfig, Services } from './types';
import { isTier, servicesForTiers, Tier } from './tier-map';
import { isProbe, Probe } from './probe';
/**
* Normalizes the `probes` value from a job entry. Returns the
* caller's list when present (validated), the job's default probe
* list when omitted, or `undefined` for jobs that have no probe
* concept. Throws if a job without probe defaults is given `probes`.
*/
export const normalizeProbes = (
jobName: string,
raw: unknown,
defaults: readonly Probe[] | undefined,
): readonly Probe[] | undefined => {
if (raw === undefined) {
return defaults;
}
if (defaults === undefined) {
throw new Error(`check-config: job "${jobName}" does not support "probes"`);
}
if (!Array.isArray(raw)) {
throw new Error(`check-config: job "${jobName}".probes must be an array of probe names`);
}
const probes: Probe[] = [];
for (const value of raw) {
if (!isProbe(value)) {
throw new Error(`check-config: job "${jobName}".probes contains unknown probe "${String(value)}"`);
}
probes.push(value);
}
return probes;
}
/**
* Normalizes a single raw job entry to (enabled, tiers, probes).
* Accepts the boolean shorthand and the object form. Validates the
* shape, the `services` tier list, and the `probes` list; throws on
* unexpected input. The caller supplies the per-job defaults, used
* when `services`/`probes` are omitted from the entry. `probes` is
* `undefined` for jobs that declare no probe defaults.
*/
export const normalizeJobEntry = (
jobName: string,
raw: RawJobConfig | undefined,
defaults: JobDefaults,
): { enabled: boolean; tiers: readonly Tier[]; probes?: readonly Probe[] } => {
if (raw === undefined) {
return { enabled: true, tiers: defaults.services, probes: defaults.probes };
}
if (typeof raw === 'boolean') {
return { enabled: raw, tiers: defaults.services, probes: defaults.probes };
}
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
throw new Error(
`check-config: job "${jobName}" must be a boolean or an object (got ${Array.isArray(raw) ? 'array' : typeof raw})`
);
}
const { enabled, services, probes } = raw as { enabled?: unknown; services?: unknown; probes?: unknown };
const enabledValue = enabled === undefined ? true : Boolean(enabled);
const resolvedProbes = normalizeProbes(jobName, probes, defaults.probes);
if (services === undefined) {
return { enabled: enabledValue, tiers: defaults.services, probes: resolvedProbes };
}
if (!Array.isArray(services)) {
throw new Error(`check-config: job "${jobName}".services must be an array of tier names`);
}
const tiers: Tier[] = [];
for (const value of services) {
if (!isTier(value)) {
throw new Error(`check-config: job "${jobName}".services contains unknown tier "${String(value)}"`);
}
tiers.push(value);
}
return { enabled: enabledValue, tiers, probes: resolvedProbes };
}
/**
* Merges a job's `requiredServices` into the resolved tier list,
* deduplicating while preserving order (required tiers first, then
* the caller/default tiers in their original order).
*/
export const mergeRequiredTiers = (
tiers: readonly Tier[],
required: readonly Tier[] | undefined,
): readonly Tier[] => {
if (!required || required.length === 0) return tiers;
const seen = new Set<Tier>();
const merged: Tier[] = [];
for (const tier of required) {
if (!seen.has(tier)) { seen.add(tier); merged.push(tier); }
}
for (const tier of tiers) {
if (!seen.has(tier)) { seen.add(tier); merged.push(tier); }
}
return merged;
}
/**
* Returns a copy of `entry` with `services` filtered to the concrete
* names produced by expanding `tiers` through the tier-map. An empty
* tier list yields `services: {}`.
*/
export const filterEntryServices = (entry: MatrixEntry, tiers: readonly Tier[]): MatrixEntry => {
const keep = servicesForTiers(tiers);
const original = entry.services ?? {};
const filtered: Services = {};
for (const [name, config] of Object.entries(original)) {
if (keep.has(name)) filtered[name] = config;
}
return { ...entry, services: filtered };
}
/**
* Per-job filter applied to the supported-version matrix: returns a
* shallow clone with every entry's `services` narrowed to the tiers
* the job needs.
*/
export const filterMatrixForJob = (matrix: Matrix, tiers: readonly Tier[]): Matrix => ({
...matrix,
include: matrix.include.map(entry => filterEntryServices(entry, tiers)),
});
/**
* Shared per-kind resolver: walks the per-kind job map and emits one
* `ResolvedJobConfig` per known job. Caller-supplied jobs override
* the defaults; jobs the caller omitted still appear, carrying the
* default `enabled: true` and the default tier list. Rejects unknown
* job names from the config so typos surface in CI.
*/
export const resolveJobs = (
raw: RawConfig,
kind: Kind,
jobs: Record<string, JobDefaults>,
matrix: Matrix,
): ResolvedConfig => {
const rawJobs = raw.jobs ?? {};
if (rawJobs === null || typeof rawJobs !== 'object' || Array.isArray(rawJobs)) {
throw new Error(`check-config: \`jobs\` must be an object`);
}
for (const name of Object.keys(rawJobs)) {
if (!(name in jobs)) {
throw new Error(
`check-config: unknown job "${name}" for kind "${kind}". Known jobs: ${Object.keys(jobs).join(', ')}`
);
}
}
const resolved: ResolvedConfig = {};
for (const [name, defaults] of Object.entries(jobs)) {
const entry = (rawJobs as Record<string, RawJobConfig>)[name];
const { enabled, tiers, probes } = normalizeJobEntry(name, entry, defaults);
const finalTiers = mergeRequiredTiers(tiers, defaults.requiredServices);
const resolvedEntry: ResolvedJobConfig = {
enabled,
matrix: filterMatrixForJob(matrix, finalTiers),
};
if (probes !== undefined) {
resolvedEntry.probes = [...probes];
}
resolved[name] = resolvedEntry;
}
return resolved;
}
/**
* Parses a JSON string into a RawConfig with shape validation
* (must be an object, not an array or primitive). Empty/whitespace
* input yields an empty config.
*/
export const parseRawConfig = (jsonText: string): RawConfig => {
const trimmed = jsonText.trim();
if (trimmed === '') return {};
let parsed: unknown;
try {
parsed = JSON.parse(trimmed);
} catch (e) {
throw new Error(`check-config: failed to parse JSON: ${(e as Error).message}`);
}
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
throw new Error(`check-config: top-level value must be an object`);
}
return parsed as RawConfig;
}
/**
* Parses the `matrix` action input. Validates the top-level shape
* (must be an object with an `include` array) so a malformed input
* fails with a clear message at the boundary.
*/
export const parseMatrixInput = (jsonText: string): Matrix => {
const trimmed = jsonText.trim();
if (trimmed === '') {
throw new Error('check-config: `matrix` input is required');
}
let parsed: unknown;
try {
parsed = JSON.parse(trimmed);
} catch (e) {
throw new Error(`check-config: failed to parse \`matrix\` input as JSON: ${(e as Error).message}`);
}
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
throw new Error('check-config: `matrix` must be a JSON object');
}
const include = (parsed as Record<string, unknown>).include;
if (!Array.isArray(include)) {
throw new Error('check-config: `matrix.include` must be an array');
}
return parsed as Matrix;
}
+13
View File
@@ -0,0 +1,13 @@
/**
* A smoke-test probe the check-store workflow can run against a
* running store. `page` does a GET / and asserts a non-empty title;
* `graphql` POSTs a storeConfig query to /graphql. Probes are opt-in
* per job because not every edition exposes every surface (e.g. the
* mage-os minimal edition ships no GraphQL modules, so /graphql 404s).
*/
export const PROBES = ['page', 'graphql'] as const;
export type Probe = (typeof PROBES)[number];
export const isProbe = (value: unknown): value is Probe =>
typeof value === 'string' && (PROBES as readonly string[]).includes(value);
+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;
}
+108
View File
@@ -0,0 +1,108 @@
import { Tier } from './tier-map';
import { Probe } from './probe';
/**
* Which reusable workflow a config belongs to. Selects the known-job
* list used for validation and the default config path.
*/
export type Kind = 'store' | 'extension';
/**
* A single service container definition from supported-version's
* matrix output. We don't model the inner shape here — we just
* preserve unknown keys when filtering.
*/
export interface ServiceConfig {
image: string;
env?: Record<string, string>;
ports?: string[];
options?: string;
volumes?: string[];
}
export interface Services {
[serviceName: string]: ServiceConfig;
}
/**
* One row of supported-version's matrix. Carries the PHP/Composer/etc
* coordinates plus the concrete `services` map this job should bring
* up. We type the known fields supported-version emits and allow
* extras to pass through untouched.
*/
export interface MatrixEntry {
services?: Services;
[key: string]: unknown;
}
/**
* The matrix shape emitted by supported-version, suitable for
* `strategy.matrix` in GitHub Actions.
*/
export interface Matrix {
include: MatrixEntry[];
[key: string]: unknown;
}
/**
* Per-job static defaults declared by each kind module.
*
* `services` is the tier list used when the caller's config does not
* override it — these tiers are user-toggleable through the schema.
*
* `requiredServices` is always merged in on top of the resolved list,
* regardless of caller overrides. Use it for tiers a job structurally
* cannot run without (e.g. mysql for a running store smoke-test) and
* which therefore should not appear in the user-facing schema enum.
*
* `probes` is the default smoke-test probe list used when the caller
* does not override it. Only jobs that declare it support the
* `probes` config key; omit it for jobs that have no probe concept.
*/
export interface JobDefaults {
services: readonly Tier[];
requiredServices?: readonly Tier[];
probes?: readonly Probe[];
}
/**
* Resolved per-job output. `enabled` mirrors the input boolean (or
* `enabled` key); `matrix` is supported-version's matrix with each
* entry's `services` filtered to the tiers this job needs.
*/
export interface ResolvedJobConfig {
enabled: boolean;
matrix: Matrix;
probes?: Probe[];
[key: string]: unknown;
}
/**
* Map of job-name -> resolved config. Keys are exactly the job names
* declared by the kind module (omitted-by-caller jobs still appear,
* carrying defaults so the consumer's `if:` guard works uniformly).
*/
export interface ResolvedConfig {
[jobName: string]: ResolvedJobConfig;
}
/**
* Shape of a single per-job entry in the user's JSON config file.
* - `true` / `false`: shorthand for `{ enabled: true|false }`
* - object: explicit enabled flag plus an optional tier list under
* `services` and an optional probe list under `probes` (both
* validated against the per-kind schema).
*/
export type RawJobConfig =
| boolean
| { enabled?: boolean; services?: string[]; probes?: string[]; [key: string]: unknown };
/**
* Top-level shape of the user's JSON config file. Job toggles live
* under `jobs`; the rest of the top level is reserved for future
* global keys.
*/
export interface RawConfig {
jobs?: { [jobName: string]: RawJobConfig };
[key: string]: unknown;
}
+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: steps:
- uses: actions/checkout@v6 - 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.5.0 # x-release-please-version
with: with:
license: ${{ secrets.SANSEC_LICENSE_KEY }} license: ${{ secrets.SANSEC_LICENSE_KEY }}
``` ```
+1 -1
View File
@@ -31,7 +31,7 @@ jobs:
name: A job to semantically compare two versions name: A job to semantically compare two versions
steps: steps:
- uses: actions/checkout@v6 - 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.5.0 # x-release-please-version
with: with:
version: 2.1.0 version: 2.1.0
compare_against: 2.2.3 compare_against: 2.2.3
+4 -4
View File
@@ -28,7 +28,7 @@ jobs:
matrix: ${{ steps.supported-version.outputs.matrix }} matrix: ${{ steps.supported-version.outputs.matrix }}
steps: steps:
- uses: actions/checkout@v6 - 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.5.0 # x-release-please-version
id: supported-version id: supported-version
compile: compile:
@@ -40,19 +40,19 @@ jobs:
steps: steps:
- uses: actions/checkout@v6 - 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.5.0 # x-release-please-version
id: setup-magento id: setup-magento
with: with:
php-version: ${{ matrix.php }} php-version: ${{ matrix.php }}
tools: composer:v${{ matrix.composer }} 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.5.0 # x-release-please-version
- run: composer install - run: composer install
env: env:
COMPOSER_AUTH: ${{ secrets.COMPOSER_AUTH }} 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.5.0 # x-release-please-version
with: with:
path: ${{ steps.setup-magento.outputs.path }} path: ${{ steps.setup-magento.outputs.path }}
``` ```
+3 -3
View File
@@ -36,7 +36,7 @@ jobs:
matrix: ${{ steps.supported-version.outputs.matrix }} matrix: ${{ steps.supported-version.outputs.matrix }}
steps: steps:
- uses: actions/checkout@v6 - 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.5.0 # x-release-please-version
id: supported-version id: supported-version
with: with:
include_services: "true" include_services: "true"
@@ -51,7 +51,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v6 - 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.5.0 # x-release-please-version
id: setup-magento id: setup-magento
with: with:
php-version: ${{ matrix.php }} php-version: ${{ matrix.php }}
@@ -64,7 +64,7 @@ jobs:
env: env:
COMPOSER_AUTH: ${{ secrets.COMPOSER_AUTH }} 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.5.0 # x-release-please-version
with: with:
services: ${{ toJSON(matrix.services) }} services: ${{ toJSON(matrix.services) }}
path: ${{ steps.setup-magento.outputs.path }} path: ${{ steps.setup-magento.outputs.path }}
+5
View File
@@ -18,6 +18,11 @@ inputs:
default: "" default: ""
description: "Additional raw flags to append to the setup:install command." 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: outputs:
command: command:
description: "The full bin/magento setup:install command that was run." 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
+101
View File
@@ -8,6 +8,7 @@ const BASE_ARGS = [
'--admin-firstname=Admin', '--admin-firstname=Admin',
'--admin-lastname=User', '--admin-lastname=User',
'--backend-frontname=admin', '--backend-frontname=admin',
'--no-interaction',
]; ];
const MYSQL_SERVICE = { const MYSQL_SERVICE = {
@@ -60,6 +61,10 @@ describe('buildInstallArgs', () => {
it('returns only base args when services is empty', () => { it('returns only base args when services is empty', () => {
expect(buildInstallArgs({})).toEqual(BASE_ARGS); expect(buildInstallArgs({})).toEqual(BASE_ARGS);
}); });
it('runs non-interactively', () => {
expect(buildInstallArgs(null)).toContain('--no-interaction');
});
}); });
describe('mysql', () => { describe('mysql', () => {
@@ -173,4 +178,100 @@ describe('buildInstallArgs', () => {
]); ]);
}); });
}); });
describe('container mode', () => {
it('uses the mysql network alias and the internal port from ports[0]', () => {
const services: Services = {
mysql: { ...MYSQL_SERVICE, ports: ['33060:3306'] },
};
expect(buildInstallArgs(services, true)).toEqual([
...BASE_ARGS,
'--db-host=mysql:3306',
'--db-name=magento_integration_tests',
'--db-user=user',
'--db-password=password',
]);
});
it('uses the opensearch alias and parses the internal port', () => {
const services: Services = {
opensearch: { image: 'opensearchproject/opensearch:2.19.1', ports: ['19200:9200'] },
};
expect(buildInstallArgs(services, true)).toEqual([
...BASE_ARGS,
'--search-engine=opensearch',
'--opensearch-host=opensearch',
'--opensearch-port=9200',
]);
});
it('uses the elasticsearch alias and parses the internal port', () => {
const services: Services = {
elasticsearch: { image: 'elasticsearch:8.11.4', ports: ['19200:9200'] },
};
expect(buildInstallArgs(services, true)).toEqual([
...BASE_ARGS,
'--search-engine=elasticsearch8',
'--elasticsearch-host=elasticsearch',
'--elasticsearch-port=9200',
]);
});
it('uses the rabbitmq alias and parses the internal port', () => {
const services: Services = {
rabbitmq: { image: 'rabbitmq:4.0-management', ports: ['15672:5672'] },
};
expect(buildInstallArgs(services, true)).toEqual([
...BASE_ARGS,
'--amqp-host=rabbitmq',
'--amqp-port=5672',
'--amqp-user=guest',
'--amqp-password=guest',
]);
});
it('uses the valkey alias when valkey is the cache service', () => {
const services: Services = {
valkey: { image: 'valkey:8.0', ports: ['16379:6379'] },
};
expect(buildInstallArgs(services, true)).toEqual([
...BASE_ARGS,
'--session-save=redis',
'--session-save-redis-host=valkey',
'--session-save-redis-port=6379',
'--cache-backend=redis',
'--cache-backend-redis-server=valkey',
'--cache-backend-redis-port=6379',
]);
});
it('uses the redis alias when redis is the cache service', () => {
const services: Services = {
redis: { image: 'redis:7.2', ports: ['16379:6379'] },
};
expect(buildInstallArgs(services, true)).toEqual([
...BASE_ARGS,
'--session-save=redis',
'--session-save-redis-host=redis',
'--session-save-redis-port=6379',
'--cache-backend=redis',
'--cache-backend-redis-server=redis',
'--cache-backend-redis-port=6379',
]);
});
it('falls back to default internal ports when ports are absent', () => {
const services: Services = {
mysql: { ...MYSQL_SERVICE, ports: undefined },
opensearch: { image: 'opensearchproject/opensearch:2.19.1' },
rabbitmq: { image: 'rabbitmq:4.0-management' },
valkey: { image: 'valkey:8.0' },
};
const args = buildInstallArgs(services, true);
expect(args).toContain('--db-host=mysql:3306');
expect(args).toContain('--opensearch-port=9200');
expect(args).toContain('--amqp-port=5672');
expect(args).toContain('--session-save-redis-port=6379');
});
});
}); });
+33 -15
View File
@@ -3,6 +3,7 @@ export interface ServiceConfig {
env?: Record<string, string>; env?: Record<string, string>;
ports?: string[]; ports?: string[];
options?: string; options?: string;
volumes?: string[];
} }
export interface Services { export interface Services {
@@ -12,6 +13,7 @@ export interface Services {
rabbitmq?: ServiceConfig; rabbitmq?: ServiceConfig;
redis?: ServiceConfig; redis?: ServiceConfig;
valkey?: ServiceConfig; valkey?: ServiceConfig;
'php-fpm'?: ServiceConfig;
} }
const BASE_ARGS = [ const BASE_ARGS = [
@@ -22,23 +24,32 @@ const BASE_ARGS = [
'--admin-firstname=Admin', '--admin-firstname=Admin',
'--admin-lastname=User', '--admin-lastname=User',
'--backend-frontname=admin', '--backend-frontname=admin',
'--no-interaction',
]; ];
const parsePort = (svc: ServiceConfig | undefined, index: 0 | 1, fallback: string): string => {
return svc?.ports?.[0]?.split(':')[index] ?? fallback;
};
export const buildMysqlPrepArgs = (mysql: ServiceConfig): string[] => { export const buildMysqlPrepArgs = (mysql: ServiceConfig): string[] => {
const rootPassword = mysql.env?.MYSQL_ROOT_PASSWORD ?? 'rootpassword'; 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;']; 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]; const args = [...BASE_ARGS];
if (!services) return args; if (!services) return args;
const portIdx: 0 | 1 = containerMode ? 1 : 0;
const hostFor = (alias: string): string => containerMode ? alias : 'localhost';
if (services.mysql) { 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( args.push(
`--db-host=127.0.0.1:${dbPort}`, `--db-host=${dbHost}`,
`--db-name=${services.mysql.env?.MYSQL_DATABASE ?? 'magento'}`, `--db-name=${services.mysql.env?.MYSQL_DATABASE ?? 'magento'}`,
`--db-user=${services.mysql.env?.MYSQL_USER ?? 'magento'}`, `--db-user=${services.mysql.env?.MYSQL_USER ?? 'magento'}`,
`--db-password=${services.mysql.env?.MYSQL_PASSWORD ?? 'magento'}`, `--db-password=${services.mysql.env?.MYSQL_PASSWORD ?? 'magento'}`,
@@ -46,39 +57,46 @@ export const buildInstallArgs = (services: Services | null): string[] => {
} }
if (services.opensearch) { if (services.opensearch) {
const port = parsePort(services.opensearch, portIdx, '9200');
args.push( args.push(
'--search-engine=opensearch', '--search-engine=opensearch',
'--opensearch-host=localhost', `--opensearch-host=${hostFor('opensearch')}`,
'--opensearch-port=9200', `--opensearch-port=${port}`,
); );
} else if (services.elasticsearch) { } else if (services.elasticsearch) {
const majorVersion = services.elasticsearch.image.split(':')[1]?.split('.')[0]; const majorVersion = services.elasticsearch.image.split(':')[1]?.split('.')[0];
const port = parsePort(services.elasticsearch, portIdx, '9200');
args.push( args.push(
`--search-engine=elasticsearch${majorVersion}`, `--search-engine=elasticsearch${majorVersion}`,
'--elasticsearch-host=localhost', `--elasticsearch-host=${hostFor('elasticsearch')}`,
'--elasticsearch-port=9200', `--elasticsearch-port=${port}`,
); );
} }
if (services.rabbitmq) { if (services.rabbitmq) {
const port = parsePort(services.rabbitmq, portIdx, '5672');
args.push( args.push(
'--amqp-host=localhost', `--amqp-host=${hostFor('rabbitmq')}`,
'--amqp-port=5672', `--amqp-port=${port}`,
'--amqp-user=guest', '--amqp-user=guest',
'--amqp-password=guest', '--amqp-password=guest',
); );
} }
if (services.valkey || services.redis) { 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( args.push(
'--session-save=redis', '--session-save=redis',
'--session-save-redis-host=localhost', `--session-save-redis-host=${host}`,
'--session-save-redis-port=6379', `--session-save-redis-port=${port}`,
'--cache-backend=redis', '--cache-backend=redis',
'--cache-backend-redis-server=localhost', `--cache-backend-redis-server=${host}`,
'--cache-backend-redis-port=6379', `--cache-backend-redis-port=${port}`,
); );
} }
return args; return args;
}; };
+40 -5
View File
@@ -1,12 +1,25 @@
import * as core from '@actions/core'; import * as core from '@actions/core';
import * as exec from '@actions/exec'; import * as exec from '@actions/exec';
import * as nodePath from 'path';
import { buildInstallArgs, buildMysqlPrepArgs, Services } from './build-command'; 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> { export async function run(): Promise<void> {
try { try {
const servicesInput = core.getInput('services'); const servicesInput = core.getInput('services');
const path = core.getInput('path') || '.'; const path = core.getInput('path') || '.';
const extraArgs = core.getInput('extra_args').trim(); const extraArgs = core.getInput('extra_args').trim();
const containerId = core.getInput('container_id').trim();
const containerMode = containerId !== '';
let services: Services | null = null; let services: Services | null = null;
if (servicesInput && servicesInput !== '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 // 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) { if (services?.mysql) {
await exec.exec('mysql', buildMysqlPrepArgs(services.mysql)); await exec.exec('mysql', buildMysqlPrepArgs(services.mysql));
} }
const args = buildInstallArgs(services); const args = buildInstallArgs(services, containerMode);
if (extraArgs) { if (extraArgs) {
args.push(...extraArgs.split(/\s+/)); 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) { } catch (error) {
core.setFailed((error as Error).message); core.setFailed((error as Error).message);
} }
} }
run(); run();
+2 -2
View File
@@ -51,7 +51,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v6 - 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.5.0 # x-release-please-version
id: setup-magento id: setup-magento
with: with:
php-version: "8.3" php-version: "8.3"
@@ -89,7 +89,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v6 - 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.5.0 # x-release-please-version
id: setup-magento id: setup-magento
with: with:
php-version: "8.3" php-version: "8.3"
+1 -1
View File
@@ -111,7 +111,7 @@ runs:
fi fi
printf '%s\n' "$line" >> .gitattributes 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@main
name: Fix Magento Out of Box Install Issues name: Fix Magento Out of Box Install Issues
with: with:
magento_directory: ${{ steps.setup-magento-compute-directory.outputs.MAGENTO_DIRECTORY }} 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 | | Input | Description | Required | Default |
|-----------------| ------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------- |-----------------------| |-----------------| ------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------- |-----------------------|
| kind | The "kind" of support you're targeting for your package. See [Kinds](#kinds). | false | 'currently-supported' | | 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 | '' | | 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' | | 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' | | 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 ## Kinds
- `currently-supported` - The currently supported Magento Open Source versions by Adobe. - `currently-supported` - The currently supported Magento Open Source versions by Adobe.
@@ -30,8 +31,50 @@ See the [action.yml](./action.yml)
## Projects ## Projects
- `mage-os` - `mage-os`
- `mage-os-minimal`
- `magento-open-source` (default) - `magento-open-source` (default)
## Service preferences
When `include_services: true` (the default), each matrix entry is enriched with a `services` map. Some tiers of services (for example, search) have more than one valid implementation across the supported Magento versions:
- **search**: `opensearch` or `elasticsearch`
- **cache**: `valkey` or `redis`
By default the action picks `opensearch` over `elasticsearch` and `valkey` over `redis` wherever both are available for the matrix entry's Magento version. `service_preferences` lets the caller override that default pick by naming the implementation they want.
Tiers without a preference fall back to the per-version default pick. Your preferences are **selective**, not **exclusive**.
### Format
A comma-separated list of service implementation names. Whitespace around names is tolerated.
```yml
with:
service_preferences: elasticsearch,valkey
```
### Accepted names
| Name | Tier |
|------------------|--------|
| `mysql` | db |
| `elasticsearch` | search |
| `opensearch` | search |
| `rabbitmq` | queue |
| `redis` | cache |
| `valkey` | cache |
### Example
```yml
- uses: graycoreio/github-actions-magento2/supported-version@v8.5.0 # x-release-please-version
id: supported-version
with:
kind: currently-supported
service_preferences: opensearch,valkey
```
## Usage ## Usage
```yml ```yml
@@ -51,7 +94,7 @@ jobs:
outputs: outputs:
matrix: ${{ steps.supported-version.outputs.matrix }} matrix: ${{ steps.supported-version.outputs.matrix }}
steps: steps:
- uses: graycoreio/github-actions-magento2/supported-version@v7.0.0 # x-release-please-version - uses: graycoreio/github-actions-magento2/supported-version@v8.5.0 # x-release-please-version
id: supported-version id: supported-version
- run: echo ${{ steps.supported-version.outputs.matrix }} - run: echo ${{ steps.supported-version.outputs.matrix }}
``` ```
+6 -1
View File
@@ -9,7 +9,7 @@ inputs:
default: "currently-supported" default: "currently-supported"
project: project:
required: false 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 # The default value is what it is to keep backward compatibility
default: "magento-open-source" default: "magento-open-source"
custom_versions: custom_versions:
@@ -27,6 +27,11 @@ inputs:
default: "true" default: "true"
description: "Whether to include a `services` key in each matrix entry with GitHub Actions service configurations." 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: outputs:
matrix: matrix:
description: "The Github Actions matrix of software technologies required to run Magento." 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 { getMatrixForKind } from './matrix/get-matrix-for-kind';
import { validateProject } from "./project/validate-projects"; import { validateProject } from "./project/validate-projects";
import { buildServicesForEntry } from "./services/build-services"; import { buildServicesForEntry } from "./services/build-services";
import { parseServicePreferences, validatePreferencesAgainstMatrix } from "./services/preferences";
export async function run(): Promise<void> { export async function run(): Promise<void> {
@@ -12,19 +13,33 @@ export async function run(): Promise<void> {
const project = core.getInput("project"); const project = core.getInput("project");
const recent_time_frame = core.getInput("recent_time_frame"); const recent_time_frame = core.getInput("recent_time_frame");
const include_services = core.getInput("include_services") === "true"; const include_services = core.getInput("include_services") === "true";
const service_preferences_raw = core.getInput("service_preferences");
validateProject(<any>project) validateProject(<any>project)
validateKind(<any>kind, customVersions ? customVersions.split(',') : undefined); 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); let matrix = getMatrixForKind(kind, project, customVersions, recent_time_frame);
if (include_services) { if (include_services) {
if (hasPreferences) {
validatePreferencesAgainstMatrix(preferences, matrix.include);
}
const workspace = process.env.GITHUB_WORKSPACE || '';
matrix = { matrix = {
magento: matrix.magento, magento: matrix.magento,
include: matrix.include.map((entry) => ({ include: matrix.include.map((entry) => ({
...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.7-p9',
'magento/project-community-edition:2.4.8-p4', 'magento/project-community-edition:2.4.8-p4',
]], ]],
['2027-04-09T00:00:00Z', 'Day of v2.4.7 EoL', [ ['2027-05-31T00:00:00Z', 'Day of v2.4.7 EoL', [
'magento/project-community-edition:2.4.7-p9', 'magento/project-community-edition:2.4.7-p10',
'magento/project-community-edition:2.4.8-p4', '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', [ ['2027-06-01T00:00:00Z', 'Day after v2.4.7 EoL', [
'magento/project-community-edition:2.4.8-p4', 'magento/project-community-edition:2.4.8-p5',
'magento/project-community-edition:2.4.9',
]], ]],
])( ])(
'supportedVersions for %s', 'supportedVersions for %s',
@@ -143,6 +145,12 @@ describe('getCurrentlySupportedVersions for mage-os', () => {
['2026-04-15T00:00:01Z', 'Release of 2.2.2', [ ['2026-04-15T00:00:01Z', 'Release of 2.2.2', [
'mage-os/project-community-edition: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', 'supportedVersions for %s',
(date, description ,result) => { (date, description ,result) => {
@@ -1,4 +1,5 @@
{ {
"mage-os": ["mage-os/project-community-edition"], "mage-os": ["mage-os/project-community-edition"],
"mage-os-minimal": ["mage-os/project-minimal-edition"],
"magento-open-source": ["magento/project-community-edition"] "magento-open-source": ["magento/project-community-edition"]
} }
@@ -1,4 +1,5 @@
{ {
"mage-os": ["mage-os/project-community-edition:next"], "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"] "magento-open-source": ["magento/project-community-edition:next"]
} }
@@ -3,6 +3,7 @@ export interface ServiceConfig {
env?: Record<string, string>; env?: Record<string, string>;
ports?: string[]; ports?: string[];
options?: string; options?: string;
volumes?: string[];
} }
export interface Services { export interface Services {
@@ -3,6 +3,7 @@
*/ */
export const KNOWN_PROJECTS = { export const KNOWN_PROJECTS = {
"mage-os": true, "mage-os": true,
"mage-os-minimal": true,
"magento-open-source": true, "magento-open-source": true,
} }
@@ -4,6 +4,7 @@ describe('validateProject', () => {
it('returns `true` if its a valid project', () => { it('returns `true` if its a valid project', () => {
expect(validateProject("magento-open-source")).toBe(true); expect(validateProject("magento-open-source")).toBe(true);
expect(validateProject("mage-os")).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', () => { it('throws a helpful exception if it is an invalid project', () => {
@@ -4,6 +4,7 @@ import {Project} from "../projects";
describe('isKnownProject', () => { describe('isKnownProject', () => {
it('returns `true` for known projects', () => { it('returns `true` for known projects', () => {
expect(isKnownProject("mage-os")).toBe(true) expect(isKnownProject("mage-os")).toBe(true)
expect(isKnownProject("mage-os-minimal")).toBe(true)
expect(isKnownProject("magento-open-source")).toBe(true) expect(isKnownProject("magento-open-source")).toBe(true)
}); });
@@ -1,5 +1,7 @@
import { buildServicesForEntry } from './build-services'; import { buildServicesForEntry } from './build-services';
import { PackageMatrixVersion } from '../matrix/matrix-type'; 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 => ({ const createTestEntry = (overrides: Partial<PackageMatrixVersion> = {}): PackageMatrixVersion => ({
magento: 'magento/project-community-edition:2.4.7', magento: 'magento/project-community-edition:2.4.7',
@@ -223,13 +225,17 @@ describe('buildServicesForEntry', () => {
describe('complete service output', () => { describe('complete service output', () => {
it('should build all services when all are available', () => { it('should build all services when all are available', () => {
const entry = createTestEntry(); 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.mysql).toBeDefined();
expect(services.opensearch).toBeDefined(); expect(services.opensearch).toBeDefined();
expect(services.rabbitmq).toBeDefined(); expect(services.rabbitmq).toBeDefined();
expect(services.valkey).toBeDefined(); expect(services.valkey).toBeDefined();
expect(services.nginx).toBeDefined();
expect(services['php-fpm']).toBeDefined();
}); });
it('should handle entry with minimal services', () => { it('should handle entry with minimal services', () => {
@@ -239,11 +245,136 @@ describe('buildServicesForEntry', () => {
opensearch: '', opensearch: '',
rabbitmq: '', rabbitmq: '',
redis: '', redis: '',
valkey: '' valkey: '',
nginx: '',
php: ''
}); });
const services = buildServicesForEntry(entry); const services = buildServicesForEntry(entry);
expect(Object.keys(services)).toHaveLength(0); 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, opensearchConfig,
rabbitmqConfig, rabbitmqConfig,
redisConfig, redisConfig,
valkeyConfig valkeyConfig,
buildNginxConfig,
buildPhpFpmConfig
} from './service-config'; } from './service-config';
import { ServicePreferences } from './preferences';
interface SearchEngineChoice { interface SearchEngineChoice {
type: 'opensearch' | 'elasticsearch'; type: 'opensearch' | 'elasticsearch';
@@ -19,10 +22,22 @@ interface CacheChoice {
} }
/** /**
* Determines which search engine to use for a matrix entry. * Picks the search engine for a matrix entry. Honors caller's `service_preferences`
* Prefers opensearch over elasticsearch. * 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() !== '') { if (entry.opensearch && entry.opensearch.trim() !== '') {
return { type: 'opensearch', image: entry.opensearch }; return { type: 'opensearch', image: entry.opensearch };
} }
@@ -33,10 +48,22 @@ function getSearchEngineChoice(entry: PackageMatrixVersion): SearchEngineChoice
} }
/** /**
* Determines which cache to use for a matrix entry. * Picks the cache for a matrix entry. Honors caller's `service_preferences`
* Prefers valkey over redis. * 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() !== '') { if (entry.valkey && entry.valkey.trim() !== '') {
return { type: 'valkey', image: entry.valkey }; 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 = {}; const services: Services = {};
// MySQL is always included if present
if (entry.mysql && entry.mysql.trim() !== '') { if (entry.mysql && entry.mysql.trim() !== '') {
services.mysql = mysqlConfig.getConfig(entry.mysql); services.mysql = mysqlConfig.getConfig(entry.mysql);
} }
// Search engine: prefer opensearch over elasticsearch const searchEngine = getSearchEngineChoice(entry, preferences.search);
const searchEngine = getSearchEngineChoice(entry);
if (searchEngine) { if (searchEngine) {
if (searchEngine.type === 'opensearch') { if (searchEngine.type === 'opensearch') {
services.opensearch = opensearchConfig.getConfig(searchEngine.image); services.opensearch = opensearchConfig.getConfig(searchEngine.image);
@@ -67,13 +101,11 @@ export function buildServicesForEntry(entry: PackageMatrixVersion): Services {
} }
} }
// RabbitMQ
if (entry.rabbitmq && entry.rabbitmq.trim() !== '') { if (entry.rabbitmq && entry.rabbitmq.trim() !== '') {
services.rabbitmq = rabbitmqConfig.getConfig(entry.rabbitmq); services.rabbitmq = rabbitmqConfig.getConfig(entry.rabbitmq);
} }
// Cache: prefer valkey over redis const cache = getCacheChoice(entry, preferences.cache);
const cache = getCacheChoice(entry);
if (cache) { if (cache) {
if (cache.type === 'valkey') { if (cache.type === 'valkey') {
services.valkey = valkeyConfig.getConfig(cache.image); 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; 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'] 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', () => { it('returns individual versions matrix for magento-open-source', () => {
expect(Object.keys(getIndividualVersionsForProject("magento-open-source")).length).toBeGreaterThan(0) 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")).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', () => { it('throws error if no individual versions are specified for given project', () => {
expect(() => getIndividualVersionsForProject(<Project>"ahsoka")).toThrowError() expect(() => getIndividualVersionsForProject(<Project>"ahsoka")).toThrowError()
}) })
@@ -16,6 +17,7 @@ describe('getCompositeVersionsForProject', () => {
it('returns composite versions matrix for magento-open-source', () => { it('returns composite versions matrix for magento-open-source', () => {
expect(Object.keys(getCompositeVersionsForProject("magento-open-source")).length).toBeGreaterThan(0) 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")).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', () => { it('throws error if no composite versions are specified for given project', () => {
@@ -3,11 +3,13 @@ import { PackageMatrixVersion } from "../matrix/matrix-type";
const individual = { const individual = {
'mage-os': require('./mage-os/individual.json'), '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') 'magento-open-source': require('./magento-open-source/individual.json')
} }
const composite = { const composite = {
'mage-os': require('./mage-os/composite.json'), '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') '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": { "mage-os/project-community-edition": {
"magento": "mage-os/project-community-edition", "magento": "mage-os/project-community-edition",
"php": 8.4, "php": 8.4,
"composer": "2.9.7", "composer": "2.9.8",
"mysql": "mysql:8.4", "mysql": "mysql:8.4",
"opensearch": "opensearchproject/opensearch:3", "opensearch": "opensearchproject/opensearch:3",
"rabbitmq": "rabbitmq:4.1-management", "rabbitmq": "rabbitmq:4.1-management",
"valkey": "valkey/valkey:8.0", "valkey": "valkey/valkey:8",
"redis": "redis:7.2",
"varnish": "varnish:7.7", "varnish": "varnish:7.7",
"nginx": "nginx:1.28", "nginx": "nginx:1.28",
"os": "ubuntu-latest", "os": "ubuntu-latest",
"release": "2026-04-15T00:00:00+0000", "release": "2026-05-19T00:00:00+0000",
"eol": "2029-04-15T00:00:00+0000" "eol": "2029-05-19T00:00:00+0000"
}, },
"mage-os/project-community-edition:next": { "mage-os/project-community-edition:next": {
"magento": "mage-os/project-community-edition:next", "magento": "mage-os/project-community-edition:next",
"php": 8.4, "php": 8.4,
"composer": "2.9.7", "composer": "2.9.8",
"mysql": "mysql:8.4", "mysql": "mysql:8.4",
"opensearch": "opensearchproject/opensearch:3", "opensearch": "opensearchproject/opensearch:3",
"rabbitmq": "rabbitmq:4.1-management", "rabbitmq": "rabbitmq:4.1-management",
"valkey": "valkey/valkey:8.0", "valkey": "valkey/valkey:8",
"redis": "redis:7.2",
"varnish": "varnish:7.7", "varnish": "varnish:7.7",
"nginx": "nginx:1.28", "nginx": "nginx:1.28",
"os": "ubuntu-latest", "os": "ubuntu-latest",
"release": "2026-04-15T00:00:00+0000", "release": "2026-05-19T00:00:00+0000",
"eol": "2029-04-15T00:00:00+0000" "eol": "2029-05-19T00:00:00+0000"
}, },
"mage-os/project-community-edition:>=1.0 <1.1": { "mage-os/project-community-edition:>=1.0 <1.1": {
"magento": "mage-os/project-community-edition:>=1.0 <1.1", "magento": "mage-os/project-community-edition:>=1.0 <1.1",
"php": 8.3, "php": 8.3,
"composer": "2.9.5", "composer": "2.9.8",
"mysql": "mariadb:10.6", "mysql": "mariadb:10.6",
"elasticsearch": "elasticsearch:8.11.4", "elasticsearch": "elasticsearch:8.11.4",
"rabbitmq": "rabbitmq:3.13-management", "rabbitmq": "rabbitmq:3.13-management",
@@ -46,7 +44,7 @@
"mage-os/project-community-edition:>=1.1 <1.2": { "mage-os/project-community-edition:>=1.1 <1.2": {
"magento": "mage-os/project-community-edition:>=1.1 <1.2", "magento": "mage-os/project-community-edition:>=1.1 <1.2",
"php": 8.4, "php": 8.4,
"composer": "2.9.5", "composer": "2.9.8",
"mysql": "mysql:8.4", "mysql": "mysql:8.4",
"opensearch": "opensearchproject/opensearch:2.19.1", "opensearch": "opensearchproject/opensearch:2.19.1",
"rabbitmq": "rabbitmq:4.0-management", "rabbitmq": "rabbitmq:4.0-management",
@@ -60,7 +58,7 @@
"mage-os/project-community-edition:>=1.2 <1.3": { "mage-os/project-community-edition:>=1.2 <1.3": {
"magento": "mage-os/project-community-edition:>=1.2 <1.3", "magento": "mage-os/project-community-edition:>=1.2 <1.3",
"php": 8.4, "php": 8.4,
"composer": "2.9.5", "composer": "2.9.8",
"mysql": "mysql:8.4", "mysql": "mysql:8.4",
"opensearch": "opensearchproject/opensearch:2.19.1", "opensearch": "opensearchproject/opensearch:2.19.1",
"rabbitmq": "rabbitmq:4.0-management", "rabbitmq": "rabbitmq:4.0-management",
@@ -74,7 +72,7 @@
"mage-os/project-community-edition:>=1.3 <1.4": { "mage-os/project-community-edition:>=1.3 <1.4": {
"magento": "mage-os/project-community-edition:>=1.2 <1.3", "magento": "mage-os/project-community-edition:>=1.2 <1.3",
"php": 8.4, "php": 8.4,
"composer": "2.9.5", "composer": "2.9.8",
"mysql": "mysql:8.4", "mysql": "mysql:8.4",
"opensearch": "opensearchproject/opensearch:2.19.1", "opensearch": "opensearchproject/opensearch:2.19.1",
"rabbitmq": "rabbitmq:4.0-management", "rabbitmq": "rabbitmq:4.0-management",
@@ -88,7 +86,7 @@
"mage-os/project-community-edition:>=2.0 <2.1": { "mage-os/project-community-edition:>=2.0 <2.1": {
"magento": "mage-os/project-community-edition:>=2.0 <2.1", "magento": "mage-os/project-community-edition:>=2.0 <2.1",
"php": 8.4, "php": 8.4,
"composer": "2.9.5", "composer": "2.9.8",
"mysql": "mysql:8.4", "mysql": "mysql:8.4",
"opensearch": "opensearchproject/opensearch:2.19.1", "opensearch": "opensearchproject/opensearch:2.19.1",
"rabbitmq": "rabbitmq:4.0-management", "rabbitmq": "rabbitmq:4.0-management",
@@ -102,7 +100,7 @@
"mage-os/project-community-edition:>=2.1 <2.2": { "mage-os/project-community-edition:>=2.1 <2.2": {
"magento": "mage-os/project-community-edition:>=2.1 <2.2", "magento": "mage-os/project-community-edition:>=2.1 <2.2",
"php": 8.4, "php": 8.4,
"composer": "2.9.5", "composer": "2.9.8",
"mysql": "mysql:8.4", "mysql": "mysql:8.4",
"opensearch": "opensearchproject/opensearch:2.19.1", "opensearch": "opensearchproject/opensearch:2.19.1",
"rabbitmq": "rabbitmq:4.0-management", "rabbitmq": "rabbitmq:4.0-management",
@@ -116,7 +114,7 @@
"mage-os/project-community-edition:>=2.2 <2.3": { "mage-os/project-community-edition:>=2.2 <2.3": {
"magento": "mage-os/project-community-edition:>=2.2 <2.3", "magento": "mage-os/project-community-edition:>=2.2 <2.3",
"php": 8.4, "php": 8.4,
"composer": "2.9.3", "composer": "2.9.8",
"mysql": "mysql:8.4", "mysql": "mysql:8.4",
"opensearch": "opensearchproject/opensearch:3", "opensearch": "opensearchproject/opensearch:3",
"rabbitmq": "rabbitmq:4.1-management", "rabbitmq": "rabbitmq:4.1-management",
@@ -126,6 +124,34 @@
"nginx": "nginx:1.28", "nginx": "nginx:1.28",
"os": "ubuntu-latest", "os": "ubuntu-latest",
"release": "2026-03-10T00:00:00+0000", "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", "magento": "mage-os/project-community-edition:1.0.0",
"upstream": "2.4.6-p3", "upstream": "2.4.6-p3",
"php": 8.1, "php": 8.1,
"composer": "2.2.21", "composer": "2.2.28",
"mysql": "mysql:8.0", "mysql": "mysql:8.0",
"elasticsearch": "elasticsearch:8.5.3", "elasticsearch": "elasticsearch:8.5.3",
"rabbitmq": "rabbitmq:3.9-management", "rabbitmq": "rabbitmq:3.9-management",
@@ -18,7 +18,7 @@
"magento": "mage-os/project-community-edition:1.0.1", "magento": "mage-os/project-community-edition:1.0.1",
"upstream": "2.4.6-p3", "upstream": "2.4.6-p3",
"php": 8.1, "php": 8.1,
"composer": "2.2.21", "composer": "2.2.28",
"mysql": "mysql:8.0", "mysql": "mysql:8.0",
"elasticsearch": "elasticsearch:8.5.3", "elasticsearch": "elasticsearch:8.5.3",
"rabbitmq": "rabbitmq:3.9-management", "rabbitmq": "rabbitmq:3.9-management",
@@ -33,7 +33,7 @@
"magento": "mage-os/project-community-edition:1.0.2", "magento": "mage-os/project-community-edition:1.0.2",
"upstream": "2.4.7-p1", "upstream": "2.4.7-p1",
"php": 8.3, "php": 8.3,
"composer": "2.7.4", "composer": "2.9.8",
"mysql": "mariadb:10.6", "mysql": "mariadb:10.6",
"elasticsearch": "elasticsearch:8.11.4", "elasticsearch": "elasticsearch:8.11.4",
"rabbitmq": "rabbitmq:3.13-management", "rabbitmq": "rabbitmq:3.13-management",
@@ -48,7 +48,7 @@
"magento": "mage-os/project-community-edition:1.0.3", "magento": "mage-os/project-community-edition:1.0.3",
"upstream": "2.4.7-p1", "upstream": "2.4.7-p1",
"php": 8.3, "php": 8.3,
"composer": "2.7.4", "composer": "2.9.8",
"mysql": "mariadb:10.6", "mysql": "mariadb:10.6",
"elasticsearch": "elasticsearch:8.11.4", "elasticsearch": "elasticsearch:8.11.4",
"rabbitmq": "rabbitmq:3.13-management", "rabbitmq": "rabbitmq:3.13-management",
@@ -63,7 +63,7 @@
"magento": "mage-os/project-community-edition:1.0.4", "magento": "mage-os/project-community-edition:1.0.4",
"upstream": "2.4.7-p2", "upstream": "2.4.7-p2",
"php": 8.3, "php": 8.3,
"composer": "2.7.4", "composer": "2.9.8",
"mysql": "mariadb:10.6", "mysql": "mariadb:10.6",
"elasticsearch": "elasticsearch:8.11.4", "elasticsearch": "elasticsearch:8.11.4",
"rabbitmq": "rabbitmq:3.13-management", "rabbitmq": "rabbitmq:3.13-management",
@@ -78,7 +78,7 @@
"magento": "mage-os/project-community-edition:1.0.5", "magento": "mage-os/project-community-edition:1.0.5",
"upstream": "2.4.7-p3", "upstream": "2.4.7-p3",
"php": 8.3, "php": 8.3,
"composer": "2.7.4", "composer": "2.9.8",
"mysql": "mariadb:10.6", "mysql": "mariadb:10.6",
"elasticsearch": "elasticsearch:8.11.4", "elasticsearch": "elasticsearch:8.11.4",
"rabbitmq": "rabbitmq:3.13-management", "rabbitmq": "rabbitmq:3.13-management",
@@ -93,7 +93,7 @@
"magento": "mage-os/project-community-edition:1.0.6", "magento": "mage-os/project-community-edition:1.0.6",
"upstream": "2.4.7-p4", "upstream": "2.4.7-p4",
"php": 8.3, "php": 8.3,
"composer": "2.7.4", "composer": "2.9.8",
"mysql": "mariadb:10.6", "mysql": "mariadb:10.6",
"elasticsearch": "elasticsearch:8.11.4", "elasticsearch": "elasticsearch:8.11.4",
"rabbitmq": "rabbitmq:3.13-management", "rabbitmq": "rabbitmq:3.13-management",
@@ -108,7 +108,7 @@
"magento": "mage-os/project-community-edition:1.1.0", "magento": "mage-os/project-community-edition:1.1.0",
"upstream": "2.4.8", "upstream": "2.4.8",
"php": 8.4, "php": 8.4,
"composer": "2.9.5", "composer": "2.9.8",
"mysql": "mysql:8.4", "mysql": "mysql:8.4",
"opensearch": "opensearchproject/opensearch:2.19.1", "opensearch": "opensearchproject/opensearch:2.19.1",
"rabbitmq": "rabbitmq:4.0-management", "rabbitmq": "rabbitmq:4.0-management",
@@ -123,7 +123,7 @@
"magento": "mage-os/project-community-edition:1.1.1", "magento": "mage-os/project-community-edition:1.1.1",
"upstream": "2.4.8", "upstream": "2.4.8",
"php": 8.4, "php": 8.4,
"composer": "2.9.5", "composer": "2.9.8",
"mysql": "mysql:8.4", "mysql": "mysql:8.4",
"opensearch": "opensearchproject/opensearch:2.19.1", "opensearch": "opensearchproject/opensearch:2.19.1",
"rabbitmq": "rabbitmq:4.0-management", "rabbitmq": "rabbitmq:4.0-management",
@@ -138,7 +138,7 @@
"magento": "mage-os/project-community-edition:1.2.0", "magento": "mage-os/project-community-edition:1.2.0",
"upstream": "2.4.8-p1", "upstream": "2.4.8-p1",
"php": 8.4, "php": 8.4,
"composer": "2.9.5", "composer": "2.9.8",
"mysql": "mysql:8.4", "mysql": "mysql:8.4",
"opensearch": "opensearchproject/opensearch:2.19.1", "opensearch": "opensearchproject/opensearch:2.19.1",
"rabbitmq": "rabbitmq:4.0-management", "rabbitmq": "rabbitmq:4.0-management",
@@ -153,7 +153,7 @@
"magento": "mage-os/project-community-edition:1.3.0", "magento": "mage-os/project-community-edition:1.3.0",
"upstream": "2.4.8-p2", "upstream": "2.4.8-p2",
"php": 8.4, "php": 8.4,
"composer": "2.9.5", "composer": "2.9.8",
"mysql": "mysql:8.4", "mysql": "mysql:8.4",
"opensearch": "opensearchproject/opensearch:2.19.1", "opensearch": "opensearchproject/opensearch:2.19.1",
"rabbitmq": "rabbitmq:4.0-management", "rabbitmq": "rabbitmq:4.0-management",
@@ -168,7 +168,7 @@
"magento": "mage-os/project-community-edition:1.3.1", "magento": "mage-os/project-community-edition:1.3.1",
"upstream": "2.4.8-p2", "upstream": "2.4.8-p2",
"php": 8.4, "php": 8.4,
"composer": "2.9.5", "composer": "2.9.8",
"mysql": "mysql:8.4", "mysql": "mysql:8.4",
"opensearch": "opensearchproject/opensearch:2.19.1", "opensearch": "opensearchproject/opensearch:2.19.1",
"rabbitmq": "rabbitmq:4.0-management", "rabbitmq": "rabbitmq:4.0-management",
@@ -183,7 +183,7 @@
"magento": "mage-os/project-community-edition:2.0.0", "magento": "mage-os/project-community-edition:2.0.0",
"upstream": "2.4.8-p3", "upstream": "2.4.8-p3",
"php": 8.4, "php": 8.4,
"composer": "2.9.5", "composer": "2.9.8",
"mysql": "mysql:8.4", "mysql": "mysql:8.4",
"opensearch": "opensearchproject/opensearch:2.19.1", "opensearch": "opensearchproject/opensearch:2.19.1",
"rabbitmq": "rabbitmq:4.0-management", "rabbitmq": "rabbitmq:4.0-management",
@@ -198,7 +198,7 @@
"magento": "mage-os/project-community-edition:2.1.0", "magento": "mage-os/project-community-edition:2.1.0",
"upstream": "2.4.8-p3", "upstream": "2.4.8-p3",
"php": 8.4, "php": 8.4,
"composer": "2.9.5", "composer": "2.9.8",
"mysql": "mysql:8.4", "mysql": "mysql:8.4",
"opensearch": "opensearchproject/opensearch:2.19.1", "opensearch": "opensearchproject/opensearch:2.19.1",
"rabbitmq": "rabbitmq:4.0-management", "rabbitmq": "rabbitmq:4.0-management",
@@ -213,7 +213,7 @@
"magento": "mage-os/project-community-edition:2.2.0", "magento": "mage-os/project-community-edition:2.2.0",
"upstream": "2.4.8-p4", "upstream": "2.4.8-p4",
"php": 8.4, "php": 8.4,
"composer": "2.9.3", "composer": "2.9.8",
"mysql": "mysql:8.4", "mysql": "mysql:8.4",
"opensearch": "opensearchproject/opensearch:3", "opensearch": "opensearchproject/opensearch:3",
"rabbitmq": "rabbitmq:4.1-management", "rabbitmq": "rabbitmq:4.1-management",
@@ -228,7 +228,7 @@
"magento": "mage-os/project-community-edition:2.2.1", "magento": "mage-os/project-community-edition:2.2.1",
"upstream": "2.4.8-p4", "upstream": "2.4.8-p4",
"php": 8.4, "php": 8.4,
"composer": "2.9.5", "composer": "2.9.8",
"mysql": "mysql:8.4", "mysql": "mysql:8.4",
"opensearch": "opensearchproject/opensearch:3", "opensearch": "opensearchproject/opensearch:3",
"rabbitmq": "rabbitmq:4.1-management", "rabbitmq": "rabbitmq:4.1-management",
@@ -243,7 +243,7 @@
"magento": "mage-os/project-community-edition:2.2.2", "magento": "mage-os/project-community-edition:2.2.2",
"upstream": "2.4.8-p4", "upstream": "2.4.8-p4",
"php": 8.4, "php": 8.4,
"composer": "2.9.7", "composer": "2.9.8",
"mysql": "mysql:8.4", "mysql": "mysql:8.4",
"opensearch": "opensearchproject/opensearch:3", "opensearch": "opensearchproject/opensearch:3",
"rabbitmq": "rabbitmq:4.1-management", "rabbitmq": "rabbitmq:4.1-management",
@@ -253,6 +253,36 @@
"nginx": "nginx:1.28", "nginx": "nginx:1.28",
"os": "ubuntu-latest", "os": "ubuntu-latest",
"release": "2026-04-15T00:00:00+0000", "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"
} }
} }

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