mirror of
https://github.com/graycoreio/github-actions-magento2.git
synced 2026-06-08 19:46:41 +00:00
Compare commits
49 Commits
v8.0.0-rc.0
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| a097371e37 | |||
| 5ee0768610 | |||
| 3c51e99538 | |||
| 32a5fd2bad | |||
| 91bd008e62 | |||
| 36953b919c | |||
| 83f9433da0 | |||
| a7d48f567e | |||
| 76eb9064ff | |||
| 147245e120 | |||
| 4df4b25e05 | |||
| fa8e597365 | |||
| 1ea5a10ef6 | |||
| 863444afbd | |||
| befe0807f7 | |||
| 1e63c019ed | |||
| ebfdeb0b73 | |||
| 8a0f197a13 | |||
| 0bf08ef692 | |||
| 35c1ace2bc | |||
| d07afbacd0 | |||
| b71bb8b4aa | |||
| e39dd46f9c | |||
| b98313e100 | |||
| 0c7d14d885 | |||
| 6d4ca8d669 | |||
| b790da1859 | |||
| e89f6ad2e0 | |||
| 8e82fcc893 | |||
| 83ef32c838 | |||
| de7c47f07d | |||
| a2e3e7758b | |||
| e6bb7be524 | |||
| 3e9f95ee56 | |||
| 9c56da774b | |||
| 8c4fefd979 | |||
| d1a31d260d | |||
| d37f001ab6 | |||
| c5221f0d68 | |||
| 1fcb3618c0 | |||
| 4fc491bc1a | |||
| 5df6c1a0bd | |||
| ff5f76339b | |||
| 0df8ac6e57 | |||
| ef06f4566b | |||
| 4c9a28930b | |||
| 22627e1000 | |||
| b0131f0fa0 | |||
| 23c77e10c8 |
@@ -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).
|
||||
@@ -50,9 +50,12 @@ jobs:
|
||||
tools: composer:v${{ matrix.composer }}
|
||||
magento_version: ${{ matrix.magento }}
|
||||
mode: extension
|
||||
composer_auth: ${{ secrets.COMPOSER_AUTH }}
|
||||
|
||||
- run: composer update --no-install
|
||||
working-directory: ${{ steps.setup-magento.outputs.path }}
|
||||
env:
|
||||
COMPOSER_AUTH: ${{ secrets.COMPOSER_AUTH }}
|
||||
|
||||
- uses: ./cache-magento
|
||||
with:
|
||||
@@ -79,6 +82,8 @@ jobs:
|
||||
|
||||
- run: composer install
|
||||
working-directory: ${{ steps.setup-magento.outputs.path }}
|
||||
env:
|
||||
COMPOSER_AUTH: ${{ secrets.COMPOSER_AUTH }}
|
||||
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
@@ -99,3 +104,5 @@ jobs:
|
||||
path: "_ghamagento/"
|
||||
composer_cache_key: ${{ matrix.magento }}
|
||||
stamp: true
|
||||
secrets:
|
||||
composer_auth: ${{ secrets.COMPOSER_AUTH }}
|
||||
@@ -31,6 +31,8 @@ jobs:
|
||||
- run: composer create-project --repository-url="https://mirror.mage-os.org" "magento/project-community-edition:2.4.5-p1" ../magento2 --no-install
|
||||
shell: bash
|
||||
name: Create Magento ${{ matrix.magento }} Project
|
||||
env:
|
||||
COMPOSER_AUTH: ${{ secrets.COMPOSER_AUTH }}
|
||||
|
||||
- uses: ./get-magento-version
|
||||
id: magento-version
|
||||
|
||||
@@ -58,6 +58,8 @@ jobs:
|
||||
|
||||
- run: composer update --no-install
|
||||
working-directory: ${{ steps.setup-magento.outputs.path }}
|
||||
env:
|
||||
COMPOSER_AUTH: ${{ secrets.COMPOSER_AUTH }}
|
||||
|
||||
- uses: ./cache-magento
|
||||
with:
|
||||
|
||||
@@ -57,6 +57,8 @@ jobs:
|
||||
|
||||
- run: composer update --no-install
|
||||
working-directory: ${{ steps.setup-magento.outputs.path }}
|
||||
env:
|
||||
COMPOSER_AUTH: ${{ secrets.COMPOSER_AUTH }}
|
||||
|
||||
- uses: ./cache-magento
|
||||
with:
|
||||
|
||||
@@ -78,6 +78,8 @@ jobs:
|
||||
|
||||
- run: composer update --no-install
|
||||
working-directory: ${{ env.magento_folder }}
|
||||
env:
|
||||
COMPOSER_AUTH: ${{ secrets.COMPOSER_AUTH }}
|
||||
|
||||
- uses: ./cache-magento
|
||||
with:
|
||||
@@ -218,6 +220,8 @@ jobs:
|
||||
|
||||
- run: composer update --no-install
|
||||
working-directory: ${{ steps.setup-magento.outputs.path }}
|
||||
env:
|
||||
COMPOSER_AUTH: ${{ secrets.COMPOSER_AUTH }}
|
||||
|
||||
- uses: ./cache-magento
|
||||
with:
|
||||
|
||||
@@ -40,3 +40,5 @@ jobs:
|
||||
path: _test/demo-package
|
||||
matrix: ${{ needs.compute_matrix.outputs.matrix }}
|
||||
stamp: true
|
||||
secrets:
|
||||
composer_auth: ${{ secrets.COMPOSER_AUTH }}
|
||||
|
||||
@@ -42,15 +42,28 @@ on:
|
||||
description: "Your composer credentials (typically a stringified json object of the contents of your auth.json)"
|
||||
|
||||
jobs:
|
||||
compute_resolved:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
resolved: ${{ steps.resolve.outputs.resolved }}
|
||||
steps:
|
||||
- uses: graycoreio/github-actions-magento2/resolve-check-config@main
|
||||
id: resolve
|
||||
with:
|
||||
kind: extension
|
||||
matrix: ${{ inputs.matrix }}
|
||||
|
||||
unit-test-extension:
|
||||
runs-on: ${{ matrix.os }}
|
||||
needs: compute_resolved
|
||||
if: ${{ fromJSON(needs.compute_resolved.outputs.resolved)['unit-test-extension'].enabled != false }}
|
||||
strategy:
|
||||
matrix: ${{ fromJSON(inputs.matrix) }}
|
||||
matrix: ${{ fromJSON(needs.compute_resolved.outputs.resolved)['unit-test-extension'].matrix }}
|
||||
fail-fast: ${{ inputs.fail-fast }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/setup-magento@v8.0.0-rc.0
|
||||
- uses: graycoreio/github-actions-magento2/setup-magento@main
|
||||
id: setup-magento
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
@@ -71,11 +84,15 @@ jobs:
|
||||
- name: Require extension
|
||||
working-directory: ${{ steps.setup-magento.outputs.path }}
|
||||
run: composer require "${{ steps.package.outputs.name }}:@dev" --no-install
|
||||
env:
|
||||
COMPOSER_AUTH: ${{ secrets.composer_auth }}
|
||||
|
||||
- run: composer update --no-install
|
||||
working-directory: ${{ steps.setup-magento.outputs.path }}
|
||||
env:
|
||||
COMPOSER_AUTH: ${{ secrets.composer_auth }}
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/cache-magento@v8.0.0-rc.0
|
||||
- uses: graycoreio/github-actions-magento2/cache-magento@main
|
||||
with:
|
||||
composer_cache_key: ${{ inputs.composer_cache_key && format('{0} | {1}', inputs.composer_cache_key, matrix.magento) || matrix.magento }}
|
||||
working-directory: ${{ steps.setup-magento.outputs.path }}
|
||||
@@ -109,13 +126,15 @@ jobs:
|
||||
|
||||
compile-extension:
|
||||
runs-on: ${{ matrix.os }}
|
||||
needs: compute_resolved
|
||||
if: ${{ fromJSON(needs.compute_resolved.outputs.resolved)['compile-extension'].enabled != false }}
|
||||
strategy:
|
||||
matrix: ${{ fromJSON(inputs.matrix) }}
|
||||
matrix: ${{ fromJSON(needs.compute_resolved.outputs.resolved)['compile-extension'].matrix }}
|
||||
fail-fast: ${{ inputs.fail-fast }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/setup-magento@v8.0.0-rc.0
|
||||
- uses: graycoreio/github-actions-magento2/setup-magento@main
|
||||
id: setup-magento
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
@@ -141,8 +160,10 @@ jobs:
|
||||
|
||||
- run: composer update --no-install
|
||||
working-directory: ${{ steps.setup-magento.outputs.path }}
|
||||
env:
|
||||
COMPOSER_AUTH: ${{ secrets.composer_auth }}
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/cache-magento@v8.0.0-rc.0
|
||||
- uses: graycoreio/github-actions-magento2/cache-magento@main
|
||||
with:
|
||||
composer_cache_key: ${{ inputs.composer_cache_key && format('{0} | {1}', inputs.composer_cache_key, matrix.magento) || matrix.magento }}
|
||||
working-directory: ${{ steps.setup-magento.outputs.path }}
|
||||
@@ -155,14 +176,25 @@ jobs:
|
||||
COMPOSER_AUTH: ${{ secrets.composer_auth }}
|
||||
COMPOSER_MIRROR_PATH_REPOS: 1
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/setup-di-compile@v8.0.0-rc.0
|
||||
- uses: graycoreio/github-actions-magento2/setup-di-compile@main
|
||||
with:
|
||||
path: ${{ steps.setup-magento.outputs.path }}
|
||||
|
||||
compute_latest_matrix:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matrix: ${{ steps.supported-version.outputs.matrix }}
|
||||
steps:
|
||||
- uses: graycoreio/github-actions-magento2/supported-version@main
|
||||
id: supported-version
|
||||
with:
|
||||
kind: latest
|
||||
|
||||
coding-standard:
|
||||
runs-on: ${{ matrix.os }}
|
||||
needs: compute_latest_matrix
|
||||
strategy:
|
||||
matrix: ${{ fromJSON(inputs.matrix) }}
|
||||
matrix: ${{ fromJSON(needs.compute_latest_matrix.outputs.matrix) }}
|
||||
fail-fast: ${{ inputs.fail-fast }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
@@ -173,25 +205,27 @@ jobs:
|
||||
tools: composer:v${{ matrix.composer }}
|
||||
coverage: none
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/cache-magento@v8.0.0-rc.0
|
||||
- uses: graycoreio/github-actions-magento2/cache-magento@main
|
||||
with:
|
||||
composer_cache_key: ${{ inputs.composer_cache_key && format('{0} | {1}', inputs.composer_cache_key, matrix.magento) || matrix.magento }}
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/coding-standard@v8.0.0-rc.0
|
||||
- uses: graycoreio/github-actions-magento2/coding-standard@main
|
||||
with:
|
||||
path: ${{ inputs.path }}
|
||||
composer_auth: ${{ secrets.composer_auth }}
|
||||
|
||||
integration_test:
|
||||
runs-on: ${{ matrix.os }}
|
||||
needs: compute_resolved
|
||||
if: ${{ fromJSON(needs.compute_resolved.outputs.resolved)['integration_test'].enabled != false }}
|
||||
strategy:
|
||||
matrix: ${{ fromJSON(inputs.matrix) }}
|
||||
matrix: ${{ fromJSON(needs.compute_resolved.outputs.resolved)['integration_test'].matrix }}
|
||||
fail-fast: ${{ inputs.fail-fast }}
|
||||
services: ${{ matrix.services }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/setup-magento@v8.0.0-rc.0
|
||||
- uses: graycoreio/github-actions-magento2/setup-magento@main
|
||||
id: setup-magento
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
@@ -217,8 +251,10 @@ jobs:
|
||||
|
||||
- run: composer update --no-install
|
||||
working-directory: ${{ steps.setup-magento.outputs.path }}
|
||||
env:
|
||||
COMPOSER_AUTH: ${{ secrets.composer_auth }}
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/cache-magento@v8.0.0-rc.0
|
||||
- uses: graycoreio/github-actions-magento2/cache-magento@main
|
||||
with:
|
||||
composer_cache_key: ${{ inputs.composer_cache_key && format('{0} | {1}', inputs.composer_cache_key, matrix.magento) || matrix.magento }}
|
||||
working-directory: ${{ steps.setup-magento.outputs.path }}
|
||||
@@ -231,7 +267,7 @@ jobs:
|
||||
COMPOSER_AUTH: ${{ secrets.composer_auth }}
|
||||
COMPOSER_MIRROR_PATH_REPOS: 1
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/get-magento-version@v8.0.0-rc.0
|
||||
- uses: graycoreio/github-actions-magento2/get-magento-version@main
|
||||
id: magento-version
|
||||
with:
|
||||
working-directory: ${{ steps.setup-magento.outputs.path }}
|
||||
@@ -265,7 +301,7 @@ jobs:
|
||||
run: ../../../vendor/bin/phpunit -c phpunit.xml.dist --testsuite Extension_Integration_Tests
|
||||
|
||||
- name: Upload test sandbox dir
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
if: failure()
|
||||
with:
|
||||
name: sandbox-data-${{ steps.magento-version.outputs.version }}
|
||||
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
compute_matrix:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matrix: ${{ steps.supported-version.outputs.matrix }}
|
||||
resolved: ${{ steps.resolve.outputs.resolved }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
if: inputs.store_artifact_name == ''
|
||||
@@ -46,22 +46,30 @@ jobs:
|
||||
name: ${{ inputs.store_artifact_name }}
|
||||
path: ${{ inputs.path }}
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/get-magento-version@v8.0.0-rc.0
|
||||
- uses: graycoreio/github-actions-magento2/get-magento-version@main
|
||||
id: get-magento-version
|
||||
with:
|
||||
working-directory: ${{ inputs.path }}
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/supported-version@v8.0.0-rc.0
|
||||
- uses: graycoreio/github-actions-magento2/supported-version@main
|
||||
id: supported-version
|
||||
with:
|
||||
project: ${{ steps.get-magento-version.outputs.supported_version_project }}
|
||||
kind: custom
|
||||
custom_versions: ${{ steps.get-magento-version.outputs.project }}:${{ fromJSON(steps.get-magento-version.outputs.version) }}
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/resolve-check-config@main
|
||||
id: resolve
|
||||
with:
|
||||
kind: store
|
||||
matrix: ${{ steps.supported-version.outputs.matrix }}
|
||||
|
||||
unit-test:
|
||||
runs-on: ${{ matrix.os }}
|
||||
needs: compute_matrix
|
||||
if: ${{ fromJSON(needs.compute_matrix.outputs.resolved)['unit-test'].enabled != false }}
|
||||
strategy:
|
||||
matrix: ${{ fromJSON(needs.compute_matrix.outputs.matrix) }}
|
||||
matrix: ${{ fromJSON(needs.compute_matrix.outputs.resolved)['unit-test'].matrix }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
@@ -73,7 +81,7 @@ jobs:
|
||||
name: ${{ inputs.store_artifact_name }}
|
||||
path: ${{ inputs.path }}
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/setup-magento@v8.0.0-rc.0
|
||||
- uses: graycoreio/github-actions-magento2/setup-magento@main
|
||||
id: setup-magento
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
@@ -82,7 +90,7 @@ jobs:
|
||||
working-directory: ${{ inputs.path }}
|
||||
composer_auth: ${{ secrets.composer_auth }}
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/cache-magento@v8.0.0-rc.0
|
||||
- uses: graycoreio/github-actions-magento2/cache-magento@main
|
||||
with:
|
||||
composer_cache_key: ${{ inputs.composer_cache_key }}
|
||||
working-directory: ${{ steps.setup-magento.outputs.path }}
|
||||
@@ -101,28 +109,30 @@ jobs:
|
||||
|
||||
cat > /tmp/testsuite.xml << 'EOF'
|
||||
<testsuite name="Store_Unit_Tests">
|
||||
<directory>../../../app/code</directory>
|
||||
<directory>../../../app/code/*/*/Test/Unit</directory>
|
||||
</testsuite>
|
||||
EOF
|
||||
sed -i '/<testsuites>/r /tmp/testsuite.xml' dev/tests/unit/phpunit.xml.dist
|
||||
|
||||
## PHPUnit 12 (Magento 2.4.9) implicitly enables failOnEmptyTestSuite when --testsuite is passed.
|
||||
## Default it off only when the consumer hasn't set it themselves, so we don't clobber explicit configuration.
|
||||
if ! grep -q 'failOnEmptyTestSuite=' dev/tests/unit/phpunit.xml.dist; then
|
||||
sed -i 's|<phpunit |<phpunit failOnEmptyTestSuite="false" |' dev/tests/unit/phpunit.xml.dist
|
||||
fi
|
||||
|
||||
## Disable allure (See https://github.com/magento/magento2/issues/36702 )
|
||||
sed -i '/<extensions>/,/<\/extensions>/d' dev/tests/unit/phpunit.xml.dist
|
||||
|
||||
- name: Run unit tests
|
||||
working-directory: ${{ steps.setup-magento.outputs.path }}
|
||||
run: |
|
||||
if find app/code -name "*Test.php" -print -quit | grep -q .; then
|
||||
vendor/bin/phpunit -c dev/tests/unit/phpunit.xml.dist --testsuite Store_Unit_Tests
|
||||
else
|
||||
echo "No unit tests found in app/code, skipping."
|
||||
fi
|
||||
run: vendor/bin/phpunit -c dev/tests/unit/phpunit.xml.dist --testsuite Store_Unit_Tests
|
||||
|
||||
coding-standard:
|
||||
runs-on: ${{ matrix.os }}
|
||||
needs: compute_matrix
|
||||
if: ${{ fromJSON(needs.compute_matrix.outputs.resolved)['coding-standard'].enabled != false }}
|
||||
strategy:
|
||||
matrix: ${{ fromJSON(needs.compute_matrix.outputs.matrix) }}
|
||||
matrix: ${{ fromJSON(needs.compute_matrix.outputs.resolved)['coding-standard'].matrix }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
@@ -134,7 +144,7 @@ jobs:
|
||||
name: ${{ inputs.store_artifact_name }}
|
||||
path: ${{ inputs.path }}
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/setup-magento@v8.0.0-rc.0
|
||||
- uses: graycoreio/github-actions-magento2/setup-magento@main
|
||||
id: setup-magento
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
@@ -143,7 +153,7 @@ jobs:
|
||||
working-directory: ${{ inputs.path }}
|
||||
composer_auth: ${{ secrets.composer_auth }}
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/cache-magento@v8.0.0-rc.0
|
||||
- uses: graycoreio/github-actions-magento2/cache-magento@main
|
||||
with:
|
||||
composer_cache_key: ${{ inputs.composer_cache_key }}
|
||||
working-directory: ${{ steps.setup-magento.outputs.path }}
|
||||
@@ -168,7 +178,72 @@ jobs:
|
||||
EOF
|
||||
fi
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/coding-standard@v8.0.0-rc.0
|
||||
- uses: graycoreio/github-actions-magento2/coding-standard@main
|
||||
with:
|
||||
path: ${{ steps.setup-magento.outputs.path }}
|
||||
composer_auth: ${{ secrets.composer_auth }}
|
||||
|
||||
smoke-test:
|
||||
runs-on: ${{ matrix.os }}
|
||||
needs: compute_matrix
|
||||
if: ${{ fromJSON(needs.compute_matrix.outputs.resolved)['smoke-test'].enabled != false }}
|
||||
services: ${{ matrix.services }}
|
||||
strategy:
|
||||
matrix: ${{ fromJSON(needs.compute_matrix.outputs.resolved)['smoke-test'].matrix }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
if: inputs.store_artifact_name == ''
|
||||
|
||||
- uses: actions/download-artifact@v8
|
||||
if: inputs.store_artifact_name != ''
|
||||
with:
|
||||
name: ${{ inputs.store_artifact_name }}
|
||||
path: ${{ inputs.path }}
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/setup-magento@main
|
||||
id: setup-magento
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
tools: composer:v${{ matrix.composer }}
|
||||
mode: store
|
||||
working-directory: ${{ inputs.path }}
|
||||
composer_auth: ${{ secrets.composer_auth }}
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/cache-magento@main
|
||||
with:
|
||||
composer_cache_key: ${{ inputs.composer_cache_key }}
|
||||
working-directory: ${{ steps.setup-magento.outputs.path }}
|
||||
stamp: ${{ inputs.stamp }}
|
||||
|
||||
- name: Composer install
|
||||
working-directory: ${{ steps.setup-magento.outputs.path }}
|
||||
run: composer install
|
||||
env:
|
||||
COMPOSER_AUTH: ${{ secrets.composer_auth }}
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/setup-install@main
|
||||
id: setup-install
|
||||
with:
|
||||
services: ${{ toJSON(matrix.services) }}
|
||||
path: ${{ steps.setup-magento.outputs.path }}
|
||||
container_id: ${{ job.services['php-fpm'].id }}
|
||||
extra_args: --magento-init-params=MAGE_MODE=developer
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/configure-service-nginx@main
|
||||
with:
|
||||
container_id: ${{ job.services.nginx.id }}
|
||||
magento_path: ${{ inputs.path }}
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/smoke-test@main
|
||||
if: contains(fromJSON(needs.compute_matrix.outputs.resolved)['smoke-test'].probes, 'page')
|
||||
with:
|
||||
kind: page
|
||||
|
||||
## graphql is opt-in: editions without GraphQL modules (e.g. mage-os
|
||||
## minimal) have no /graphql endpoint. Enable it per store via
|
||||
## `.github/check-store.json` -> jobs.smoke-test.probes: ["page", "graphql"].
|
||||
- uses: graycoreio/github-actions-magento2/smoke-test@main
|
||||
if: contains(fromJSON(needs.compute_matrix.outputs.resolved)['smoke-test'].probes, 'graphql')
|
||||
with:
|
||||
kind: graphql
|
||||
@@ -82,7 +82,7 @@ jobs:
|
||||
COMPOSER_AUTH: ${{ secrets.composer_auth }}
|
||||
name: Create Magento ${{ matrix.magento }} Project
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/get-magento-version@v8.0.0-rc.0
|
||||
- uses: graycoreio/github-actions-magento2/get-magento-version@main
|
||||
id: magento-version
|
||||
with:
|
||||
working-directory: ${{ inputs.magento_directory }}
|
||||
@@ -182,7 +182,7 @@ jobs:
|
||||
name: Run Integration Tests
|
||||
|
||||
- name: Upload test sandbox dir
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
if: failure()
|
||||
with:
|
||||
name: sandbox-data-${{ steps.magento-version.outputs.version }}
|
||||
|
||||
@@ -6,11 +6,15 @@ on:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release-candidate:
|
||||
description: 'Cut a release-candidate (prerelease) instead of a normal release.'
|
||||
type: boolean
|
||||
release-mode:
|
||||
description: 'auto = follow conventional commits; rc = bump the rc suffix; graduate = graduate the current rc to a stable release.'
|
||||
type: choice
|
||||
required: false
|
||||
default: false
|
||||
default: auto
|
||||
options:
|
||||
- auto
|
||||
- rc
|
||||
- graduate
|
||||
|
||||
env:
|
||||
RELEASE_BRANCH: release-please--branches--main--components--github-actions-magento2
|
||||
@@ -24,10 +28,10 @@ jobs:
|
||||
releases_created: ${{ steps.release.outputs.releases_created }}
|
||||
steps:
|
||||
- id: release
|
||||
uses: googleapis/release-please-action@v4
|
||||
uses: googleapis/release-please-action@v5
|
||||
with:
|
||||
token: ${{ secrets.GRAYCORE_GITHUB_TOKEN }}
|
||||
config-file: ${{ inputs.release-candidate && 'release-please-config.rc.json' || 'release-please-config.json' }}
|
||||
config-file: ${{ inputs.release-mode == 'rc' && 'release-please-config.rc.json' || (inputs.release-mode == 'graduate' && 'release-please-config.graduate.json' || 'release-please-config.json') }}
|
||||
|
||||
- name: Check if release branch exists
|
||||
id: branch-check
|
||||
|
||||
@@ -1 +1 @@
|
||||
{".":"8.0.0-rc.0"}
|
||||
{".":"8.5.0"}
|
||||
|
||||
@@ -2,6 +2,95 @@
|
||||
|
||||
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)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **setup-magento:** prevent Magento dir from being mirrored into vendor ([22627e1](https://github.com/graycoreio/github-actions-magento2/commit/22627e100059b090adaf2484a09db2d5568492ce))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **check-extension:** prevent recursively mirroring _ghamagento into _ghamagento ([b0131f0](https://github.com/graycoreio/github-actions-magento2/commit/b0131f0fa08dae3b41c6ae476ae5f81ac654c68b))
|
||||
* **check-store:** only run unit tests for unit tests ([ef06f45](https://github.com/graycoreio/github-actions-magento2/commit/ef06f4566ba2bd9e132d0fe85becb5a96b58aa38))
|
||||
|
||||
## [8.0.0-rc.0](https://github.com/graycoreio/github-actions-magento2/compare/v7.0.0...v8.0.0-rc.0) (2026-05-10)
|
||||
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ The `composer.lock` hash is derived from `working-directory/composer.lock` using
|
||||
### Extension (download cache only)
|
||||
|
||||
```yml
|
||||
- uses: graycoreio/github-actions-magento2/cache-magento@v7.0.0 # x-release-please-version
|
||||
- uses: graycoreio/github-actions-magento2/cache-magento@v8.5.0 # x-release-please-version
|
||||
with:
|
||||
composer_cache_key: ${{ inputs.composer_cache_key }}
|
||||
```
|
||||
@@ -41,13 +41,13 @@ The `composer.lock` hash is derived from `working-directory/composer.lock` using
|
||||
### Extension or store (download + vendor stamp cache)
|
||||
|
||||
```yml
|
||||
- uses: graycoreio/github-actions-magento2/setup-magento@v7.0.0 # x-release-please-version
|
||||
- uses: graycoreio/github-actions-magento2/setup-magento@v8.5.0 # x-release-please-version
|
||||
id: setup-magento
|
||||
with:
|
||||
mode: extension # or store
|
||||
# ...
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/cache-magento@v7.0.0 # x-release-please-version
|
||||
- uses: graycoreio/github-actions-magento2/cache-magento@v8.5.0 # x-release-please-version
|
||||
with:
|
||||
composer_cache_key: ${{ inputs.composer_cache_key }}
|
||||
working-directory: ${{ steps.setup-magento.outputs.path }}
|
||||
@@ -69,7 +69,7 @@ As such, use `stamp: true` when `composer.lock` is stable across most runs — a
|
||||
> **Dependabot / Renovate:** Each time a Dependabot or Renovate PR is merged, the remaining open PRs rebase and each produces a new `composer.lock`. This cascades into a large number of unique cache entries, inflating storage costs without delivering proportional compute savings — because automated PRs are not waiting on fast feedback. The fix is to disable stamp caching for automated dependency PRs entirely:
|
||||
>
|
||||
> ```yml
|
||||
> - uses: graycoreio/github-actions-magento2/cache-magento@v7.0.0 # x-release-please-version
|
||||
> - uses: graycoreio/github-actions-magento2/cache-magento@v8.5.0 # x-release-please-version
|
||||
> with:
|
||||
> stamp: ${{ github.actor != 'dependabot[bot]' }}
|
||||
> ```
|
||||
|
||||
@@ -62,7 +62,7 @@ runs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/get-magento-version@v8.0.0-rc.0
|
||||
- uses: graycoreio/github-actions-magento2/get-magento-version@main
|
||||
id: cache-magento-get-magento-version
|
||||
with:
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
tools: composer:v2
|
||||
coverage: none
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/coding-standard@v7.0.0 # x-release-please-version
|
||||
- uses: graycoreio/github-actions-magento2/coding-standard@v8.5.0 # x-release-please-version
|
||||
with:
|
||||
path: app/code # Optional, defaults to .
|
||||
version: 25 # Optional, will use the latest if omitted.
|
||||
|
||||
@@ -52,12 +52,12 @@ runs:
|
||||
fi
|
||||
|
||||
- name: Get Composer Version
|
||||
uses: graycoreio/github-actions-magento2/get-composer-version@v8.0.0-rc.0
|
||||
uses: graycoreio/github-actions-magento2/get-composer-version@main
|
||||
id: get-composer-version
|
||||
if: steps.check-installed.outputs.installed != 'true'
|
||||
|
||||
- name: Check if allow-plugins option is available for this version of composer
|
||||
uses: graycoreio/github-actions-magento2/semver-compare@v8.0.0-rc.0
|
||||
uses: graycoreio/github-actions-magento2/semver-compare@main
|
||||
id: is-allow-plugins-available
|
||||
if: steps.check-installed.outputs.installed != 'true'
|
||||
with:
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
# Configure Service Nginx
|
||||
|
||||
A GitHub Action that pushes a Magento-aware nginx configuration into an **already-running nginx service container**, reloads it, and waits for the container's healthcheck to pass.
|
||||
|
||||
The action does **not** start nginx. It assumes the calling workflow declared nginx as a `services:` container (typically alongside `php-fpm`, and other mandatory Magento services).
|
||||
|
||||
The shipped `default.conf` is a thin outer wrapper that defines a `fastcgi_backend` upstream pointing at `php-fpm:9000`, sets `$MAGE_ROOT`, and includes Magento's own `nginx.conf.sample` from your own Magento install. All real routing rules come from Magento's bundled file.
|
||||
|
||||
## When to use this
|
||||
|
||||
Use this action when you have a workflow that:
|
||||
|
||||
1. Boots nginx as `services:` containers with the workspace bind-mounted at `/var/www/html`, and
|
||||
2. Wants those containers to actually serve a Magento store you've already installed into the workspace (e.g. for end-to-end smoke tests, integration tests, or any HTTP-driven check).
|
||||
|
||||
You do **not** need this action if:
|
||||
|
||||
- You're not running nginx at all (unit tests, coding standards, static analysis).
|
||||
- nginx is started by something other than a GitHub Actions `services:` block
|
||||
- You've already configured nginx some other way and don't need a Magento-ready outer config.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- An nginx service container is running on the same Docker host as the runner, with an image matching the `image` input.
|
||||
- A `php-fpm` container, the included `default.conf` will set up a fast-cgi backend to `php-fpm:9000`.
|
||||
- The runner's workspace (`$GITHUB_WORKSPACE`) is bind-mounted into the nginx container at `/var/www/html`.
|
||||
- A Magento install exists at `${{ inputs.magento_path }}` relative to the workspace, with `nginx.conf.sample` present (it ships with Magento by default after `composer install`).
|
||||
|
||||
## Inputs
|
||||
|
||||
| Input | Required | Default | Description |
|
||||
| ------------------------ | -------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `container_id` | Yes | — | The ID of the running nginx service container. Pass `${{ job.services.nginx.id }}` (replace `nginx` with whatever you named the service). |
|
||||
| `magento_path` | No | `.` | Path to the Magento store, relative to the GitHub workspace. Combined with the `/var/www/html` mount prefix to compute the in-container `MAGE_ROOT`. |
|
||||
| `health_timeout_seconds` | No | `10` | How long to wait for nginx to report `healthy` after the config is pushed and the container restarts. |
|
||||
|
||||
## Usage
|
||||
|
||||
```yml
|
||||
jobs:
|
||||
smoke-test:
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
## There are other service requirements for Magento, but this is just for the explanation of this service
|
||||
nginx:
|
||||
image: nginx:1.27-alpine
|
||||
ports: ["80:80"]
|
||||
volumes:
|
||||
- ${{ github.workspace }}:/var/www/html
|
||||
options: --health-cmd "nginx -t" --health-interval=10s --health-retries=3
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: graycoreio/github-actions-magento2/setup-magento@main
|
||||
id: setup-magento
|
||||
with:
|
||||
mode: store
|
||||
- run: composer install
|
||||
working-directory: ${{ steps.setup-magento.outputs.path }}
|
||||
- uses: graycoreio/github-actions-magento2/setup-install@main
|
||||
with:
|
||||
services: ${{ toJSON(matrix.services) }}
|
||||
path: ${{ steps.setup-magento.outputs.path }}
|
||||
container_id: ${{ job.services['php-fpm'].id }}
|
||||
- uses: graycoreio/github-actions-magento2/configure-service-nginx@main
|
||||
with:
|
||||
container_id: ${{ job.services.nginx.id }}
|
||||
magento_path: ${{ inputs.path }}
|
||||
- uses: graycoreio/github-actions-magento2/smoke-test@main
|
||||
with:
|
||||
kind: page
|
||||
```
|
||||
@@ -0,0 +1,67 @@
|
||||
name: "Configure nginx service container"
|
||||
author: "Graycore"
|
||||
description: "Pushes a Magento-aware nginx config into an already-running nginx service container, restarts it, and waits for the container healthcheck to pass."
|
||||
|
||||
inputs:
|
||||
container_id:
|
||||
description: "The ID of the running nginx service container. Pass the value of `job.services.nginx.id` (replace `nginx` with whatever you named the service in `services:`)."
|
||||
required: true
|
||||
magento_path:
|
||||
description: "Path to the Magento store, relative to the GitHub workspace. The workspace is mounted at /var/www/html in both the nginx and php-fpm service containers, so this is combined with that prefix to compute MAGE_ROOT."
|
||||
required: false
|
||||
default: "."
|
||||
health_timeout_seconds:
|
||||
description: "How long to wait for nginx to become healthy after the config is pushed and the container is restarted."
|
||||
required: false
|
||||
default: "10"
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Push nginx config
|
||||
shell: bash
|
||||
env:
|
||||
NGINX_CID: ${{ inputs.container_id }}
|
||||
ACTION_PATH: ${{ github.action_path }}
|
||||
MAGENTO_PATH: ${{ inputs.magento_path }}
|
||||
run: |
|
||||
case "$MAGENTO_PATH" in
|
||||
""|".") MAGE_ROOT="/var/www/html" ;;
|
||||
/*) MAGE_ROOT="$MAGENTO_PATH" ;;
|
||||
*) MAGE_ROOT="/var/www/html/$MAGENTO_PATH" ;;
|
||||
esac
|
||||
MAGE_ROOT="${MAGE_ROOT%/}"
|
||||
|
||||
sed "s|__MAGE_ROOT__|$MAGE_ROOT|g" "$ACTION_PATH/conf.d/default.conf" > /tmp/default.conf
|
||||
|
||||
docker cp /tmp/default.conf "$NGINX_CID:/etc/nginx/conf.d/default.conf"
|
||||
|
||||
echo "--- default.conf in container ---"
|
||||
docker exec "$NGINX_CID" cat /etc/nginx/conf.d/default.conf
|
||||
docker exec "$NGINX_CID" nginx -t
|
||||
|
||||
- name: Restart nginx and wait for healthy
|
||||
shell: bash
|
||||
env:
|
||||
NGINX_CID: ${{ inputs.container_id }}
|
||||
TIMEOUT: ${{ inputs.health_timeout_seconds }}
|
||||
run: |
|
||||
docker restart "$NGINX_CID"
|
||||
deadline=$(( $(date +%s) + TIMEOUT ))
|
||||
last=""
|
||||
while [ "$(date +%s)" -lt "$deadline" ]; do
|
||||
last=$(docker inspect --format '{{.State.Health.Status}}' "$NGINX_CID" 2>/dev/null || echo "")
|
||||
running=$(docker inspect --format '{{.State.Running}}' "$NGINX_CID" 2>/dev/null || echo "")
|
||||
echo "running=$running health=$last"
|
||||
if [ "$last" = "healthy" ]; then
|
||||
exit 0
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
echo "nginx did not reach healthy state within ${TIMEOUT}s (last=$last)"
|
||||
docker logs "$NGINX_CID" 2>&1 | tail -80 || true
|
||||
exit 1
|
||||
|
||||
branding:
|
||||
icon: "code"
|
||||
color: "green"
|
||||
@@ -0,0 +1,10 @@
|
||||
upstream fastcgi_backend {
|
||||
server php-fpm:9000;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80 default_server;
|
||||
server_name _;
|
||||
set $MAGE_ROOT __MAGE_ROOT__;
|
||||
include __MAGE_ROOT__/nginx[.]conf*;
|
||||
}
|
||||
@@ -18,6 +18,23 @@ See the [check-extension.yaml](../../.github/workflows/check-extension.yaml)
|
||||
|
||||
The Magento matrix format outlined by the [supported versions action.](https://github.com/graycoreio/github-actions-magento2/tree/main/supported-version/supported.json)
|
||||
|
||||
## Configuration
|
||||
|
||||
Each check can be toggled on/off through an optional `.github/check-extension.json` file in the repo that calls this workflow.
|
||||
|
||||
You can learn more about this file here in the [`resolve-check-config` action.](../../resolve-check-config/README.md):
|
||||
|
||||
Reference the published JSON Schema with `$schema` to get autocompletion and inline validation in editors that support it — see [`check-extension.schema.json`](../../resolve-check-config/check-extension.schema.json):
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/graycoreio/github-actions-magento2/main/resolve-check-config/check-extension.schema.json",
|
||||
"jobs": {
|
||||
"integration_test": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```yml
|
||||
@@ -38,12 +55,12 @@ jobs:
|
||||
matrix: ${{ steps.supported-version.outputs.matrix }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: graycoreio/github-actions-magento2/supported-version@v7.0.0 # x-release-please-version
|
||||
- uses: graycoreio/github-actions-magento2/supported-version@v8.5.0 # x-release-please-version
|
||||
id: supported-version
|
||||
- run: echo ${{ steps.supported-version.outputs.matrix }}
|
||||
check-extension:
|
||||
needs: compute_matrix
|
||||
uses: graycoreio/github-actions-magento2/.github/workflows/check-extension.yaml@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:
|
||||
matrix: ${{ needs.compute_matrix.outputs.matrix }}
|
||||
```
|
||||
|
||||
@@ -24,6 +24,24 @@ See the [check-store.yaml](../../.github/workflows/check-store.yaml)
|
||||
|
||||
- **Unit Tests** — runs PHPUnit against custom code in `app/code`. Skipped automatically if no test files are found.
|
||||
- **Coding Standard** — runs the Magento Coding Standard against `app/code`. Uses your `phpcs.xml` (or `.phpcs.xml`, `phpcs.xml.dist`, `.phpcs.xml.dist`) if one exists, otherwise a sensible default is generated.
|
||||
- **Smoke Test** — boots your store against the supported-version service set (mysql, search, queue, cache, nginx, php-fpm) and runs the smoke probes against it.
|
||||
|
||||
## Configuration
|
||||
|
||||
Each check can be toggled on/off through an optional `.github/check-store.json` file in the repo that calls this workflow.
|
||||
|
||||
You can learn more about this file here in the [`resolve-check-config` action.](../../resolve-check-config/README.md):
|
||||
|
||||
Reference the published JSON Schema with `$schema` to get autocompletion and inline validation in editors that support it — see [`check-store.schema.json`](../../resolve-check-config/check-store.schema.json):
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/graycoreio/github-actions-magento2/main/resolve-check-config/check-store.schema.json",
|
||||
"jobs": {
|
||||
"coding-standard": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -40,7 +58,7 @@ on:
|
||||
|
||||
jobs:
|
||||
check-store:
|
||||
uses: graycoreio/github-actions-magento2/.github/workflows/check-store.yaml@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:
|
||||
composer_auth: ${{ secrets.COMPOSER_AUTH }}
|
||||
```
|
||||
@@ -52,7 +70,7 @@ If your pipeline builds or prepares the store in a prior job and uploads it as a
|
||||
```yml
|
||||
jobs:
|
||||
check-store:
|
||||
uses: graycoreio/github-actions-magento2/.github/workflows/check-store.yaml@v7.0.0 # x-release-please-version
|
||||
uses: graycoreio/github-actions-magento2/.github/workflows/check-store.yaml@v8.5.0 # x-release-please-version
|
||||
secrets:
|
||||
composer_auth: ${{ secrets.COMPOSER_AUTH }}
|
||||
```
|
||||
|
||||
@@ -50,13 +50,13 @@ jobs:
|
||||
matrix: ${{ steps.supported-version.outputs.matrix }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: graycoreio/github-actions-magento2/supported-version@v7.0.0 # x-release-please-version
|
||||
- uses: graycoreio/github-actions-magento2/supported-version@v8.5.0 # x-release-please-version
|
||||
with:
|
||||
include_services: true
|
||||
id: supported-version
|
||||
integration-workflow:
|
||||
needs: compute_matrix
|
||||
uses: graycoreio/github-actions-magento2/.github/workflows/integration.yaml@v7.0.0 # x-release-please-version
|
||||
uses: graycoreio/github-actions-magento2/.github/workflows/integration.yaml@v8.5.0 # x-release-please-version
|
||||
with:
|
||||
package_name: my-vendor/package
|
||||
matrix: ${{ needs.compute_matrix.outputs.matrix }}
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: graycoreio/github-actions-magento2/fix-magento-install@v7.0.0 # x-release-please-version
|
||||
- uses: graycoreio/github-actions-magento2/fix-magento-install@v8.5.0 # x-release-please-version
|
||||
with:
|
||||
magento_directory: path/to/magento
|
||||
```
|
||||
|
||||
@@ -9,7 +9,7 @@ inputs:
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- uses: graycoreio/github-actions-magento2/get-magento-version@v8.0.0-rc.0
|
||||
- uses: graycoreio/github-actions-magento2/get-magento-version@main
|
||||
id: init-magento-get-magento-version
|
||||
with:
|
||||
working-directory: ${{ inputs.magento_directory }}
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
name: A job to compute an installed Composer version.
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: graycoreio/github-actions-magento2/get-composer-version@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
|
||||
- run: echo version ${{ steps.get-composer-version.outputs.version }}
|
||||
shell: bash
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
name: A job to compute an installed Magento version.
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: graycoreio/github-actions-magento2/get-magento-version@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
|
||||
- run: echo version ${{ steps.get-magento-version.outputs.version }}
|
||||
shell: bash
|
||||
|
||||
@@ -15,6 +15,9 @@ outputs:
|
||||
project:
|
||||
description: 'The Magento project package name (e.g. magento/project-community-edition)'
|
||||
value: ${{ steps.get-magento-version.outputs.project }}
|
||||
supported_version_project:
|
||||
description: 'The `project` value to pass to the supported-version action (e.g. `magento-open-source`, `mage-os`, `mage-os-minimal`). Empty when no Magento project is detected.'
|
||||
value: ${{ steps.get-magento-version.outputs.supported_version_project }}
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"packages": [
|
||||
{
|
||||
"name": "mage-os/product-minimal-edition",
|
||||
"version": "3.0.0"
|
||||
}
|
||||
],
|
||||
"packages-dev": []
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
set -uo pipefail
|
||||
|
||||
WORKING_DIR="${1:-.}"
|
||||
PATTERN="magento/product-(community|enterprise)-edition|mage-os/product-community-edition"
|
||||
PATTERN="magento/product-(community|enterprise)-edition|mage-os/product-(community|minimal)-edition"
|
||||
|
||||
cd "$WORKING_DIR"
|
||||
|
||||
@@ -16,5 +16,13 @@ PRODUCT=$(echo "${RESULT:-}" | awk '{print $1}')
|
||||
VERSION=$(echo "${RESULT:-}" | awk '{print $2}' | sed 's/^v//')
|
||||
PROJECT=$(echo "$PRODUCT" | sed 's/product-/project-/')
|
||||
|
||||
case "$PROJECT" in
|
||||
magento/*) SUPPORTED_VERSION_PROJECT="magento-open-source" ;;
|
||||
mage-os/project-community-edition) SUPPORTED_VERSION_PROJECT="mage-os" ;;
|
||||
mage-os/project-minimal-edition) SUPPORTED_VERSION_PROJECT="mage-os-minimal" ;;
|
||||
*) SUPPORTED_VERSION_PROJECT="" ;;
|
||||
esac
|
||||
|
||||
echo "version=\"$VERSION\""
|
||||
echo "project=$PROJECT"
|
||||
echo "supported_version_project=$SUPPORTED_VERSION_PROJECT"
|
||||
|
||||
+23
-12
@@ -24,31 +24,42 @@ field() {
|
||||
}
|
||||
|
||||
OUT=$(bash "$SCRIPT" "$FIXTURES/store-lock")
|
||||
assert_eq "store lock: version" '"2.4.7"' "$(field "$OUT" version)"
|
||||
assert_eq "store lock: project" "magento/project-community-edition" "$(field "$OUT" project)"
|
||||
assert_eq "store lock: version" '"2.4.7"' "$(field "$OUT" version)"
|
||||
assert_eq "store lock: project" "magento/project-community-edition" "$(field "$OUT" project)"
|
||||
assert_eq "store lock: supported_version_project" "magento-open-source" "$(field "$OUT" supported_version_project)"
|
||||
|
||||
OUT=$(bash "$SCRIPT" "$FIXTURES/store-v-prefix")
|
||||
assert_eq "store v-prefix: version" '"2.4.6"' "$(field "$OUT" version)"
|
||||
|
||||
OUT=$(bash "$SCRIPT" "$FIXTURES/enterprise")
|
||||
assert_eq "enterprise: version" '"2.4.7-p1"' "$(field "$OUT" version)"
|
||||
assert_eq "enterprise: project" "magento/project-enterprise-edition" "$(field "$OUT" project)"
|
||||
assert_eq "enterprise: version" '"2.4.7-p1"' "$(field "$OUT" version)"
|
||||
assert_eq "enterprise: project" "magento/project-enterprise-edition" "$(field "$OUT" project)"
|
||||
assert_eq "enterprise: supported_version_project" "magento-open-source" "$(field "$OUT" supported_version_project)"
|
||||
|
||||
OUT=$(bash "$SCRIPT" "$FIXTURES/mage-os")
|
||||
assert_eq "mage-os: version" '"1.0.0"' "$(field "$OUT" version)"
|
||||
assert_eq "mage-os: project" "mage-os/project-community-edition" "$(field "$OUT" project)"
|
||||
assert_eq "mage-os: version" '"1.0.0"' "$(field "$OUT" version)"
|
||||
assert_eq "mage-os: project" "mage-os/project-community-edition" "$(field "$OUT" project)"
|
||||
assert_eq "mage-os: supported_version_project" "mage-os" "$(field "$OUT" supported_version_project)"
|
||||
|
||||
OUT=$(bash "$SCRIPT" "$FIXTURES/mage-os-minimal")
|
||||
assert_eq "mage-os-minimal: version" '"3.0.0"' "$(field "$OUT" version)"
|
||||
assert_eq "mage-os-minimal: project" "mage-os/project-minimal-edition" "$(field "$OUT" project)"
|
||||
assert_eq "mage-os-minimal: supported_version_project" "mage-os-minimal" "$(field "$OUT" supported_version_project)"
|
||||
|
||||
OUT=$(bash "$SCRIPT" "$FIXTURES/store-json")
|
||||
assert_eq "store json: version" '"2.4.6-p1"' "$(field "$OUT" version)"
|
||||
assert_eq "store json: project" "magento/project-community-edition" "$(field "$OUT" project)"
|
||||
assert_eq "store json: version" '"2.4.6-p1"' "$(field "$OUT" version)"
|
||||
assert_eq "store json: project" "magento/project-community-edition" "$(field "$OUT" project)"
|
||||
assert_eq "store json: supported_version_project" "magento-open-source" "$(field "$OUT" supported_version_project)"
|
||||
|
||||
OUT=$(bash "$SCRIPT" "$FIXTURES/extension")
|
||||
assert_eq "extension: version" '""' "$(field "$OUT" version)"
|
||||
assert_eq "extension: project" "" "$(field "$OUT" project)"
|
||||
assert_eq "extension: version" '""' "$(field "$OUT" version)"
|
||||
assert_eq "extension: project" "" "$(field "$OUT" project)"
|
||||
assert_eq "extension: supported_version_project" "" "$(field "$OUT" supported_version_project)"
|
||||
|
||||
OUT=$(bash "$SCRIPT" "$FIXTURES/empty")
|
||||
assert_eq "empty dir: version" '""' "$(field "$OUT" version)"
|
||||
assert_eq "empty dir: project" "" "$(field "$OUT" project)"
|
||||
assert_eq "empty dir: version" '""' "$(field "$OUT" version)"
|
||||
assert_eq "empty dir: project" "" "$(field "$OUT" project)"
|
||||
assert_eq "empty dir: supported_version_project" "" "$(field "$OUT" supported_version_project)"
|
||||
|
||||
echo ""
|
||||
echo "$PASS passed, $FAIL failed"
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@graycoreio/github-actions-magento2",
|
||||
"version": "8.0.0-rc.0",
|
||||
"version": "8.5.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@graycoreio/github-actions-magento2",
|
||||
"version": "8.0.0-rc.0",
|
||||
"version": "8.5.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.11.1",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@graycoreio/github-actions-magento2",
|
||||
"version": "8.0.0-rc.0",
|
||||
"version": "8.5.0",
|
||||
"description": "Github Actions for Magento 2",
|
||||
"scripts": {
|
||||
"test": "cd supported-version && npm run test && cd - && cd setup-install && npm run test && cd -",
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"bump-minor-pre-major": true,
|
||||
"bump-patch-for-minor-pre-major": true,
|
||||
"draft-pull-request": true,
|
||||
"prerelease": false,
|
||||
"include-component-in-tag": false,
|
||||
"include-v-in-tag": true,
|
||||
"pull-request-title-pattern": "chore: release ${version}",
|
||||
"packages": {
|
||||
".": {
|
||||
"release-type": "node",
|
||||
"versioning": "prerelease",
|
||||
"extra-files": [
|
||||
{ "type": "generic", "path": "*/README.md", "glob": true },
|
||||
{ "type": "generic", "path": "docs/workflows/*.md", "glob": true }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
# "Resolve Check Config" Action
|
||||
|
||||
Reads `.github/check-<kind>.json` (or a path you specify), validates job names against the known list for the selected workflow kind, and emits a per-job filtered version of the `supported-version` matrix. Each job in the output carries an `enabled` flag and its own `matrix`, where every entry's `services` map has been narrowed to the tiers that job actually needs. Consumers gate each job with `fromJSON(...)['<job>'].enabled != false` and feed `fromJSON(...)['<job>'].matrix` into `strategy.matrix`.
|
||||
|
||||
A missing config file is fine — every known job is emitted with its default tier list.
|
||||
|
||||
## Schemas
|
||||
|
||||
Reference the published JSON Schema from your config's `$schema` key for autocompletion and inline validation in editors that support it:
|
||||
|
||||
- [`check-store.schema.json`](./check-store.schema.json) — config for the [MageCheck Store](../docs/workflows/check-store.md) workflow
|
||||
- [`check-extension.schema.json`](./check-extension.schema.json) — config for the [MageCheck Extension](../docs/workflows/check-extension.md) workflow
|
||||
|
||||
## Inputs
|
||||
|
||||
| Input | Description | Required | Default |
|
||||
|---------------|--------------------------------------------------------------------------------------------------------------------------------------------------------|----------|-------------------------------|
|
||||
| `kind` | Which reusable workflow this config belongs to: `store` or `extension`. Selects the default `config_path`, the known-job list, and the per-job tier defaults. | true | |
|
||||
| `matrix` | The matrix JSON emitted by the `supported-version` action. Each entry's `services` map is filtered per-job based on the resolved tier list. | true | |
|
||||
| `config_path` | Path to the check-config JSON file, relative to the runner workspace. | false | `.github/check-<kind>.json` |
|
||||
|
||||
## Usage
|
||||
|
||||
```yml
|
||||
jobs:
|
||||
compute_matrix:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
resolved: ${{ steps.resolve.outputs.resolved }}
|
||||
steps:
|
||||
- uses: graycoreio/github-actions-magento2/supported-version@v8.5.0 # x-release-please-version
|
||||
id: supported-version
|
||||
with:
|
||||
kind: currently-supported
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/resolve-check-config@v8.5.0 # x-release-please-version
|
||||
id: resolve
|
||||
with:
|
||||
kind: store
|
||||
matrix: ${{ steps.supported-version.outputs.matrix }}
|
||||
|
||||
smoke-test:
|
||||
runs-on: ${{ matrix.os }}
|
||||
needs: compute_matrix
|
||||
if: ${{ fromJSON(needs.compute_matrix.outputs.resolved)['smoke-test'].enabled != false }}
|
||||
services: ${{ matrix.services }}
|
||||
strategy:
|
||||
matrix: ${{ fromJSON(needs.compute_matrix.outputs.resolved)['smoke-test'].matrix }}
|
||||
steps:
|
||||
- run: echo "running with ${{ toJSON(matrix.services) }}"
|
||||
```
|
||||
|
||||
Example `.github/check-store.json` for opting out of a specific job:
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/graycoreio/github-actions-magento2/main/resolve-check-config/check-store.schema.json",
|
||||
"jobs": {
|
||||
"coding-standard": false
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,27 @@
|
||||
name: "Resolve check config"
|
||||
author: "Graycore"
|
||||
description: "Reads .github/check-<kind>.json (or a path you specify), validates job names against the known list for that workflow kind, and emits a per-job filtered version of the supported-version matrix. Missing config file is treated as 'all jobs enabled with their default tier list.'"
|
||||
|
||||
inputs:
|
||||
kind:
|
||||
required: true
|
||||
description: "Which reusable workflow this config belongs to: `store` or `extension`. Selects the default `config_path`, the known-job list used for validation, and the per-job default tier list."
|
||||
matrix:
|
||||
required: true
|
||||
description: "The matrix JSON emitted by the `supported-version` action. Each entry's `services` map is filtered per-job based on the resolved tier list and embedded in the per-job `matrix` output."
|
||||
config_path:
|
||||
required: false
|
||||
default: ""
|
||||
description: "Path to the check-config JSON file, relative to the runner workspace. Defaults to `.github/check-<kind>.json`. Missing file is fine — every known job is emitted with its default tier list."
|
||||
|
||||
outputs:
|
||||
resolved:
|
||||
description: "The per-job resolved configuration as a JSON object. Each top-level key is a known job name for the selected kind; values are objects with `enabled` (boolean) and `matrix` (a copy of the supported-version matrix where every entry's `services` is filtered to the tiers the job needs). Consumers default-enable omitted jobs via `fromJSON(...)['<job>'].enabled != false` and use `fromJSON(...)['<job>'].matrix` for `strategy.matrix`."
|
||||
|
||||
runs:
|
||||
using: "node24"
|
||||
main: "dist/index.js"
|
||||
|
||||
branding:
|
||||
icon: "check-square"
|
||||
color: "green"
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://raw.githubusercontent.com/graycoreio/github-actions-magento2/main/resolve-check-config/check-extension.schema.json",
|
||||
"title": "graycoreio check-extension config",
|
||||
"description": "Configuration consumed by the check-extension reusable workflow. Per-job toggles and settings live under `jobs`. Top-level remains open for future global keys.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"jobs": {
|
||||
"type": "object",
|
||||
"description": "Per-job configuration. Each key is a job name declared by check-extension; unknown keys are rejected.",
|
||||
"properties": {
|
||||
"unit-test-extension": { "$ref": "#/$defs/jobConfig" },
|
||||
"compile-extension": { "$ref": "#/$defs/jobConfig" },
|
||||
"coding-standard": { "$ref": "#/$defs/jobConfig" },
|
||||
"integration_test": { "$ref": "#/$defs/jobConfig" }
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": true,
|
||||
"$defs": {
|
||||
"jobConfig": {
|
||||
"description": "How a single job should be configured. Boolean form is shorthand for { enabled: <bool> }; object form allows extra per-job keys.",
|
||||
"oneOf": [
|
||||
{ "type": "boolean" },
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Whether the job should run. Defaults to true when the key is present.",
|
||||
"default": true
|
||||
},
|
||||
"services": {
|
||||
"type": "array",
|
||||
"description": "Tier names this job needs as GitHub Actions service containers. mysql is always implicit.",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": ["search", "queue", "cache", "web"]
|
||||
},
|
||||
"uniqueItems": true
|
||||
}
|
||||
},
|
||||
"additionalProperties": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://raw.githubusercontent.com/graycoreio/github-actions-magento2/main/resolve-check-config/check-store.schema.json",
|
||||
"title": "graycoreio check-store config",
|
||||
"description": "Configuration consumed by the check-store reusable workflow. Per-job toggles and settings live under `jobs`. Top-level remains open for future global keys.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"jobs": {
|
||||
"type": "object",
|
||||
"description": "Per-job configuration. Each key is a job name declared by check-store; unknown keys are rejected.",
|
||||
"properties": {
|
||||
"unit-test": { "$ref": "#/$defs/jobConfig" },
|
||||
"coding-standard": { "$ref": "#/$defs/jobConfig" },
|
||||
"smoke-test": { "$ref": "#/$defs/smokeJobConfig" }
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": true,
|
||||
"$defs": {
|
||||
"jobConfig": {
|
||||
"description": "How a single job should be configured. Boolean form is shorthand for { enabled: <bool> }; object form allows extra per-job keys.",
|
||||
"oneOf": [
|
||||
{ "type": "boolean" },
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Whether the job should run. Defaults to true when the key is present.",
|
||||
"default": true
|
||||
}
|
||||
},
|
||||
"additionalProperties": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"smokeJobConfig": {
|
||||
"description": "How the smoke-test job should be configured. Boolean form is shorthand for { enabled: <bool> }; object form adds a `probes` list on top of `enabled`.",
|
||||
"oneOf": [
|
||||
{ "type": "boolean" },
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Whether the job should run. Defaults to true when the key is present.",
|
||||
"default": true
|
||||
},
|
||||
"probes": {
|
||||
"type": "array",
|
||||
"description": "Which smoke-test probes to run. Defaults to [\"page\"]. Add \"graphql\" to also probe the GraphQL endpoint — only for editions that ship GraphQL modules (the mage-os minimal edition does not, so /graphql 404s there).",
|
||||
"items": { "enum": ["page", "graphql"] },
|
||||
"default": ["page"]
|
||||
}
|
||||
},
|
||||
"additionalProperties": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+69
File diff suppressed because one or more lines are too long
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"$schema": "../check-extension.schema.json",
|
||||
"jobs": {
|
||||
"unit-test-extension": true,
|
||||
"compile-extension": true,
|
||||
"coding-standard": true,
|
||||
"integration_test": {
|
||||
"services": ["search", "cache"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"$schema": "../check-extension.schema.json",
|
||||
"jobs": {
|
||||
"integration_test": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"$schema": "../check-store.schema.json",
|
||||
"jobs": {
|
||||
"unit-test": true,
|
||||
"coding-standard": true,
|
||||
"integration-test": {
|
||||
"services": ["search", "queue", "cache"]
|
||||
},
|
||||
"smoke-test": {
|
||||
"services": ["search", "queue", "cache", "nginx", "php-fpm"],
|
||||
"probes": ["page", "graphql"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"$schema": "../check-store.schema.json",
|
||||
"jobs": {
|
||||
"smoke-test": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
module.exports = {
|
||||
clearMocks: true,
|
||||
moduleFileExtensions: ['js', 'ts'],
|
||||
testMatch: ['**/*.spec.ts'],
|
||||
transform: {
|
||||
'^.+\\.ts$': 'ts-jest'
|
||||
},
|
||||
verbose: true
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "@graycoreio/github-actions-magento2-resolve-check-config",
|
||||
"version": "1.0.0",
|
||||
"description": "A Github Action that reads .github/<workflow>.json, validates it against the known job list, and emits resolved per-job configuration.",
|
||||
"main": "index.js",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "npx esbuild --outfile=dist/index.js --platform=node --bundle --minify src/index.ts",
|
||||
"test": "jest"
|
||||
},
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@actions/core": "0.0.0-PLACEHOLDER"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "0.0.0-PLACEHOLDER",
|
||||
"jest": "0.0.0-PLACEHOLDER",
|
||||
"ts-jest": "0.0.0-PLACEHOLDER"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import * as core from '@actions/core';
|
||||
import * as fs from 'fs';
|
||||
import * as nodePath from 'path';
|
||||
import { assertKind } from './kind';
|
||||
import { parseMatrixInput, parseRawConfig } from './parse';
|
||||
import { resolveConfig } from './resolve';
|
||||
|
||||
export const run = async (): Promise<void> => {
|
||||
try {
|
||||
const kind = assertKind(core.getInput('kind', { required: true }));
|
||||
const matrix = parseMatrixInput(core.getInput('matrix', { required: true }));
|
||||
const configPath = core.getInput('config_path') || `.github/check-${kind}.json`;
|
||||
const workspace = process.env.GITHUB_WORKSPACE || process.cwd();
|
||||
const absolute = nodePath.resolve(workspace, configPath);
|
||||
|
||||
let raw = {};
|
||||
if (fs.existsSync(absolute)) {
|
||||
const text = fs.readFileSync(absolute, 'utf-8');
|
||||
raw = parseRawConfig(text);
|
||||
core.info(`resolve-check-config: read ${absolute}`);
|
||||
} else {
|
||||
core.info(`resolve-check-config: ${absolute} not found — emitting defaults for every known job`);
|
||||
}
|
||||
|
||||
const resolved = resolveConfig(raw, kind, matrix);
|
||||
|
||||
core.setOutput('resolved', JSON.stringify(resolved));
|
||||
} catch (error) {
|
||||
core.setFailed((error as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -0,0 +1,26 @@
|
||||
import { assertKind, isKind } from './kind';
|
||||
|
||||
describe('isKind / assertKind', () => {
|
||||
it('accepts "store"', () => {
|
||||
expect(isKind('store')).toBe(true);
|
||||
expect(assertKind('store')).toBe('store');
|
||||
});
|
||||
|
||||
it('accepts "extension"', () => {
|
||||
expect(isKind('extension')).toBe(true);
|
||||
expect(assertKind('extension')).toBe('extension');
|
||||
});
|
||||
|
||||
it('rejects other strings', () => {
|
||||
expect(isKind('taco')).toBe(false);
|
||||
expect(() => assertKind('taco')).toThrowError(/`kind` must be 'store' or 'extension'/);
|
||||
});
|
||||
|
||||
it('rejects empty input', () => {
|
||||
expect(() => assertKind('')).toThrowError(/`kind` must be 'store' or 'extension'/);
|
||||
});
|
||||
|
||||
it('rejects non-string input', () => {
|
||||
expect(() => assertKind(undefined)).toThrowError(/`kind` must be 'store' or 'extension'/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Kind } from './types';
|
||||
|
||||
/**
|
||||
* Type guard for the `kind` input. Use this when you have an
|
||||
* `unknown` value (e.g. from `core.getInput`) and want to narrow it
|
||||
* without throwing.
|
||||
*/
|
||||
export const isKind = (value: unknown): value is Kind =>
|
||||
value === 'store' || value === 'extension';
|
||||
|
||||
/**
|
||||
* Narrows an `unknown` (typically the raw action input) to `Kind` or
|
||||
* throws a user-facing error naming the accepted values. Prefer this
|
||||
* at the action boundary so a bad `kind` fails fast with a clear
|
||||
* message rather than later as an obscure dispatch miss.
|
||||
*/
|
||||
export const assertKind = (value: unknown): Kind => {
|
||||
if (!isKind(value)) {
|
||||
throw new Error(`check-config: \`kind\` must be 'store' or 'extension' (got ${JSON.stringify(value)})`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import { EXTENSION_JOBS, KNOWN_JOBS_EXTENSION, resolveExtensionConfig } from './extension';
|
||||
import { Matrix } from '../types';
|
||||
|
||||
const FULL_SERVICES = {
|
||||
mysql: { image: 'mysql:8' },
|
||||
opensearch: { image: 'opensearchproject/opensearch:2' },
|
||||
rabbitmq: { image: 'rabbitmq:3' },
|
||||
valkey: { image: 'valkey:8' },
|
||||
nginx: { image: 'nginx:1.27' },
|
||||
'php-fpm': { image: 'php:8.3-fpm' },
|
||||
};
|
||||
|
||||
const MATRIX: Matrix = {
|
||||
include: [{ php: '8.3', services: { ...FULL_SERVICES } }],
|
||||
};
|
||||
|
||||
describe('EXTENSION_JOBS', () => {
|
||||
it('declares the check-extension jobs', () => {
|
||||
expect(Object.keys(EXTENSION_JOBS).sort()).toEqual([
|
||||
'coding-standard',
|
||||
'compile-extension',
|
||||
'integration_test',
|
||||
'unit-test-extension',
|
||||
]);
|
||||
});
|
||||
|
||||
it('keeps KNOWN_JOBS_EXTENSION in sync with the map keys', () => {
|
||||
expect([...KNOWN_JOBS_EXTENSION].sort()).toEqual(Object.keys(EXTENSION_JOBS).sort());
|
||||
});
|
||||
|
||||
it('declares integration_test required tiers (no web)', () => {
|
||||
expect(EXTENSION_JOBS['integration_test'].services).toEqual([]);
|
||||
expect([...EXTENSION_JOBS['integration_test'].requiredServices!].sort()).toEqual([
|
||||
'cache',
|
||||
'db',
|
||||
'queue',
|
||||
'search',
|
||||
]);
|
||||
});
|
||||
|
||||
it('leaves the non-service jobs with empty defaults', () => {
|
||||
for (const name of ['unit-test-extension', 'compile-extension', 'coding-standard']) {
|
||||
expect(EXTENSION_JOBS[name].services).toEqual([]);
|
||||
expect(EXTENSION_JOBS[name].requiredServices).toBeUndefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveExtensionConfig', () => {
|
||||
it('emits every known job', () => {
|
||||
const resolved = resolveExtensionConfig({}, MATRIX);
|
||||
expect(Object.keys(resolved).sort()).toEqual([
|
||||
'coding-standard',
|
||||
'compile-extension',
|
||||
'integration_test',
|
||||
'unit-test-extension',
|
||||
]);
|
||||
});
|
||||
|
||||
it('emits services={} for the non-service jobs', () => {
|
||||
const resolved = resolveExtensionConfig({}, MATRIX);
|
||||
for (const name of ['unit-test-extension', 'compile-extension', 'coding-standard']) {
|
||||
expect(resolved[name].matrix.include[0].services).toEqual({});
|
||||
}
|
||||
});
|
||||
|
||||
it('integration_test includes mysql/search/queue/cache but NOT nginx/php-fpm', () => {
|
||||
const resolved = resolveExtensionConfig({}, MATRIX);
|
||||
expect(Object.keys(resolved['integration_test'].matrix.include[0].services!).sort()).toEqual([
|
||||
'mysql',
|
||||
'opensearch',
|
||||
'rabbitmq',
|
||||
'valkey',
|
||||
]);
|
||||
});
|
||||
|
||||
it('keeps integration_test required tiers even when caller overrides services to []', () => {
|
||||
const resolved = resolveExtensionConfig(
|
||||
{ jobs: { integration_test: { services: [] } } },
|
||||
MATRIX,
|
||||
);
|
||||
expect(Object.keys(resolved['integration_test'].matrix.include[0].services!).sort()).toEqual([
|
||||
'mysql',
|
||||
'opensearch',
|
||||
'rabbitmq',
|
||||
'valkey',
|
||||
]);
|
||||
});
|
||||
|
||||
it('throws on a typo in the job name', () => {
|
||||
expect(() => resolveExtensionConfig({ jobs: { 'inteegration_test': false } }, MATRIX)).toThrowError(
|
||||
/unknown job "inteegration_test" for kind "extension"/
|
||||
);
|
||||
});
|
||||
|
||||
it('throws when a store-only job name is used', () => {
|
||||
expect(() => resolveExtensionConfig({ jobs: { 'smoke-test': false } }, MATRIX)).toThrowError(
|
||||
/unknown job "smoke-test" for kind "extension"/
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
import { resolveJobs } from '../parse';
|
||||
import { JobDefaults, Matrix, RawConfig, ResolvedConfig } from '../types';
|
||||
|
||||
/**
|
||||
* Per-job defaults for the `check-extension.yaml` reusable workflow.
|
||||
* Edit this map when a job is added, removed, or renamed in that
|
||||
* workflow — keys are validated against caller config and the values
|
||||
* supply the default tier list used when the caller doesn't override
|
||||
* `services` themselves.
|
||||
*/
|
||||
export const EXTENSION_JOBS: Record<string, JobDefaults> = {
|
||||
'unit-test-extension': { services: [] },
|
||||
'compile-extension': { services: [] },
|
||||
'coding-standard': { services: [] },
|
||||
'integration_test': {
|
||||
services: [],
|
||||
requiredServices: ['db', 'search', 'queue', 'cache'],
|
||||
},
|
||||
};
|
||||
|
||||
export const KNOWN_JOBS_EXTENSION: readonly string[] = Object.keys(EXTENSION_JOBS);
|
||||
|
||||
/**
|
||||
* Resolves a parsed config file + supported-version matrix against
|
||||
* the check-extension job list. Thin wrapper that binds the kind and
|
||||
* the per-job defaults so callers don't repeat the wiring.
|
||||
*/
|
||||
export const resolveExtensionConfig = (raw: RawConfig, matrix: Matrix): ResolvedConfig =>
|
||||
resolveJobs(raw, 'extension', EXTENSION_JOBS, matrix);
|
||||
@@ -0,0 +1,122 @@
|
||||
import { KNOWN_JOBS_STORE, resolveStoreConfig, STORE_JOBS } from './store';
|
||||
import { Matrix } from '../types';
|
||||
|
||||
const MATRIX: Matrix = {
|
||||
include: [{
|
||||
php: '8.3',
|
||||
services: {
|
||||
mysql: { image: 'mysql:8' },
|
||||
opensearch: { image: 'opensearchproject/opensearch:2' },
|
||||
rabbitmq: { image: 'rabbitmq:3' },
|
||||
valkey: { image: 'valkey:8' },
|
||||
nginx: { image: 'nginx:1.27' },
|
||||
'php-fpm': { image: 'php:8.3-fpm' },
|
||||
},
|
||||
}],
|
||||
};
|
||||
|
||||
describe('STORE_JOBS', () => {
|
||||
it('declares the check-store jobs', () => {
|
||||
expect(Object.keys(STORE_JOBS).sort()).toEqual(['coding-standard', 'smoke-test', 'unit-test']);
|
||||
});
|
||||
|
||||
it('declares smoke-test required tiers (end-user cannot toggle)', () => {
|
||||
expect(STORE_JOBS['smoke-test'].services).toEqual([]);
|
||||
expect([...STORE_JOBS['smoke-test'].requiredServices!].sort()).toEqual([
|
||||
'cache',
|
||||
'db',
|
||||
'queue',
|
||||
'search',
|
||||
'web',
|
||||
]);
|
||||
});
|
||||
|
||||
it('defaults smoke-test to the page probe only (graphql is opt-in)', () => {
|
||||
expect(STORE_JOBS['smoke-test'].probes).toEqual(['page']);
|
||||
});
|
||||
|
||||
it('exposes empty service defaults for unit-test and coding-standard', () => {
|
||||
expect(STORE_JOBS['unit-test'].services).toEqual([]);
|
||||
expect(STORE_JOBS['coding-standard'].services).toEqual([]);
|
||||
});
|
||||
|
||||
it('keeps KNOWN_JOBS_STORE in sync with the map keys', () => {
|
||||
expect([...KNOWN_JOBS_STORE].sort()).toEqual(Object.keys(STORE_JOBS).sort());
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveStoreConfig', () => {
|
||||
it('emits every known job with default tier expansion, always including mysql for smoke-test', () => {
|
||||
const resolved = resolveStoreConfig({}, MATRIX);
|
||||
expect(Object.keys(resolved).sort()).toEqual(['coding-standard', 'smoke-test', 'unit-test']);
|
||||
expect(resolved['unit-test'].matrix.include[0].services).toEqual({});
|
||||
expect(Object.keys(resolved['smoke-test'].matrix.include[0].services!).sort()).toEqual([
|
||||
'mysql',
|
||||
'nginx',
|
||||
'opensearch',
|
||||
'php-fpm',
|
||||
'rabbitmq',
|
||||
'valkey',
|
||||
]);
|
||||
});
|
||||
|
||||
it('keeps every required service even when caller overrides smoke-test services to []', () => {
|
||||
const resolved = resolveStoreConfig(
|
||||
{ jobs: { 'smoke-test': { services: [] } } },
|
||||
MATRIX,
|
||||
);
|
||||
expect(Object.keys(resolved['smoke-test'].matrix.include[0].services!).sort()).toEqual([
|
||||
'mysql',
|
||||
'nginx',
|
||||
'opensearch',
|
||||
'php-fpm',
|
||||
'rabbitmq',
|
||||
'valkey',
|
||||
]);
|
||||
});
|
||||
|
||||
it('honors enabled=false for a job', () => {
|
||||
const resolved = resolveStoreConfig(
|
||||
{ jobs: { 'smoke-test': false } },
|
||||
MATRIX,
|
||||
);
|
||||
expect(resolved['smoke-test'].enabled).toBe(false);
|
||||
});
|
||||
|
||||
it('emits the default page-only probe list for smoke-test', () => {
|
||||
const resolved = resolveStoreConfig({}, MATRIX);
|
||||
expect(resolved['smoke-test'].probes).toEqual(['page']);
|
||||
});
|
||||
|
||||
it('honors a smoke-test probes override', () => {
|
||||
const resolved = resolveStoreConfig(
|
||||
{ jobs: { 'smoke-test': { probes: ['page', 'graphql'] } } },
|
||||
MATRIX,
|
||||
);
|
||||
expect(resolved['smoke-test'].probes).toEqual(['page', 'graphql']);
|
||||
});
|
||||
|
||||
it('does not emit probes on jobs without a probe concept', () => {
|
||||
const resolved = resolveStoreConfig({}, MATRIX);
|
||||
expect(resolved['unit-test'].probes).toBeUndefined();
|
||||
expect(resolved['coding-standard'].probes).toBeUndefined();
|
||||
});
|
||||
|
||||
it('rejects probes on a job that does not support it', () => {
|
||||
expect(() => resolveStoreConfig({ jobs: { 'unit-test': { probes: ['page'] } } }, MATRIX)).toThrowError(
|
||||
/job "unit-test" does not support "probes"/
|
||||
);
|
||||
});
|
||||
|
||||
it('throws on a typo in the job name', () => {
|
||||
expect(() => resolveStoreConfig({ jobs: { 'smkoe-test': false } }, MATRIX)).toThrowError(
|
||||
/unknown job "smkoe-test" for kind "store"/
|
||||
);
|
||||
});
|
||||
|
||||
it('throws when an extension-only job name is used', () => {
|
||||
expect(() => resolveStoreConfig({ jobs: { 'unit-test-extension': false } }, MATRIX)).toThrowError(
|
||||
/unknown job "unit-test-extension" for kind "store"/
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
import { resolveJobs } from '../parse';
|
||||
import { JobDefaults, Matrix, RawConfig, ResolvedConfig } from '../types';
|
||||
|
||||
/**
|
||||
* Per-job defaults for the `check-store.yaml` reusable workflow.
|
||||
* Edit this map when a job is added, removed, or renamed in that
|
||||
* workflow — keys are validated against caller config and the values
|
||||
* supply the default tier list used when the caller doesn't override
|
||||
* `services` themselves.
|
||||
*/
|
||||
export const STORE_JOBS: Record<string, JobDefaults> = {
|
||||
'unit-test': { services: [] },
|
||||
'coding-standard': { services: [] },
|
||||
'smoke-test': {
|
||||
services: [],
|
||||
requiredServices: ['db', 'search', 'queue', 'cache', 'web'],
|
||||
probes: ['page'],
|
||||
},
|
||||
};
|
||||
|
||||
export const KNOWN_JOBS_STORE: readonly string[] = Object.keys(STORE_JOBS);
|
||||
|
||||
/**
|
||||
* Resolves a parsed config file + supported-version matrix against
|
||||
* the check-store job list. Thin wrapper that binds the kind and the
|
||||
* per-job defaults so callers don't repeat the wiring.
|
||||
*/
|
||||
export const resolveStoreConfig = (raw: RawConfig, matrix: Matrix): ResolvedConfig =>
|
||||
resolveJobs(raw, 'store', STORE_JOBS, matrix);
|
||||
@@ -0,0 +1,367 @@
|
||||
import {
|
||||
filterEntryServices,
|
||||
filterMatrixForJob,
|
||||
mergeRequiredTiers,
|
||||
normalizeJobEntry,
|
||||
normalizeProbes,
|
||||
parseMatrixInput,
|
||||
parseRawConfig,
|
||||
resolveJobs,
|
||||
} from './parse';
|
||||
import { JobDefaults, Matrix } from './types';
|
||||
|
||||
const FULL_SERVICES = {
|
||||
mysql: { image: 'mysql:8' },
|
||||
opensearch: { image: 'opensearchproject/opensearch:2' },
|
||||
rabbitmq: { image: 'rabbitmq:3' },
|
||||
valkey: { image: 'valkey:8' },
|
||||
nginx: { image: 'nginx:1.27' },
|
||||
'php-fpm': { image: 'php:8.3-fpm' },
|
||||
};
|
||||
|
||||
const MATRIX: Matrix = {
|
||||
include: [{ php: '8.3', services: { ...FULL_SERVICES } }],
|
||||
};
|
||||
|
||||
const noDefaults: JobDefaults = { services: [] };
|
||||
const smokeDefaults: JobDefaults = { services: ['search', 'queue', 'cache', 'web'] };
|
||||
const probeDefaults: JobDefaults = { services: [], probes: ['page'] };
|
||||
|
||||
describe('normalizeJobEntry', () => {
|
||||
it('defaults enabled=true and uses the default tiers when entry is undefined', () => {
|
||||
expect(normalizeJobEntry('smoke-test', undefined, smokeDefaults)).toEqual({
|
||||
enabled: true,
|
||||
tiers: smokeDefaults.services,
|
||||
});
|
||||
});
|
||||
|
||||
it('treats true shorthand as enabled with defaults', () => {
|
||||
expect(normalizeJobEntry('smoke-test', true, smokeDefaults)).toEqual({
|
||||
enabled: true,
|
||||
tiers: smokeDefaults.services,
|
||||
});
|
||||
});
|
||||
|
||||
it('treats false shorthand as disabled with defaults', () => {
|
||||
expect(normalizeJobEntry('smoke-test', false, smokeDefaults)).toEqual({
|
||||
enabled: false,
|
||||
tiers: smokeDefaults.services,
|
||||
});
|
||||
});
|
||||
|
||||
it('empty object is enabled with defaults', () => {
|
||||
expect(normalizeJobEntry('smoke-test', {}, smokeDefaults)).toEqual({
|
||||
enabled: true,
|
||||
tiers: smokeDefaults.services,
|
||||
});
|
||||
});
|
||||
|
||||
it('preserves enabled when explicitly set', () => {
|
||||
expect(normalizeJobEntry('smoke-test', { enabled: false }, smokeDefaults)).toEqual({
|
||||
enabled: false,
|
||||
tiers: smokeDefaults.services,
|
||||
});
|
||||
});
|
||||
|
||||
it('overrides the default tiers when services is set', () => {
|
||||
expect(normalizeJobEntry('smoke-test', { services: ['cache', 'web'] }, smokeDefaults)).toEqual({
|
||||
enabled: true,
|
||||
tiers: ['cache', 'web'],
|
||||
});
|
||||
});
|
||||
|
||||
it('accepts an empty services array as "no services"', () => {
|
||||
expect(normalizeJobEntry('smoke-test', { services: [] }, smokeDefaults)).toEqual({
|
||||
enabled: true,
|
||||
tiers: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('throws when entry is a non-array primitive other than boolean', () => {
|
||||
expect(() => normalizeJobEntry('unit-test', 'true' as never, noDefaults)).toThrowError(
|
||||
/must be a boolean or an object/
|
||||
);
|
||||
});
|
||||
|
||||
it('throws when entry is an array', () => {
|
||||
expect(() => normalizeJobEntry('unit-test', [] as never, noDefaults)).toThrowError(/got array/);
|
||||
});
|
||||
|
||||
it('throws when services is not an array', () => {
|
||||
expect(() => normalizeJobEntry('smoke-test', { services: 'search' } as never, smokeDefaults)).toThrowError(
|
||||
/services must be an array of tier names/
|
||||
);
|
||||
});
|
||||
|
||||
it('throws when services contains an unknown tier', () => {
|
||||
expect(() => normalizeJobEntry('smoke-test', { services: ['llm'] } as never, smokeDefaults)).toThrowError(
|
||||
/services contains unknown tier "llm"/
|
||||
);
|
||||
});
|
||||
|
||||
it('carries the default probes when the entry omits them', () => {
|
||||
expect(normalizeJobEntry('smoke-test', { services: [] }, probeDefaults)).toEqual({
|
||||
enabled: true,
|
||||
tiers: [],
|
||||
probes: ['page'],
|
||||
});
|
||||
});
|
||||
|
||||
it('carries the default probes for the boolean shorthand', () => {
|
||||
expect(normalizeJobEntry('smoke-test', true, probeDefaults)).toEqual({
|
||||
enabled: true,
|
||||
tiers: [],
|
||||
probes: ['page'],
|
||||
});
|
||||
});
|
||||
|
||||
it('overrides the default probes when probes is set', () => {
|
||||
expect(normalizeJobEntry('smoke-test', { probes: ['page', 'graphql'] }, probeDefaults)).toEqual({
|
||||
enabled: true,
|
||||
tiers: [],
|
||||
probes: ['page', 'graphql'],
|
||||
});
|
||||
});
|
||||
|
||||
it('omits probes for a job that declares no probe defaults', () => {
|
||||
expect(normalizeJobEntry('unit-test', { services: [] }, noDefaults).probes).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('normalizeProbes', () => {
|
||||
it('returns the defaults when probes is omitted', () => {
|
||||
expect(normalizeProbes('smoke-test', undefined, ['page'])).toEqual(['page']);
|
||||
});
|
||||
|
||||
it('returns undefined for a job with no probe defaults when omitted', () => {
|
||||
expect(normalizeProbes('unit-test', undefined, undefined)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('throws when probes is set on a job that does not support it', () => {
|
||||
expect(() => normalizeProbes('unit-test', ['page'], undefined)).toThrowError(
|
||||
/job "unit-test" does not support "probes"/
|
||||
);
|
||||
});
|
||||
|
||||
it('throws when probes is not an array', () => {
|
||||
expect(() => normalizeProbes('smoke-test', 'page', ['page'])).toThrowError(
|
||||
/probes must be an array of probe names/
|
||||
);
|
||||
});
|
||||
|
||||
it('throws when probes contains an unknown probe', () => {
|
||||
expect(() => normalizeProbes('smoke-test', ['rest'], ['page'])).toThrowError(
|
||||
/probes contains unknown probe "rest"/
|
||||
);
|
||||
});
|
||||
|
||||
it('accepts an empty probes array', () => {
|
||||
expect(normalizeProbes('smoke-test', [], ['page'])).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mergeRequiredTiers', () => {
|
||||
it('returns the input list when required is undefined', () => {
|
||||
expect(mergeRequiredTiers(['cache'], undefined)).toEqual(['cache']);
|
||||
});
|
||||
|
||||
it('returns the input list when required is empty', () => {
|
||||
expect(mergeRequiredTiers(['cache'], [])).toEqual(['cache']);
|
||||
});
|
||||
|
||||
it('prepends required tiers ahead of the input tiers', () => {
|
||||
expect(mergeRequiredTiers(['cache', 'web'], ['db'])).toEqual(['db', 'cache', 'web']);
|
||||
});
|
||||
|
||||
it('deduplicates when a required tier already appears in the input', () => {
|
||||
expect(mergeRequiredTiers(['db', 'cache'], ['db'])).toEqual(['db', 'cache']);
|
||||
});
|
||||
|
||||
it('deduplicates within required itself', () => {
|
||||
expect(mergeRequiredTiers(['cache'], ['db', 'db'])).toEqual(['db', 'cache']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('filterEntryServices', () => {
|
||||
it('returns services={} for an empty tier list', () => {
|
||||
const out = filterEntryServices({ php: '8.3', services: FULL_SERVICES }, []);
|
||||
expect(out.services).toEqual({});
|
||||
expect(out.php).toBe('8.3');
|
||||
});
|
||||
|
||||
it('keeps only services in the requested tiers', () => {
|
||||
const out = filterEntryServices({ php: '8.3', services: FULL_SERVICES }, ['cache', 'web']);
|
||||
expect(Object.keys(out.services!).sort()).toEqual(['nginx', 'php-fpm', 'valkey']);
|
||||
});
|
||||
|
||||
it('drops services that the matrix doesn\'t carry (elasticsearch absent)', () => {
|
||||
const out = filterEntryServices({ services: { opensearch: FULL_SERVICES.opensearch } }, ['search']);
|
||||
expect(Object.keys(out.services!)).toEqual(['opensearch']);
|
||||
});
|
||||
|
||||
it('emits services={} when the entry has no services map', () => {
|
||||
const out = filterEntryServices({ php: '8.3' }, ['cache']);
|
||||
expect(out.services).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('filterMatrixForJob', () => {
|
||||
it('preserves matrix shape, mapping every entry through filterEntryServices', () => {
|
||||
const out = filterMatrixForJob(MATRIX, ['queue']);
|
||||
expect(out.include).toHaveLength(1);
|
||||
expect(Object.keys(out.include[0].services!)).toEqual(['rabbitmq']);
|
||||
});
|
||||
|
||||
it('passes through unrelated top-level matrix keys', () => {
|
||||
const out = filterMatrixForJob({ ...MATRIX, magento: ['2.4.7'] } as Matrix, []);
|
||||
expect((out as Matrix).magento).toEqual(['2.4.7']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveJobs', () => {
|
||||
const jobs: Record<string, JobDefaults> = {
|
||||
'unit-test': noDefaults,
|
||||
'smoke-test': smokeDefaults,
|
||||
};
|
||||
|
||||
it('emits every known job, defaulted-enabled, when raw is empty', () => {
|
||||
const out = resolveJobs({}, 'store', jobs, MATRIX);
|
||||
expect(Object.keys(out).sort()).toEqual(['smoke-test', 'unit-test']);
|
||||
expect(out['unit-test'].enabled).toBe(true);
|
||||
expect(out['smoke-test'].enabled).toBe(true);
|
||||
});
|
||||
|
||||
it('emits services={} on entries for a no-default job', () => {
|
||||
const out = resolveJobs({}, 'store', jobs, MATRIX);
|
||||
expect(out['unit-test'].matrix.include[0].services).toEqual({});
|
||||
});
|
||||
|
||||
it('expands the smoke-test default tiers across the matrix entry', () => {
|
||||
const out = resolveJobs({}, 'store', jobs, MATRIX);
|
||||
expect(Object.keys(out['smoke-test'].matrix.include[0].services!).sort()).toEqual([
|
||||
'nginx',
|
||||
'opensearch',
|
||||
'php-fpm',
|
||||
'rabbitmq',
|
||||
'valkey',
|
||||
]);
|
||||
});
|
||||
|
||||
it('applies a caller-supplied services override', () => {
|
||||
const out = resolveJobs(
|
||||
{ jobs: { 'smoke-test': { services: ['cache'] } } },
|
||||
'store',
|
||||
jobs,
|
||||
MATRIX,
|
||||
);
|
||||
expect(Object.keys(out['smoke-test'].matrix.include[0].services!)).toEqual(['valkey']);
|
||||
});
|
||||
|
||||
it('always merges requiredServices into the matrix even when caller overrides services', () => {
|
||||
const withRequired: Record<string, JobDefaults> = {
|
||||
'smoke-test': { ...smokeDefaults, requiredServices: ['db'] },
|
||||
};
|
||||
const out = resolveJobs(
|
||||
{ jobs: { 'smoke-test': { services: ['cache'] } } },
|
||||
'store',
|
||||
withRequired,
|
||||
MATRIX,
|
||||
);
|
||||
expect(Object.keys(out['smoke-test'].matrix.include[0].services!).sort()).toEqual(['mysql', 'valkey']);
|
||||
});
|
||||
|
||||
it('keeps requiredServices even when caller overrides services to []', () => {
|
||||
const withRequired: Record<string, JobDefaults> = {
|
||||
'smoke-test': { ...smokeDefaults, requiredServices: ['db'] },
|
||||
};
|
||||
const out = resolveJobs(
|
||||
{ jobs: { 'smoke-test': { services: [] } } },
|
||||
'store',
|
||||
withRequired,
|
||||
MATRIX,
|
||||
);
|
||||
expect(Object.keys(out['smoke-test'].matrix.include[0].services!)).toEqual(['mysql']);
|
||||
});
|
||||
|
||||
it('honors caller enabled=false but still emits a filtered matrix', () => {
|
||||
const out = resolveJobs(
|
||||
{ jobs: { 'smoke-test': false } },
|
||||
'store',
|
||||
jobs,
|
||||
MATRIX,
|
||||
);
|
||||
expect(out['smoke-test'].enabled).toBe(false);
|
||||
expect(out['smoke-test'].matrix.include[0].services).toBeDefined();
|
||||
});
|
||||
|
||||
it('throws on unknown job names with the kind in the message', () => {
|
||||
expect(() => resolveJobs({ jobs: { taco: false } }, 'store', jobs, MATRIX)).toThrowError(
|
||||
/unknown job "taco" for kind "store"/
|
||||
);
|
||||
});
|
||||
|
||||
it('throws when `jobs` is not an object', () => {
|
||||
expect(() => resolveJobs({ jobs: 'oops' } as never, 'store', jobs, MATRIX)).toThrowError(
|
||||
/`jobs` must be an object/
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseRawConfig', () => {
|
||||
it('returns an empty object for empty input', () => {
|
||||
expect(parseRawConfig('')).toEqual({});
|
||||
});
|
||||
|
||||
it('returns an empty object for whitespace input', () => {
|
||||
expect(parseRawConfig(' \n ')).toEqual({});
|
||||
});
|
||||
|
||||
it('parses a valid object', () => {
|
||||
expect(parseRawConfig('{"jobs": {"unit-test": true}}')).toEqual({
|
||||
jobs: { 'unit-test': true },
|
||||
});
|
||||
});
|
||||
|
||||
it('throws on syntactically invalid JSON', () => {
|
||||
expect(() => parseRawConfig('{not json}')).toThrowError(/failed to parse JSON/);
|
||||
});
|
||||
|
||||
it('throws when top level is an array', () => {
|
||||
expect(() => parseRawConfig('[]')).toThrowError(/top-level value must be an object/);
|
||||
});
|
||||
|
||||
it('throws when top level is a primitive', () => {
|
||||
expect(() => parseRawConfig('"hello"')).toThrowError(/top-level value must be an object/);
|
||||
});
|
||||
|
||||
it('throws when top level is null', () => {
|
||||
expect(() => parseRawConfig('null')).toThrowError(/top-level value must be an object/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseMatrixInput', () => {
|
||||
it('parses a valid matrix', () => {
|
||||
const out = parseMatrixInput('{"include": [{"php": "8.3"}]}');
|
||||
expect(out.include).toEqual([{ php: '8.3' }]);
|
||||
});
|
||||
|
||||
it('throws on empty input', () => {
|
||||
expect(() => parseMatrixInput('')).toThrowError(/`matrix` input is required/);
|
||||
});
|
||||
|
||||
it('throws on invalid JSON', () => {
|
||||
expect(() => parseMatrixInput('{nope}')).toThrowError(/failed to parse `matrix` input/);
|
||||
});
|
||||
|
||||
it('throws when top level is an array', () => {
|
||||
expect(() => parseMatrixInput('[]')).toThrowError(/`matrix` must be a JSON object/);
|
||||
});
|
||||
|
||||
it('throws when include is missing', () => {
|
||||
expect(() => parseMatrixInput('{}')).toThrowError(/`matrix.include` must be an array/);
|
||||
});
|
||||
|
||||
it('throws when include is not an array', () => {
|
||||
expect(() => parseMatrixInput('{"include": "nope"}')).toThrowError(/`matrix.include` must be an array/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,211 @@
|
||||
import { JobDefaults, Kind, Matrix, MatrixEntry, RawConfig, RawJobConfig, ResolvedConfig, ResolvedJobConfig, Services } from './types';
|
||||
import { isTier, servicesForTiers, Tier } from './tier-map';
|
||||
import { isProbe, Probe } from './probe';
|
||||
|
||||
/**
|
||||
* Normalizes the `probes` value from a job entry. Returns the
|
||||
* caller's list when present (validated), the job's default probe
|
||||
* list when omitted, or `undefined` for jobs that have no probe
|
||||
* concept. Throws if a job without probe defaults is given `probes`.
|
||||
*/
|
||||
export const normalizeProbes = (
|
||||
jobName: string,
|
||||
raw: unknown,
|
||||
defaults: readonly Probe[] | undefined,
|
||||
): readonly Probe[] | undefined => {
|
||||
if (raw === undefined) {
|
||||
return defaults;
|
||||
}
|
||||
if (defaults === undefined) {
|
||||
throw new Error(`check-config: job "${jobName}" does not support "probes"`);
|
||||
}
|
||||
if (!Array.isArray(raw)) {
|
||||
throw new Error(`check-config: job "${jobName}".probes must be an array of probe names`);
|
||||
}
|
||||
const probes: Probe[] = [];
|
||||
for (const value of raw) {
|
||||
if (!isProbe(value)) {
|
||||
throw new Error(`check-config: job "${jobName}".probes contains unknown probe "${String(value)}"`);
|
||||
}
|
||||
probes.push(value);
|
||||
}
|
||||
return probes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes a single raw job entry to (enabled, tiers, probes).
|
||||
* Accepts the boolean shorthand and the object form. Validates the
|
||||
* shape, the `services` tier list, and the `probes` list; throws on
|
||||
* unexpected input. The caller supplies the per-job defaults, used
|
||||
* when `services`/`probes` are omitted from the entry. `probes` is
|
||||
* `undefined` for jobs that declare no probe defaults.
|
||||
*/
|
||||
export const normalizeJobEntry = (
|
||||
jobName: string,
|
||||
raw: RawJobConfig | undefined,
|
||||
defaults: JobDefaults,
|
||||
): { enabled: boolean; tiers: readonly Tier[]; probes?: readonly Probe[] } => {
|
||||
if (raw === undefined) {
|
||||
return { enabled: true, tiers: defaults.services, probes: defaults.probes };
|
||||
}
|
||||
if (typeof raw === 'boolean') {
|
||||
return { enabled: raw, tiers: defaults.services, probes: defaults.probes };
|
||||
}
|
||||
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
|
||||
throw new Error(
|
||||
`check-config: job "${jobName}" must be a boolean or an object (got ${Array.isArray(raw) ? 'array' : typeof raw})`
|
||||
);
|
||||
}
|
||||
const { enabled, services, probes } = raw as { enabled?: unknown; services?: unknown; probes?: unknown };
|
||||
const enabledValue = enabled === undefined ? true : Boolean(enabled);
|
||||
const resolvedProbes = normalizeProbes(jobName, probes, defaults.probes);
|
||||
|
||||
if (services === undefined) {
|
||||
return { enabled: enabledValue, tiers: defaults.services, probes: resolvedProbes };
|
||||
}
|
||||
if (!Array.isArray(services)) {
|
||||
throw new Error(`check-config: job "${jobName}".services must be an array of tier names`);
|
||||
}
|
||||
const tiers: Tier[] = [];
|
||||
for (const value of services) {
|
||||
if (!isTier(value)) {
|
||||
throw new Error(`check-config: job "${jobName}".services contains unknown tier "${String(value)}"`);
|
||||
}
|
||||
tiers.push(value);
|
||||
}
|
||||
return { enabled: enabledValue, tiers, probes: resolvedProbes };
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges a job's `requiredServices` into the resolved tier list,
|
||||
* deduplicating while preserving order (required tiers first, then
|
||||
* the caller/default tiers in their original order).
|
||||
*/
|
||||
export const mergeRequiredTiers = (
|
||||
tiers: readonly Tier[],
|
||||
required: readonly Tier[] | undefined,
|
||||
): readonly Tier[] => {
|
||||
if (!required || required.length === 0) return tiers;
|
||||
const seen = new Set<Tier>();
|
||||
const merged: Tier[] = [];
|
||||
for (const tier of required) {
|
||||
if (!seen.has(tier)) { seen.add(tier); merged.push(tier); }
|
||||
}
|
||||
for (const tier of tiers) {
|
||||
if (!seen.has(tier)) { seen.add(tier); merged.push(tier); }
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy of `entry` with `services` filtered to the concrete
|
||||
* names produced by expanding `tiers` through the tier-map. An empty
|
||||
* tier list yields `services: {}`.
|
||||
*/
|
||||
export const filterEntryServices = (entry: MatrixEntry, tiers: readonly Tier[]): MatrixEntry => {
|
||||
const keep = servicesForTiers(tiers);
|
||||
const original = entry.services ?? {};
|
||||
const filtered: Services = {};
|
||||
for (const [name, config] of Object.entries(original)) {
|
||||
if (keep.has(name)) filtered[name] = config;
|
||||
}
|
||||
return { ...entry, services: filtered };
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-job filter applied to the supported-version matrix: returns a
|
||||
* shallow clone with every entry's `services` narrowed to the tiers
|
||||
* the job needs.
|
||||
*/
|
||||
export const filterMatrixForJob = (matrix: Matrix, tiers: readonly Tier[]): Matrix => ({
|
||||
...matrix,
|
||||
include: matrix.include.map(entry => filterEntryServices(entry, tiers)),
|
||||
});
|
||||
|
||||
/**
|
||||
* Shared per-kind resolver: walks the per-kind job map and emits one
|
||||
* `ResolvedJobConfig` per known job. Caller-supplied jobs override
|
||||
* the defaults; jobs the caller omitted still appear, carrying the
|
||||
* default `enabled: true` and the default tier list. Rejects unknown
|
||||
* job names from the config so typos surface in CI.
|
||||
*/
|
||||
export const resolveJobs = (
|
||||
raw: RawConfig,
|
||||
kind: Kind,
|
||||
jobs: Record<string, JobDefaults>,
|
||||
matrix: Matrix,
|
||||
): ResolvedConfig => {
|
||||
const rawJobs = raw.jobs ?? {};
|
||||
if (rawJobs === null || typeof rawJobs !== 'object' || Array.isArray(rawJobs)) {
|
||||
throw new Error(`check-config: \`jobs\` must be an object`);
|
||||
}
|
||||
for (const name of Object.keys(rawJobs)) {
|
||||
if (!(name in jobs)) {
|
||||
throw new Error(
|
||||
`check-config: unknown job "${name}" for kind "${kind}". Known jobs: ${Object.keys(jobs).join(', ')}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const resolved: ResolvedConfig = {};
|
||||
for (const [name, defaults] of Object.entries(jobs)) {
|
||||
const entry = (rawJobs as Record<string, RawJobConfig>)[name];
|
||||
const { enabled, tiers, probes } = normalizeJobEntry(name, entry, defaults);
|
||||
const finalTiers = mergeRequiredTiers(tiers, defaults.requiredServices);
|
||||
const resolvedEntry: ResolvedJobConfig = {
|
||||
enabled,
|
||||
matrix: filterMatrixForJob(matrix, finalTiers),
|
||||
};
|
||||
if (probes !== undefined) {
|
||||
resolvedEntry.probes = [...probes];
|
||||
}
|
||||
resolved[name] = resolvedEntry;
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a JSON string into a RawConfig with shape validation
|
||||
* (must be an object, not an array or primitive). Empty/whitespace
|
||||
* input yields an empty config.
|
||||
*/
|
||||
export const parseRawConfig = (jsonText: string): RawConfig => {
|
||||
const trimmed = jsonText.trim();
|
||||
if (trimmed === '') return {};
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(trimmed);
|
||||
} catch (e) {
|
||||
throw new Error(`check-config: failed to parse JSON: ${(e as Error).message}`);
|
||||
}
|
||||
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
||||
throw new Error(`check-config: top-level value must be an object`);
|
||||
}
|
||||
return parsed as RawConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the `matrix` action input. Validates the top-level shape
|
||||
* (must be an object with an `include` array) so a malformed input
|
||||
* fails with a clear message at the boundary.
|
||||
*/
|
||||
export const parseMatrixInput = (jsonText: string): Matrix => {
|
||||
const trimmed = jsonText.trim();
|
||||
if (trimmed === '') {
|
||||
throw new Error('check-config: `matrix` input is required');
|
||||
}
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(trimmed);
|
||||
} catch (e) {
|
||||
throw new Error(`check-config: failed to parse \`matrix\` input as JSON: ${(e as Error).message}`);
|
||||
}
|
||||
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
||||
throw new Error('check-config: `matrix` must be a JSON object');
|
||||
}
|
||||
const include = (parsed as Record<string, unknown>).include;
|
||||
if (!Array.isArray(include)) {
|
||||
throw new Error('check-config: `matrix.include` must be an array');
|
||||
}
|
||||
return parsed as Matrix;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* A smoke-test probe the check-store workflow can run against a
|
||||
* running store. `page` does a GET / and asserts a non-empty title;
|
||||
* `graphql` POSTs a storeConfig query to /graphql. Probes are opt-in
|
||||
* per job because not every edition exposes every surface (e.g. the
|
||||
* mage-os minimal edition ships no GraphQL modules, so /graphql 404s).
|
||||
*/
|
||||
export const PROBES = ['page', 'graphql'] as const;
|
||||
|
||||
export type Probe = (typeof PROBES)[number];
|
||||
|
||||
export const isProbe = (value: unknown): value is Probe =>
|
||||
typeof value === 'string' && (PROBES as readonly string[]).includes(value);
|
||||
@@ -0,0 +1,39 @@
|
||||
import { resolveConfig } from './resolve';
|
||||
import { Matrix } from './types';
|
||||
|
||||
const MATRIX: Matrix = {
|
||||
include: [{
|
||||
php: '8.3',
|
||||
services: {
|
||||
mysql: { image: 'mysql:8' },
|
||||
opensearch: { image: 'opensearchproject/opensearch:2' },
|
||||
rabbitmq: { image: 'rabbitmq:3' },
|
||||
valkey: { image: 'valkey:8' },
|
||||
nginx: { image: 'nginx:1.27' },
|
||||
'php-fpm': { image: 'php:8.3-fpm' },
|
||||
},
|
||||
}],
|
||||
};
|
||||
|
||||
describe('resolveConfig', () => {
|
||||
it('routes kind=store to the store resolver', () => {
|
||||
const resolved = resolveConfig({ jobs: { 'smoke-test': false } }, 'store', MATRIX);
|
||||
expect(resolved['smoke-test'].enabled).toBe(false);
|
||||
expect(resolved['unit-test'].enabled).toBe(true);
|
||||
});
|
||||
|
||||
it('routes kind=extension to the extension resolver', () => {
|
||||
const resolved = resolveConfig({ jobs: { 'compile-extension': false } }, 'extension', MATRIX);
|
||||
expect(resolved['compile-extension'].enabled).toBe(false);
|
||||
expect(resolved['integration_test'].enabled).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects a job name from the other kind', () => {
|
||||
expect(() => resolveConfig({ jobs: { 'smoke-test': false } }, 'extension', MATRIX)).toThrowError(
|
||||
/unknown job "smoke-test" for kind "extension"/
|
||||
);
|
||||
expect(() => resolveConfig({ jobs: { 'unit-test-extension': false } }, 'store', MATRIX)).toThrowError(
|
||||
/unknown job "unit-test-extension" for kind "store"/
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Kind, Matrix, RawConfig, ResolvedConfig } from './types';
|
||||
import { resolveStoreConfig } from './kinds/store';
|
||||
import { resolveExtensionConfig } from './kinds/extension';
|
||||
|
||||
/**
|
||||
* Dispatches to the per-kind resolver. Each kind owns its own list
|
||||
* of jobs and per-job defaults; this function just routes the call
|
||||
* and forwards the supported-version matrix.
|
||||
*/
|
||||
export const resolveConfig = (raw: RawConfig, kind: Kind, matrix: Matrix): ResolvedConfig => {
|
||||
if (kind === 'store') return resolveStoreConfig(raw, matrix);
|
||||
return resolveExtensionConfig(raw, matrix);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { isTier, servicesForTiers, TIER_TO_SERVICES } from './tier-map';
|
||||
|
||||
describe('isTier', () => {
|
||||
it('accepts every key in TIER_TO_SERVICES', () => {
|
||||
for (const tier of Object.keys(TIER_TO_SERVICES)) {
|
||||
expect(isTier(tier)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects unknown strings', () => {
|
||||
expect(isTier('llm')).toBe(false);
|
||||
expect(isTier('')).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects non-strings', () => {
|
||||
expect(isTier(42)).toBe(false);
|
||||
expect(isTier(null)).toBe(false);
|
||||
expect(isTier(undefined)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('servicesForTiers', () => {
|
||||
it('returns an empty set for an empty tier list', () => {
|
||||
expect([...servicesForTiers([])]).toEqual([]);
|
||||
});
|
||||
|
||||
it('expands a single tier to its concrete service names', () => {
|
||||
expect([...servicesForTiers(['queue'])]).toEqual(['rabbitmq']);
|
||||
});
|
||||
|
||||
it('expands the search tier to both implementations', () => {
|
||||
expect([...servicesForTiers(['search'])].sort()).toEqual(['elasticsearch', 'opensearch']);
|
||||
});
|
||||
|
||||
it('expands the web tier to nginx + php-fpm', () => {
|
||||
expect([...servicesForTiers(['web'])].sort()).toEqual(['nginx', 'php-fpm']);
|
||||
});
|
||||
|
||||
it('unions across multiple tiers', () => {
|
||||
expect([...servicesForTiers(['cache', 'queue'])].sort()).toEqual(['rabbitmq', 'redis', 'valkey']);
|
||||
});
|
||||
|
||||
it('deduplicates if the same tier appears twice', () => {
|
||||
expect([...servicesForTiers(['queue', 'queue'])]).toEqual(['rabbitmq']);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* A category of services that can be selected for a job. Mirrors the
|
||||
* `Tier` concept in `supported-version` but adds the `web` tier so the
|
||||
* smoke-test and integration jobs can opt the nginx + php-fpm pair in
|
||||
* or out as a unit. Intentionally narrower than supported-version's
|
||||
* tier set: this file only names tiers that the resolve-check-config
|
||||
* schema lets callers toggle.
|
||||
*/
|
||||
export type Tier = 'db' | 'search' | 'queue' | 'cache' | 'web';
|
||||
|
||||
/**
|
||||
* Expansion of each tier to the concrete service names that may
|
||||
* appear in a supported-version matrix entry's `services` map.
|
||||
* Filtering a matrix entry's services for a tier list means keeping
|
||||
* the keys that union to the values of the selected tiers here.
|
||||
*/
|
||||
export const TIER_TO_SERVICES: Record<Tier, readonly string[]> = {
|
||||
db: ['mysql'],
|
||||
search: ['opensearch', 'elasticsearch'],
|
||||
queue: ['rabbitmq'],
|
||||
cache: ['valkey', 'redis'],
|
||||
web: ['nginx', 'php-fpm'],
|
||||
};
|
||||
|
||||
export const isTier = (value: unknown): value is Tier =>
|
||||
typeof value === 'string' && value in TIER_TO_SERVICES;
|
||||
|
||||
/**
|
||||
* Returns the flat set of concrete service names for a list of tiers.
|
||||
* Used to filter a matrix entry's `services` map down to only the
|
||||
* containers a particular job actually needs.
|
||||
*/
|
||||
export const servicesForTiers = (tiers: readonly Tier[]): Set<string> => {
|
||||
const result = new Set<string>();
|
||||
for (const tier of tiers) {
|
||||
for (const name of TIER_TO_SERVICES[tier]) result.add(name);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import { Tier } from './tier-map';
|
||||
import { Probe } from './probe';
|
||||
|
||||
/**
|
||||
* Which reusable workflow a config belongs to. Selects the known-job
|
||||
* list used for validation and the default config path.
|
||||
*/
|
||||
export type Kind = 'store' | 'extension';
|
||||
|
||||
/**
|
||||
* A single service container definition from supported-version's
|
||||
* matrix output. We don't model the inner shape here — we just
|
||||
* preserve unknown keys when filtering.
|
||||
*/
|
||||
export interface ServiceConfig {
|
||||
image: string;
|
||||
env?: Record<string, string>;
|
||||
ports?: string[];
|
||||
options?: string;
|
||||
volumes?: string[];
|
||||
}
|
||||
|
||||
export interface Services {
|
||||
[serviceName: string]: ServiceConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* One row of supported-version's matrix. Carries the PHP/Composer/etc
|
||||
* coordinates plus the concrete `services` map this job should bring
|
||||
* up. We type the known fields supported-version emits and allow
|
||||
* extras to pass through untouched.
|
||||
*/
|
||||
export interface MatrixEntry {
|
||||
services?: Services;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* The matrix shape emitted by supported-version, suitable for
|
||||
* `strategy.matrix` in GitHub Actions.
|
||||
*/
|
||||
export interface Matrix {
|
||||
include: MatrixEntry[];
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-job static defaults declared by each kind module.
|
||||
*
|
||||
* `services` is the tier list used when the caller's config does not
|
||||
* override it — these tiers are user-toggleable through the schema.
|
||||
*
|
||||
* `requiredServices` is always merged in on top of the resolved list,
|
||||
* regardless of caller overrides. Use it for tiers a job structurally
|
||||
* cannot run without (e.g. mysql for a running store smoke-test) and
|
||||
* which therefore should not appear in the user-facing schema enum.
|
||||
*
|
||||
* `probes` is the default smoke-test probe list used when the caller
|
||||
* does not override it. Only jobs that declare it support the
|
||||
* `probes` config key; omit it for jobs that have no probe concept.
|
||||
*/
|
||||
export interface JobDefaults {
|
||||
services: readonly Tier[];
|
||||
requiredServices?: readonly Tier[];
|
||||
probes?: readonly Probe[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolved per-job output. `enabled` mirrors the input boolean (or
|
||||
* `enabled` key); `matrix` is supported-version's matrix with each
|
||||
* entry's `services` filtered to the tiers this job needs.
|
||||
*/
|
||||
export interface ResolvedJobConfig {
|
||||
enabled: boolean;
|
||||
matrix: Matrix;
|
||||
probes?: Probe[];
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map of job-name -> resolved config. Keys are exactly the job names
|
||||
* declared by the kind module (omitted-by-caller jobs still appear,
|
||||
* carrying defaults so the consumer's `if:` guard works uniformly).
|
||||
*/
|
||||
export interface ResolvedConfig {
|
||||
[jobName: string]: ResolvedJobConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shape of a single per-job entry in the user's JSON config file.
|
||||
* - `true` / `false`: shorthand for `{ enabled: true|false }`
|
||||
* - object: explicit enabled flag plus an optional tier list under
|
||||
* `services` and an optional probe list under `probes` (both
|
||||
* validated against the per-kind schema).
|
||||
*/
|
||||
export type RawJobConfig =
|
||||
| boolean
|
||||
| { enabled?: boolean; services?: string[]; probes?: string[]; [key: string]: unknown };
|
||||
|
||||
/**
|
||||
* Top-level shape of the user's JSON config file. Job toggles live
|
||||
* under `jobs`; the rest of the top level is reserved for future
|
||||
* global keys.
|
||||
*/
|
||||
export interface RawConfig {
|
||||
jobs?: { [jobName: string]: RawJobConfig };
|
||||
[key: string]: unknown;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
"typeRoots": ["../node_modules/@types"],
|
||||
"types": ["jest"]
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/sansec-ecomscan@v7.0.0 # x-release-please-version
|
||||
- uses: graycoreio/github-actions-magento2/sansec-ecomscan@v8.5.0 # x-release-please-version
|
||||
with:
|
||||
license: ${{ secrets.SANSEC_LICENSE_KEY }}
|
||||
```
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
name: A job to semantically compare two versions
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: graycoreio/github-actions-magento2/semver-compare@v7.0.0 # x-release-please-version
|
||||
- uses: graycoreio/github-actions-magento2/semver-compare@v8.5.0 # x-release-please-version
|
||||
with:
|
||||
version: 2.1.0
|
||||
compare_against: 2.2.3
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
matrix: ${{ steps.supported-version.outputs.matrix }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: graycoreio/github-actions-magento2/supported-version@v7.0.0 # x-release-please-version
|
||||
- uses: graycoreio/github-actions-magento2/supported-version@v8.5.0 # x-release-please-version
|
||||
id: supported-version
|
||||
|
||||
compile:
|
||||
@@ -40,19 +40,19 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/setup-magento@v7.0.0 # x-release-please-version
|
||||
- uses: graycoreio/github-actions-magento2/setup-magento@v8.5.0 # x-release-please-version
|
||||
id: setup-magento
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
tools: composer:v${{ matrix.composer }}
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/cache-magento@v7.0.0 # x-release-please-version
|
||||
- uses: graycoreio/github-actions-magento2/cache-magento@v8.5.0 # x-release-please-version
|
||||
|
||||
- run: composer install
|
||||
env:
|
||||
COMPOSER_AUTH: ${{ secrets.COMPOSER_AUTH }}
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/setup-di-compile@v7.0.0 # x-release-please-version
|
||||
- uses: graycoreio/github-actions-magento2/setup-di-compile@v8.5.0 # x-release-please-version
|
||||
with:
|
||||
path: ${{ steps.setup-magento.outputs.path }}
|
||||
```
|
||||
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
matrix: ${{ steps.supported-version.outputs.matrix }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: graycoreio/github-actions-magento2/supported-version@v7.0.0 # x-release-please-version
|
||||
- uses: graycoreio/github-actions-magento2/supported-version@v8.5.0 # x-release-please-version
|
||||
id: supported-version
|
||||
with:
|
||||
include_services: "true"
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/setup-magento@v7.0.0 # x-release-please-version
|
||||
- uses: graycoreio/github-actions-magento2/setup-magento@v8.5.0 # x-release-please-version
|
||||
id: setup-magento
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
@@ -64,7 +64,7 @@ jobs:
|
||||
env:
|
||||
COMPOSER_AUTH: ${{ secrets.COMPOSER_AUTH }}
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/setup-install@v7.0.0 # x-release-please-version
|
||||
- uses: graycoreio/github-actions-magento2/setup-install@v8.5.0 # x-release-please-version
|
||||
with:
|
||||
services: ${{ toJSON(matrix.services) }}
|
||||
path: ${{ steps.setup-magento.outputs.path }}
|
||||
|
||||
@@ -18,6 +18,11 @@ inputs:
|
||||
default: ""
|
||||
description: "Additional raw flags to append to the setup:install command."
|
||||
|
||||
container_id:
|
||||
required: false
|
||||
default: ""
|
||||
description: "When set, runs setup:install via `docker exec` inside the container with this `container_id` (typically the value of `job.services['php-fpm'].id`) and uses service-network aliases (mysql, redis, etc.) instead of runner-side localhost when running setup:install."
|
||||
|
||||
outputs:
|
||||
command:
|
||||
description: "The full bin/magento setup:install command that was run."
|
||||
|
||||
Vendored
+28
-28
File diff suppressed because one or more lines are too long
@@ -8,6 +8,7 @@ const BASE_ARGS = [
|
||||
'--admin-firstname=Admin',
|
||||
'--admin-lastname=User',
|
||||
'--backend-frontname=admin',
|
||||
'--no-interaction',
|
||||
];
|
||||
|
||||
const MYSQL_SERVICE = {
|
||||
@@ -60,6 +61,10 @@ describe('buildInstallArgs', () => {
|
||||
it('returns only base args when services is empty', () => {
|
||||
expect(buildInstallArgs({})).toEqual(BASE_ARGS);
|
||||
});
|
||||
|
||||
it('runs non-interactively', () => {
|
||||
expect(buildInstallArgs(null)).toContain('--no-interaction');
|
||||
});
|
||||
});
|
||||
|
||||
describe('mysql', () => {
|
||||
@@ -173,4 +178,100 @@ describe('buildInstallArgs', () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('container mode', () => {
|
||||
it('uses the mysql network alias and the internal port from ports[0]', () => {
|
||||
const services: Services = {
|
||||
mysql: { ...MYSQL_SERVICE, ports: ['33060:3306'] },
|
||||
};
|
||||
expect(buildInstallArgs(services, true)).toEqual([
|
||||
...BASE_ARGS,
|
||||
'--db-host=mysql:3306',
|
||||
'--db-name=magento_integration_tests',
|
||||
'--db-user=user',
|
||||
'--db-password=password',
|
||||
]);
|
||||
});
|
||||
|
||||
it('uses the opensearch alias and parses the internal port', () => {
|
||||
const services: Services = {
|
||||
opensearch: { image: 'opensearchproject/opensearch:2.19.1', ports: ['19200:9200'] },
|
||||
};
|
||||
expect(buildInstallArgs(services, true)).toEqual([
|
||||
...BASE_ARGS,
|
||||
'--search-engine=opensearch',
|
||||
'--opensearch-host=opensearch',
|
||||
'--opensearch-port=9200',
|
||||
]);
|
||||
});
|
||||
|
||||
it('uses the elasticsearch alias and parses the internal port', () => {
|
||||
const services: Services = {
|
||||
elasticsearch: { image: 'elasticsearch:8.11.4', ports: ['19200:9200'] },
|
||||
};
|
||||
expect(buildInstallArgs(services, true)).toEqual([
|
||||
...BASE_ARGS,
|
||||
'--search-engine=elasticsearch8',
|
||||
'--elasticsearch-host=elasticsearch',
|
||||
'--elasticsearch-port=9200',
|
||||
]);
|
||||
});
|
||||
|
||||
it('uses the rabbitmq alias and parses the internal port', () => {
|
||||
const services: Services = {
|
||||
rabbitmq: { image: 'rabbitmq:4.0-management', ports: ['15672:5672'] },
|
||||
};
|
||||
expect(buildInstallArgs(services, true)).toEqual([
|
||||
...BASE_ARGS,
|
||||
'--amqp-host=rabbitmq',
|
||||
'--amqp-port=5672',
|
||||
'--amqp-user=guest',
|
||||
'--amqp-password=guest',
|
||||
]);
|
||||
});
|
||||
|
||||
it('uses the valkey alias when valkey is the cache service', () => {
|
||||
const services: Services = {
|
||||
valkey: { image: 'valkey:8.0', ports: ['16379:6379'] },
|
||||
};
|
||||
expect(buildInstallArgs(services, true)).toEqual([
|
||||
...BASE_ARGS,
|
||||
'--session-save=redis',
|
||||
'--session-save-redis-host=valkey',
|
||||
'--session-save-redis-port=6379',
|
||||
'--cache-backend=redis',
|
||||
'--cache-backend-redis-server=valkey',
|
||||
'--cache-backend-redis-port=6379',
|
||||
]);
|
||||
});
|
||||
|
||||
it('uses the redis alias when redis is the cache service', () => {
|
||||
const services: Services = {
|
||||
redis: { image: 'redis:7.2', ports: ['16379:6379'] },
|
||||
};
|
||||
expect(buildInstallArgs(services, true)).toEqual([
|
||||
...BASE_ARGS,
|
||||
'--session-save=redis',
|
||||
'--session-save-redis-host=redis',
|
||||
'--session-save-redis-port=6379',
|
||||
'--cache-backend=redis',
|
||||
'--cache-backend-redis-server=redis',
|
||||
'--cache-backend-redis-port=6379',
|
||||
]);
|
||||
});
|
||||
|
||||
it('falls back to default internal ports when ports are absent', () => {
|
||||
const services: Services = {
|
||||
mysql: { ...MYSQL_SERVICE, ports: undefined },
|
||||
opensearch: { image: 'opensearchproject/opensearch:2.19.1' },
|
||||
rabbitmq: { image: 'rabbitmq:4.0-management' },
|
||||
valkey: { image: 'valkey:8.0' },
|
||||
};
|
||||
const args = buildInstallArgs(services, true);
|
||||
expect(args).toContain('--db-host=mysql:3306');
|
||||
expect(args).toContain('--opensearch-port=9200');
|
||||
expect(args).toContain('--amqp-port=5672');
|
||||
expect(args).toContain('--session-save-redis-port=6379');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,7 @@ export interface ServiceConfig {
|
||||
env?: Record<string, string>;
|
||||
ports?: string[];
|
||||
options?: string;
|
||||
volumes?: string[];
|
||||
}
|
||||
|
||||
export interface Services {
|
||||
@@ -12,6 +13,7 @@ export interface Services {
|
||||
rabbitmq?: ServiceConfig;
|
||||
redis?: ServiceConfig;
|
||||
valkey?: ServiceConfig;
|
||||
'php-fpm'?: ServiceConfig;
|
||||
}
|
||||
|
||||
const BASE_ARGS = [
|
||||
@@ -22,23 +24,32 @@ const BASE_ARGS = [
|
||||
'--admin-firstname=Admin',
|
||||
'--admin-lastname=User',
|
||||
'--backend-frontname=admin',
|
||||
'--no-interaction',
|
||||
];
|
||||
|
||||
const parsePort = (svc: ServiceConfig | undefined, index: 0 | 1, fallback: string): string => {
|
||||
return svc?.ports?.[0]?.split(':')[index] ?? fallback;
|
||||
};
|
||||
|
||||
export const buildMysqlPrepArgs = (mysql: ServiceConfig): string[] => {
|
||||
const rootPassword = mysql.env?.MYSQL_ROOT_PASSWORD ?? 'rootpassword';
|
||||
const port = mysql.ports?.[0]?.split(':')[0] ?? '3306';
|
||||
const port = parsePort(mysql, 0, '3306');
|
||||
return ['-h127.0.0.1', `--port=${port}`, '-uroot', `-p${rootPassword}`, '-e', 'SET GLOBAL log_bin_trust_function_creators = 1;'];
|
||||
};
|
||||
|
||||
export const buildInstallArgs = (services: Services | null): string[] => {
|
||||
export const buildInstallArgs = (services: Services | null, containerMode = false): string[] => {
|
||||
const args = [...BASE_ARGS];
|
||||
|
||||
if (!services) return args;
|
||||
|
||||
const portIdx: 0 | 1 = containerMode ? 1 : 0;
|
||||
const hostFor = (alias: string): string => containerMode ? alias : 'localhost';
|
||||
|
||||
if (services.mysql) {
|
||||
const dbPort = services.mysql.ports?.[0]?.split(':')[0] ?? '3306';
|
||||
const dbPort = parsePort(services.mysql, portIdx, '3306');
|
||||
const dbHost = containerMode ? `mysql:${dbPort}` : `127.0.0.1:${dbPort}`;
|
||||
args.push(
|
||||
`--db-host=127.0.0.1:${dbPort}`,
|
||||
`--db-host=${dbHost}`,
|
||||
`--db-name=${services.mysql.env?.MYSQL_DATABASE ?? 'magento'}`,
|
||||
`--db-user=${services.mysql.env?.MYSQL_USER ?? 'magento'}`,
|
||||
`--db-password=${services.mysql.env?.MYSQL_PASSWORD ?? 'magento'}`,
|
||||
@@ -46,37 +57,44 @@ export const buildInstallArgs = (services: Services | null): string[] => {
|
||||
}
|
||||
|
||||
if (services.opensearch) {
|
||||
const port = parsePort(services.opensearch, portIdx, '9200');
|
||||
args.push(
|
||||
'--search-engine=opensearch',
|
||||
'--opensearch-host=localhost',
|
||||
'--opensearch-port=9200',
|
||||
`--opensearch-host=${hostFor('opensearch')}`,
|
||||
`--opensearch-port=${port}`,
|
||||
);
|
||||
} else if (services.elasticsearch) {
|
||||
const majorVersion = services.elasticsearch.image.split(':')[1]?.split('.')[0];
|
||||
const port = parsePort(services.elasticsearch, portIdx, '9200');
|
||||
args.push(
|
||||
`--search-engine=elasticsearch${majorVersion}`,
|
||||
'--elasticsearch-host=localhost',
|
||||
'--elasticsearch-port=9200',
|
||||
`--elasticsearch-host=${hostFor('elasticsearch')}`,
|
||||
`--elasticsearch-port=${port}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (services.rabbitmq) {
|
||||
const port = parsePort(services.rabbitmq, portIdx, '5672');
|
||||
args.push(
|
||||
'--amqp-host=localhost',
|
||||
'--amqp-port=5672',
|
||||
`--amqp-host=${hostFor('rabbitmq')}`,
|
||||
`--amqp-port=${port}`,
|
||||
'--amqp-user=guest',
|
||||
'--amqp-password=guest',
|
||||
);
|
||||
}
|
||||
|
||||
if (services.valkey || services.redis) {
|
||||
const cacheKey: 'valkey' | 'redis' = services.valkey ? 'valkey' : 'redis';
|
||||
const cache = services[cacheKey]!;
|
||||
const port = parsePort(cache, portIdx, '6379');
|
||||
const host = hostFor(cacheKey);
|
||||
args.push(
|
||||
'--session-save=redis',
|
||||
'--session-save-redis-host=localhost',
|
||||
'--session-save-redis-port=6379',
|
||||
`--session-save-redis-host=${host}`,
|
||||
`--session-save-redis-port=${port}`,
|
||||
'--cache-backend=redis',
|
||||
'--cache-backend-redis-server=localhost',
|
||||
'--cache-backend-redis-port=6379',
|
||||
`--cache-backend-redis-server=${host}`,
|
||||
`--cache-backend-redis-port=${port}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,25 @@
|
||||
import * as core from '@actions/core';
|
||||
import * as exec from '@actions/exec';
|
||||
import * as nodePath from 'path';
|
||||
import { buildInstallArgs, buildMysqlPrepArgs, Services } from './build-command';
|
||||
|
||||
const resolveContainerPath = (runnerPath: string): string => {
|
||||
const workspace = process.env.GITHUB_WORKSPACE || '';
|
||||
const absolute = nodePath.resolve(workspace, runnerPath);
|
||||
const relative = nodePath.relative(workspace, absolute);
|
||||
if (relative.startsWith('..')) {
|
||||
throw new Error(`container_id: path ${runnerPath} resolves outside GITHUB_WORKSPACE (${workspace})`);
|
||||
}
|
||||
return relative ? `/var/www/html/${relative}` : '/var/www/html';
|
||||
};
|
||||
|
||||
export async function run(): Promise<void> {
|
||||
try {
|
||||
const servicesInput = core.getInput('services');
|
||||
const path = core.getInput('path') || '.';
|
||||
const extraArgs = core.getInput('extra_args').trim();
|
||||
const containerId = core.getInput('container_id').trim();
|
||||
const containerMode = containerId !== '';
|
||||
|
||||
let services: Services | null = null;
|
||||
if (servicesInput && servicesInput !== 'null') {
|
||||
@@ -14,20 +27,42 @@ export async function run(): Promise<void> {
|
||||
}
|
||||
|
||||
// setup:install creates MySQL triggers, which requires log_bin_trust_function_creators=1
|
||||
// when binary logging is enabled.
|
||||
// when binary logging is enabled. The prep always runs runner-side against the published port.
|
||||
if (services?.mysql) {
|
||||
await exec.exec('mysql', buildMysqlPrepArgs(services.mysql));
|
||||
}
|
||||
|
||||
const args = buildInstallArgs(services);
|
||||
const args = buildInstallArgs(services, containerMode);
|
||||
|
||||
if (extraArgs) {
|
||||
args.push(...extraArgs.split(/\s+/));
|
||||
}
|
||||
|
||||
core.setOutput('command', `php bin/magento setup:install ${args.join(' ')}`);
|
||||
if (containerMode) {
|
||||
const containerPath = resolveContainerPath(path);
|
||||
|
||||
await exec.exec('php', ['bin/magento', 'setup:install', ...args], { cwd: path });
|
||||
const command = `docker exec -w ${containerPath} ${containerId} php bin/magento setup:install ${args.join(' ')}`;
|
||||
core.setOutput('command', command);
|
||||
|
||||
await exec.exec('docker', [
|
||||
'exec',
|
||||
'-w', containerPath,
|
||||
containerId,
|
||||
'php', 'bin/magento', 'setup:install',
|
||||
...args,
|
||||
]);
|
||||
|
||||
// setup:install runs as root inside the container, but php-fpm workers
|
||||
// serve requests as `www-data`. Hand ownership of the Magento writable
|
||||
// dirs to www-data so request-time cache/log writes succeed.
|
||||
await exec.exec('docker', [
|
||||
'exec', containerId, 'sh', '-c',
|
||||
`for d in var generated pub/static pub/media; do [ -d "${containerPath}/$d" ] && chown -R www-data:www-data "${containerPath}/$d"; done`,
|
||||
]);
|
||||
} else {
|
||||
core.setOutput('command', `php bin/magento setup:install ${args.join(' ')}`);
|
||||
await exec.exec('php', ['bin/magento', 'setup:install', ...args], { cwd: path });
|
||||
}
|
||||
} catch (error) {
|
||||
core.setFailed((error as Error).message);
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/setup-magento@v7.0.0 # x-release-please-version
|
||||
- uses: graycoreio/github-actions-magento2/setup-magento@v8.5.0 # x-release-please-version
|
||||
id: setup-magento
|
||||
with:
|
||||
php-version: "8.3"
|
||||
@@ -89,7 +89,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/setup-magento@v7.0.0 # x-release-please-version
|
||||
- uses: graycoreio/github-actions-magento2/setup-magento@v8.5.0 # x-release-please-version
|
||||
id: setup-magento
|
||||
with:
|
||||
php-version: "8.3"
|
||||
|
||||
@@ -95,7 +95,23 @@ runs:
|
||||
name: Ensure app/etc exists for magento composer plugin
|
||||
if: inputs.mode == 'extension'
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/fix-magento-install@v8.0.0-rc.0
|
||||
- name: Prevent Magento install from being mirrored into consumer vendor
|
||||
if: inputs.mode == 'extension'
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
env:
|
||||
MAGENTO_DIRECTORY: ${{ steps.setup-magento-compute-directory.outputs.MAGENTO_DIRECTORY }}
|
||||
run: |
|
||||
line="/$MAGENTO_DIRECTORY export-ignore"
|
||||
if [[ -f .gitattributes ]] && grep -qxF "$line" .gitattributes; then
|
||||
exit 0
|
||||
fi
|
||||
if [[ -s .gitattributes && -n "$(tail -c1 .gitattributes)" ]]; then
|
||||
printf '\n' >> .gitattributes
|
||||
fi
|
||||
printf '%s\n' "$line" >> .gitattributes
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/fix-magento-install@main
|
||||
name: Fix Magento Out of Box Install Issues
|
||||
with:
|
||||
magento_directory: ${{ steps.setup-magento-compute-directory.outputs.MAGENTO_DIRECTORY }}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
name: "Magento Smoke Test"
|
||||
author: "Graycore"
|
||||
description: "Hits a running Magento instance with a basic page or GraphQL probe and asserts the response looks right."
|
||||
|
||||
inputs:
|
||||
kind:
|
||||
description: "Which probe to run: `page` (GET / with title check) or `graphql` (POST /graphql with storeConfig query)."
|
||||
required: true
|
||||
host:
|
||||
description: "Host (and optional port) to probe. Defaults to `localhost`."
|
||||
required: false
|
||||
default: "localhost"
|
||||
timeout:
|
||||
description: "curl --max-time in seconds."
|
||||
required: false
|
||||
default: "60"
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Validate kind
|
||||
shell: bash
|
||||
env:
|
||||
KIND: ${{ inputs.kind }}
|
||||
run: |
|
||||
case "$KIND" in
|
||||
page|graphql) ;;
|
||||
*) echo "FATAL: kind must be 'page' or 'graphql' (got '$KIND')"; exit 1 ;;
|
||||
esac
|
||||
|
||||
- name: Smoke test page
|
||||
if: inputs.kind == 'page'
|
||||
shell: bash
|
||||
env:
|
||||
HOST: ${{ inputs.host }}
|
||||
TIMEOUT: ${{ inputs.timeout }}
|
||||
run: |
|
||||
status=$(curl -sS --max-time "$TIMEOUT" \
|
||||
-o /tmp/smoke-page.html -w "%{http_code}" "http://$HOST/")
|
||||
if [ "$status" != "200" ]; then
|
||||
echo "Page returned HTTP $status"
|
||||
head -c 4000 /tmp/smoke-page.html
|
||||
exit 1
|
||||
fi
|
||||
if ! grep -qE '<title>[[:space:]]*[^<[:space:]][^<]*</title>' /tmp/smoke-page.html; then
|
||||
echo "Page missing non-empty <title>"
|
||||
head -c 4000 /tmp/smoke-page.html
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Smoke test GraphQL
|
||||
if: inputs.kind == 'graphql'
|
||||
shell: bash
|
||||
env:
|
||||
HOST: ${{ inputs.host }}
|
||||
TIMEOUT: ${{ inputs.timeout }}
|
||||
run: |
|
||||
status=$(curl -sS --max-time "$TIMEOUT" \
|
||||
-H "Content-Type: application/json" \
|
||||
-X POST \
|
||||
-d '{"query":"{ storeConfig { store_code } }"}' \
|
||||
-o /tmp/smoke-graphql.json -w "%{http_code}" \
|
||||
"http://$HOST/graphql")
|
||||
if [ "$status" != "200" ]; then
|
||||
echo "GraphQL returned HTTP $status"
|
||||
cat /tmp/smoke-graphql.json
|
||||
exit 1
|
||||
fi
|
||||
if ! jq -e '.data.storeConfig.store_code' /tmp/smoke-graphql.json > /dev/null; then
|
||||
echo "GraphQL response missing data.storeConfig.store_code"
|
||||
cat /tmp/smoke-graphql.json
|
||||
exit 1
|
||||
fi
|
||||
|
||||
branding:
|
||||
icon: "check-circle"
|
||||
color: "green"
|
||||
@@ -14,10 +14,11 @@ See the [action.yml](./action.yml)
|
||||
| Input | Description | Required | Default |
|
||||
|-----------------| ------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------- |-----------------------|
|
||||
| kind | The "kind" of support you're targeting for your package. See [Kinds](#kinds). | false | 'currently-supported' |
|
||||
| project | The project to return the supported versions for. Allowed values are `mage-os` and `magento-open-source` | false | 'magento-open-source' |
|
||||
| project | The project to return the supported versions for. Allowed values are `mage-os`, `mage-os-minimal`, and `magento-open-source` | false | 'magento-open-source' |
|
||||
| custom_versions | The versions you want to support, as a comma-separated string, i.e. 'magento/project-community-edition:2.3.7-p3, magento/project-community-edition:2.4.2-p2' | false | '' |
|
||||
| recent_time_frame | The time frame (from today) used when `kind` is `recent`. Combination of years (y), months (m), and days (d), e.g. `2y 2m 2d`. | false | '2y' |
|
||||
| include_services | Whether to include a `services` key in each matrix entry with GitHub Actions service container configurations for MySQL, search engine, RabbitMQ, and cache. | false | 'true' |
|
||||
| service_preferences | Comma-separated list of service implementations to prefer (e.g. `elasticsearch,valkey`). See [Service preferences](#service-preferences). | false | '' |
|
||||
|
||||
## Kinds
|
||||
- `currently-supported` - The currently supported Magento Open Source versions by Adobe.
|
||||
@@ -30,8 +31,50 @@ See the [action.yml](./action.yml)
|
||||
|
||||
## Projects
|
||||
- `mage-os`
|
||||
- `mage-os-minimal`
|
||||
- `magento-open-source` (default)
|
||||
|
||||
## Service preferences
|
||||
|
||||
When `include_services: true` (the default), each matrix entry is enriched with a `services` map. Some tiers of services (for example, search) have more than one valid implementation across the supported Magento versions:
|
||||
|
||||
- **search**: `opensearch` or `elasticsearch`
|
||||
- **cache**: `valkey` or `redis`
|
||||
|
||||
By default the action picks `opensearch` over `elasticsearch` and `valkey` over `redis` wherever both are available for the matrix entry's Magento version. `service_preferences` lets the caller override that default pick by naming the implementation they want.
|
||||
|
||||
Tiers without a preference fall back to the per-version default pick. Your preferences are **selective**, not **exclusive**.
|
||||
|
||||
### Format
|
||||
|
||||
A comma-separated list of service implementation names. Whitespace around names is tolerated.
|
||||
|
||||
```yml
|
||||
with:
|
||||
service_preferences: elasticsearch,valkey
|
||||
```
|
||||
|
||||
### Accepted names
|
||||
|
||||
| Name | Tier |
|
||||
|------------------|--------|
|
||||
| `mysql` | db |
|
||||
| `elasticsearch` | search |
|
||||
| `opensearch` | search |
|
||||
| `rabbitmq` | queue |
|
||||
| `redis` | cache |
|
||||
| `valkey` | cache |
|
||||
|
||||
### Example
|
||||
|
||||
```yml
|
||||
- uses: graycoreio/github-actions-magento2/supported-version@v8.5.0 # x-release-please-version
|
||||
id: supported-version
|
||||
with:
|
||||
kind: currently-supported
|
||||
service_preferences: opensearch,valkey
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```yml
|
||||
@@ -51,7 +94,7 @@ jobs:
|
||||
outputs:
|
||||
matrix: ${{ steps.supported-version.outputs.matrix }}
|
||||
steps:
|
||||
- uses: graycoreio/github-actions-magento2/supported-version@v7.0.0 # x-release-please-version
|
||||
- uses: graycoreio/github-actions-magento2/supported-version@v8.5.0 # x-release-please-version
|
||||
id: supported-version
|
||||
- run: echo ${{ steps.supported-version.outputs.matrix }}
|
||||
```
|
||||
|
||||
@@ -9,7 +9,7 @@ inputs:
|
||||
default: "currently-supported"
|
||||
project:
|
||||
required: false
|
||||
description: "The project to return the supported versions for. Allowed values are `mage-os` and `magento-open-source`"
|
||||
description: "The project to return the supported versions for. Allowed values are `mage-os`, `mage-os-minimal`, and `magento-open-source`"
|
||||
# The default value is what it is to keep backward compatibility
|
||||
default: "magento-open-source"
|
||||
custom_versions:
|
||||
@@ -27,6 +27,11 @@ inputs:
|
||||
default: "true"
|
||||
description: "Whether to include a `services` key in each matrix entry with GitHub Actions service configurations."
|
||||
|
||||
service_preferences:
|
||||
required: false
|
||||
default: ""
|
||||
description: "Comma-separated list of service implementations to prefer (e.g. `elasticsearch,valkey`). Each name overrides the per-tier default implementation pick. Errors on unknown names, tier collisions (two names sharing a tier), or implementations not supported by every matrix entry for your selected kind."
|
||||
|
||||
outputs:
|
||||
matrix:
|
||||
description: "The Github Actions matrix of software technologies required to run Magento."
|
||||
|
||||
Vendored
+29
-25
File diff suppressed because one or more lines are too long
@@ -3,6 +3,7 @@ import { validateKind } from './kind/validate-kinds';
|
||||
import { getMatrixForKind } from './matrix/get-matrix-for-kind';
|
||||
import { validateProject } from "./project/validate-projects";
|
||||
import { buildServicesForEntry } from "./services/build-services";
|
||||
import { parseServicePreferences, validatePreferencesAgainstMatrix } from "./services/preferences";
|
||||
|
||||
|
||||
export async function run(): Promise<void> {
|
||||
@@ -12,19 +13,33 @@ export async function run(): Promise<void> {
|
||||
const project = core.getInput("project");
|
||||
const recent_time_frame = core.getInput("recent_time_frame");
|
||||
const include_services = core.getInput("include_services") === "true";
|
||||
const service_preferences_raw = core.getInput("service_preferences");
|
||||
|
||||
validateProject(<any>project)
|
||||
|
||||
validateKind(<any>kind, customVersions ? customVersions.split(',') : undefined);
|
||||
|
||||
const preferences = parseServicePreferences(service_preferences_raw);
|
||||
const hasPreferences = Object.keys(preferences).length > 0;
|
||||
|
||||
if (!include_services && hasPreferences) {
|
||||
throw new Error(
|
||||
'service_preferences cannot be combined with include_services: false. Set include_services: true or clear service_preferences.'
|
||||
);
|
||||
}
|
||||
|
||||
let matrix = getMatrixForKind(kind, project, customVersions, recent_time_frame);
|
||||
|
||||
if (include_services) {
|
||||
if (hasPreferences) {
|
||||
validatePreferencesAgainstMatrix(preferences, matrix.include);
|
||||
}
|
||||
const workspace = process.env.GITHUB_WORKSPACE || '';
|
||||
matrix = {
|
||||
magento: matrix.magento,
|
||||
include: matrix.include.map((entry) => ({
|
||||
...entry,
|
||||
services: buildServicesForEntry(entry)
|
||||
services: buildServicesForEntry(entry, preferences, workspace)
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
@@ -88,12 +88,14 @@ describe('getCurrentlySupportedVersions for magento-open-source', () => {
|
||||
'magento/project-community-edition:2.4.7-p9',
|
||||
'magento/project-community-edition:2.4.8-p4',
|
||||
]],
|
||||
['2027-04-09T00:00:00Z', 'Day of v2.4.7 EoL', [
|
||||
'magento/project-community-edition:2.4.7-p9',
|
||||
'magento/project-community-edition:2.4.8-p4',
|
||||
['2027-05-31T00:00:00Z', 'Day of v2.4.7 EoL', [
|
||||
'magento/project-community-edition:2.4.7-p10',
|
||||
'magento/project-community-edition:2.4.8-p5',
|
||||
'magento/project-community-edition:2.4.9',
|
||||
]],
|
||||
['2027-04-10T00:00:00Z', 'Day after v2.4.7 EoL', [
|
||||
'magento/project-community-edition:2.4.8-p4',
|
||||
['2027-06-01T00:00:00Z', 'Day after v2.4.7 EoL', [
|
||||
'magento/project-community-edition:2.4.8-p5',
|
||||
'magento/project-community-edition:2.4.9',
|
||||
]],
|
||||
])(
|
||||
'supportedVersions for %s',
|
||||
@@ -143,6 +145,12 @@ describe('getCurrentlySupportedVersions for mage-os', () => {
|
||||
['2026-04-15T00:00:01Z', 'Release of 2.2.2', [
|
||||
'mage-os/project-community-edition:2.2.2',
|
||||
]],
|
||||
['2026-05-13T00:00:01Z', 'Release of 2.3.0', [
|
||||
'mage-os/project-community-edition:2.3.0',
|
||||
]],
|
||||
['2026-05-19T00:00:01Z', 'Release of 3.0.0', [
|
||||
'mage-os/project-community-edition:3.0.0',
|
||||
]],
|
||||
])(
|
||||
'supportedVersions for %s',
|
||||
(date, description ,result) => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"mage-os": ["mage-os/project-community-edition"],
|
||||
"mage-os-minimal": ["mage-os/project-minimal-edition"],
|
||||
"magento-open-source": ["magento/project-community-edition"]
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"mage-os": ["mage-os/project-community-edition:next"],
|
||||
"mage-os-minimal": ["mage-os/project-minimal-edition:next"],
|
||||
"magento-open-source": ["magento/project-community-edition:next"]
|
||||
}
|
||||
@@ -3,6 +3,7 @@ export interface ServiceConfig {
|
||||
env?: Record<string, string>;
|
||||
ports?: string[];
|
||||
options?: string;
|
||||
volumes?: string[];
|
||||
}
|
||||
|
||||
export interface Services {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
*/
|
||||
export const KNOWN_PROJECTS = {
|
||||
"mage-os": true,
|
||||
"mage-os-minimal": true,
|
||||
"magento-open-source": true,
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ describe('validateProject', () => {
|
||||
it('returns `true` if its a valid project', () => {
|
||||
expect(validateProject("magento-open-source")).toBe(true);
|
||||
expect(validateProject("mage-os")).toBe(true);
|
||||
expect(validateProject("mage-os-minimal")).toBe(true);
|
||||
});
|
||||
|
||||
it('throws a helpful exception if it is an invalid project', () => {
|
||||
|
||||
@@ -4,6 +4,7 @@ import {Project} from "../projects";
|
||||
describe('isKnownProject', () => {
|
||||
it('returns `true` for known projects', () => {
|
||||
expect(isKnownProject("mage-os")).toBe(true)
|
||||
expect(isKnownProject("mage-os-minimal")).toBe(true)
|
||||
expect(isKnownProject("magento-open-source")).toBe(true)
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { buildServicesForEntry } from './build-services';
|
||||
import { PackageMatrixVersion } from '../matrix/matrix-type';
|
||||
import mageOsMinimalIndividual from '../versions/mage-os-minimal/individual.json';
|
||||
import mageOsMinimalComposite from '../versions/mage-os-minimal/composite.json';
|
||||
|
||||
const createTestEntry = (overrides: Partial<PackageMatrixVersion> = {}): PackageMatrixVersion => ({
|
||||
magento: 'magento/project-community-edition:2.4.7',
|
||||
@@ -223,13 +225,17 @@ describe('buildServicesForEntry', () => {
|
||||
describe('complete service output', () => {
|
||||
it('should build all services when all are available', () => {
|
||||
const entry = createTestEntry();
|
||||
const services = buildServicesForEntry(entry);
|
||||
const services = buildServicesForEntry(entry, {}, '/runner/ws');
|
||||
|
||||
expect(Object.keys(services)).toHaveLength(4);
|
||||
expect(Object.keys(services).sort()).toEqual(
|
||||
['mysql', 'nginx', 'opensearch', 'php-fpm', 'rabbitmq', 'valkey']
|
||||
);
|
||||
expect(services.mysql).toBeDefined();
|
||||
expect(services.opensearch).toBeDefined();
|
||||
expect(services.rabbitmq).toBeDefined();
|
||||
expect(services.valkey).toBeDefined();
|
||||
expect(services.nginx).toBeDefined();
|
||||
expect(services['php-fpm']).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle entry with minimal services', () => {
|
||||
@@ -239,11 +245,136 @@ describe('buildServicesForEntry', () => {
|
||||
opensearch: '',
|
||||
rabbitmq: '',
|
||||
redis: '',
|
||||
valkey: ''
|
||||
valkey: '',
|
||||
nginx: '',
|
||||
php: ''
|
||||
});
|
||||
const services = buildServicesForEntry(entry);
|
||||
|
||||
expect(Object.keys(services)).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('web tier (nginx + php-fpm)', () => {
|
||||
it('emits both nginx and php-fpm when both data points are present', () => {
|
||||
const entry = createTestEntry();
|
||||
const services = buildServicesForEntry(entry, {}, '/runner/ws');
|
||||
|
||||
expect(services.nginx).toBeDefined();
|
||||
expect(services['php-fpm']).toBeDefined();
|
||||
});
|
||||
|
||||
it('uses the entry.nginx image for nginx', () => {
|
||||
const entry = createTestEntry({ nginx: 'nginx:1.27-alpine' });
|
||||
const services = buildServicesForEntry(entry, {}, '/runner/ws');
|
||||
|
||||
expect(services.nginx.image).toBe('nginx:1.27-alpine');
|
||||
});
|
||||
|
||||
it('composes the php-fpm image from entry.php using mappia', () => {
|
||||
const entry = createTestEntry({ php: '8.2' });
|
||||
const services = buildServicesForEntry(entry, {}, '/runner/ws');
|
||||
|
||||
expect(services['php-fpm'].image).toBe('mappia/magento-php:fpm-alpine8.2');
|
||||
});
|
||||
|
||||
it('mounts the runner workspace at /var/www/html on both services', () => {
|
||||
const entry = createTestEntry();
|
||||
const services = buildServicesForEntry(entry, {}, '/home/runner/work/foo/foo');
|
||||
|
||||
expect(services.nginx.volumes).toEqual(['/home/runner/work/foo/foo:/var/www/html']);
|
||||
expect(services['php-fpm'].volumes).toEqual(['/home/runner/work/foo/foo:/var/www/html']);
|
||||
});
|
||||
|
||||
it('exposes port 80 on nginx with the nginx -t healthcheck', () => {
|
||||
const entry = createTestEntry();
|
||||
const services = buildServicesForEntry(entry, {}, '/runner/ws');
|
||||
|
||||
expect(services.nginx.ports).toEqual(['80:80']);
|
||||
expect(services.nginx.options).toContain('nginx -t');
|
||||
});
|
||||
|
||||
it('skips both when entry.nginx is empty (they emit together or not at all)', () => {
|
||||
const entry = createTestEntry({ nginx: '' });
|
||||
const services = buildServicesForEntry(entry, {}, '/runner/ws');
|
||||
|
||||
expect(services.nginx).toBeUndefined();
|
||||
expect(services['php-fpm']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('skips both when entry.php is empty (they emit together or not at all)', () => {
|
||||
const entry = createTestEntry({ php: '' });
|
||||
const services = buildServicesForEntry(entry, {}, '/runner/ws');
|
||||
|
||||
expect(services.nginx).toBeUndefined();
|
||||
expect(services['php-fpm']).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('with service preferences', () => {
|
||||
it('uses elasticsearch when search preference is elasticsearch, even if opensearch is available', () => {
|
||||
const entry = createTestEntry();
|
||||
const services = buildServicesForEntry(entry, { search: 'elasticsearch' });
|
||||
|
||||
expect(services.elasticsearch).toBeDefined();
|
||||
expect(services.elasticsearch.image).toBe('elasticsearch:8.11.4');
|
||||
expect(services.opensearch).toBeUndefined();
|
||||
});
|
||||
|
||||
it('uses opensearch when search preference is opensearch (matches default)', () => {
|
||||
const entry = createTestEntry();
|
||||
const services = buildServicesForEntry(entry, { search: 'opensearch' });
|
||||
|
||||
expect(services.opensearch).toBeDefined();
|
||||
expect(services.elasticsearch).toBeUndefined();
|
||||
});
|
||||
|
||||
it('uses redis when cache preference is redis, even if valkey is available', () => {
|
||||
const entry = createTestEntry();
|
||||
const services = buildServicesForEntry(entry, { cache: 'redis' });
|
||||
|
||||
expect(services.redis).toBeDefined();
|
||||
expect(services.redis.image).toBe('redis:7.2');
|
||||
expect(services.valkey).toBeUndefined();
|
||||
});
|
||||
|
||||
it('applies preferences across multiple tiers independently', () => {
|
||||
const entry = createTestEntry();
|
||||
const services = buildServicesForEntry(entry, { search: 'elasticsearch', cache: 'redis' });
|
||||
|
||||
expect(services.elasticsearch).toBeDefined();
|
||||
expect(services.redis).toBeDefined();
|
||||
expect(services.opensearch).toBeUndefined();
|
||||
expect(services.valkey).toBeUndefined();
|
||||
});
|
||||
|
||||
it('falls back to default-pick for tiers without a preference', () => {
|
||||
const entry = createTestEntry();
|
||||
const services = buildServicesForEntry(entry, { search: 'elasticsearch' });
|
||||
|
||||
expect(services.elasticsearch).toBeDefined();
|
||||
expect(services.valkey).toBeDefined();
|
||||
});
|
||||
|
||||
it('treats single-implementation-tier preferences as no-ops', () => {
|
||||
const entry = createTestEntry();
|
||||
const withPref = buildServicesForEntry(entry, { db: 'mysql', queue: 'rabbitmq' });
|
||||
const withoutPref = buildServicesForEntry(entry);
|
||||
|
||||
expect(withPref).toEqual(withoutPref);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mage-os-minimal', () => {
|
||||
const minimalEntries: [string, PackageMatrixVersion][] = [
|
||||
...Object.entries(mageOsMinimalIndividual as unknown as Record<string, PackageMatrixVersion>),
|
||||
...Object.entries(mageOsMinimalComposite as unknown as Record<string, PackageMatrixVersion>)
|
||||
];
|
||||
|
||||
it.each(minimalEntries)('omits rabbitmq from services for %s', (_key, entry) => {
|
||||
const services = buildServicesForEntry(entry);
|
||||
|
||||
expect(services.rabbitmq).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,8 +5,11 @@ import {
|
||||
opensearchConfig,
|
||||
rabbitmqConfig,
|
||||
redisConfig,
|
||||
valkeyConfig
|
||||
valkeyConfig,
|
||||
buildNginxConfig,
|
||||
buildPhpFpmConfig
|
||||
} from './service-config';
|
||||
import { ServicePreferences } from './preferences';
|
||||
|
||||
interface SearchEngineChoice {
|
||||
type: 'opensearch' | 'elasticsearch';
|
||||
@@ -19,10 +22,22 @@ interface CacheChoice {
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines which search engine to use for a matrix entry.
|
||||
* Prefers opensearch over elasticsearch.
|
||||
* Picks the search engine for a matrix entry. Honors caller's `service_preferences`
|
||||
* when set; otherwise prefers opensearch over elasticsearch.
|
||||
*/
|
||||
function getSearchEngineChoice(entry: PackageMatrixVersion): SearchEngineChoice | null {
|
||||
const getSearchEngineChoice = (entry: PackageMatrixVersion, preference?: string): SearchEngineChoice | null => {
|
||||
if (preference === 'opensearch') {
|
||||
if (entry.opensearch && entry.opensearch.trim() !== '') {
|
||||
return { type: 'opensearch', image: entry.opensearch };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (preference === 'elasticsearch') {
|
||||
if (entry.elasticsearch && entry.elasticsearch.trim() !== '') {
|
||||
return { type: 'elasticsearch', image: entry.elasticsearch };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (entry.opensearch && entry.opensearch.trim() !== '') {
|
||||
return { type: 'opensearch', image: entry.opensearch };
|
||||
}
|
||||
@@ -33,10 +48,22 @@ function getSearchEngineChoice(entry: PackageMatrixVersion): SearchEngineChoice
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines which cache to use for a matrix entry.
|
||||
* Prefers valkey over redis.
|
||||
* Picks the cache for a matrix entry. Honors caller's `service_preferences`
|
||||
* when set; otherwise prefers valkey over redis.
|
||||
*/
|
||||
function getCacheChoice(entry: PackageMatrixVersion): CacheChoice | null {
|
||||
const getCacheChoice = (entry: PackageMatrixVersion, preference?: string): CacheChoice | null => {
|
||||
if (preference === 'valkey') {
|
||||
if (entry.valkey && entry.valkey.trim() !== '') {
|
||||
return { type: 'valkey', image: entry.valkey };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (preference === 'redis') {
|
||||
if (entry.redis && entry.redis.trim() !== '') {
|
||||
return { type: 'redis', image: entry.redis };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (entry.valkey && entry.valkey.trim() !== '') {
|
||||
return { type: 'valkey', image: entry.valkey };
|
||||
}
|
||||
@@ -47,18 +74,25 @@ function getCacheChoice(entry: PackageMatrixVersion): CacheChoice | null {
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the services object for a single matrix entry.
|
||||
* Builds the services object for a single matrix entry. Emits every
|
||||
* tier the entry has data for: mysql, search (opensearch/elasticsearch),
|
||||
* queue (rabbitmq), cache (valkey/redis), and web (nginx + php-fpm
|
||||
* together). The web tier requires `workspace` so the volume mount
|
||||
* has a real path; if the entry lacks either nginx or php data the
|
||||
* web tier is skipped entirely (they're coupled — emit both or neither).
|
||||
*/
|
||||
export function buildServicesForEntry(entry: PackageMatrixVersion): Services {
|
||||
export const buildServicesForEntry = (
|
||||
entry: PackageMatrixVersion,
|
||||
preferences: ServicePreferences = {},
|
||||
workspace: string = ''
|
||||
): Services => {
|
||||
const services: Services = {};
|
||||
|
||||
// MySQL is always included if present
|
||||
if (entry.mysql && entry.mysql.trim() !== '') {
|
||||
services.mysql = mysqlConfig.getConfig(entry.mysql);
|
||||
}
|
||||
|
||||
// Search engine: prefer opensearch over elasticsearch
|
||||
const searchEngine = getSearchEngineChoice(entry);
|
||||
const searchEngine = getSearchEngineChoice(entry, preferences.search);
|
||||
if (searchEngine) {
|
||||
if (searchEngine.type === 'opensearch') {
|
||||
services.opensearch = opensearchConfig.getConfig(searchEngine.image);
|
||||
@@ -67,13 +101,11 @@ export function buildServicesForEntry(entry: PackageMatrixVersion): Services {
|
||||
}
|
||||
}
|
||||
|
||||
// RabbitMQ
|
||||
if (entry.rabbitmq && entry.rabbitmq.trim() !== '') {
|
||||
services.rabbitmq = rabbitmqConfig.getConfig(entry.rabbitmq);
|
||||
}
|
||||
|
||||
// Cache: prefer valkey over redis
|
||||
const cache = getCacheChoice(entry);
|
||||
const cache = getCacheChoice(entry, preferences.cache);
|
||||
if (cache) {
|
||||
if (cache.type === 'valkey') {
|
||||
services.valkey = valkeyConfig.getConfig(cache.image);
|
||||
@@ -82,5 +114,12 @@ export function buildServicesForEntry(entry: PackageMatrixVersion): Services {
|
||||
}
|
||||
}
|
||||
|
||||
const nginxImage = (entry.nginx || '').trim();
|
||||
const phpVersion = String(entry.php ?? '').trim();
|
||||
if (nginxImage !== '' && phpVersion !== '') {
|
||||
services.nginx = buildNginxConfig(nginxImage, workspace);
|
||||
services['php-fpm'] = buildPhpFpmConfig(phpVersion, workspace);
|
||||
}
|
||||
|
||||
return services;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export { Tier, ServicePreferences, KNOWN_SERVICE_NAMES, tierFor } from './tier-map';
|
||||
export { parseServicePreferences } from './parse-service-preferences';
|
||||
export { validatePreferencesAgainstMatrix } from './validate-preferences-against-matrix';
|
||||
@@ -0,0 +1,50 @@
|
||||
import { parseServicePreferences } from './parse-service-preferences';
|
||||
|
||||
describe('parseServicePreferences', () => {
|
||||
it('returns an empty map when input is empty', () => {
|
||||
expect(parseServicePreferences('')).toEqual({});
|
||||
});
|
||||
|
||||
it('returns an empty map for whitespace-only input', () => {
|
||||
expect(parseServicePreferences(' ')).toEqual({});
|
||||
});
|
||||
|
||||
it('maps a single name to its tier', () => {
|
||||
expect(parseServicePreferences('opensearch')).toEqual({ search: 'opensearch' });
|
||||
});
|
||||
|
||||
it('maps two names in different tiers', () => {
|
||||
expect(parseServicePreferences('elasticsearch,valkey')).toEqual({
|
||||
search: 'elasticsearch',
|
||||
cache: 'valkey',
|
||||
});
|
||||
});
|
||||
|
||||
it('accepts single-implementation-tier names as no-op-like preferences', () => {
|
||||
expect(parseServicePreferences('mysql,rabbitmq')).toEqual({
|
||||
db: 'mysql',
|
||||
queue: 'rabbitmq',
|
||||
});
|
||||
});
|
||||
|
||||
it('tolerates whitespace around names', () => {
|
||||
expect(parseServicePreferences(' opensearch , valkey ')).toEqual({
|
||||
search: 'opensearch',
|
||||
cache: 'valkey',
|
||||
});
|
||||
});
|
||||
|
||||
it('throws on unknown service name', () => {
|
||||
expect(() => parseServicePreferences('foobar')).toThrowError(/unknown service "foobar"/);
|
||||
});
|
||||
|
||||
it('throws on a collision in the search tier', () => {
|
||||
expect(() => parseServicePreferences('elasticsearch,opensearch')).toThrowError(
|
||||
/collision in tier "search"/
|
||||
);
|
||||
});
|
||||
|
||||
it('throws on a collision in the cache tier', () => {
|
||||
expect(() => parseServicePreferences('redis,valkey')).toThrowError(/collision in tier "cache"/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
import { ServicePreferences, SERVICE_TIER_MAP, KNOWN_SERVICE_NAMES } from './tier-map';
|
||||
|
||||
/**
|
||||
* Parses the comma-separated `service_preferences` input into a
|
||||
* tier-to-implementation map. Throws on unknown service names or when
|
||||
* two names collide in the same tier.
|
||||
*/
|
||||
export const parseServicePreferences = (raw: string): ServicePreferences => {
|
||||
const trimmed = (raw || '').trim();
|
||||
if (trimmed === '') return {};
|
||||
|
||||
const names = trimmed.split(',').map(s => s.trim()).filter(s => s !== '');
|
||||
const preferences: ServicePreferences = {};
|
||||
|
||||
for (const name of names) {
|
||||
const tier = SERVICE_TIER_MAP[name];
|
||||
if (!tier) {
|
||||
throw new Error(
|
||||
`service_preferences: unknown service "${name}". Known services: ${KNOWN_SERVICE_NAMES.join(', ')}`
|
||||
);
|
||||
}
|
||||
const existing = preferences[tier];
|
||||
if (existing) {
|
||||
throw new Error(
|
||||
`service_preferences: collision in tier "${tier}" — both "${existing}" and "${name}" specified`
|
||||
);
|
||||
}
|
||||
preferences[tier] = name;
|
||||
}
|
||||
|
||||
return preferences;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { tierFor } from './tier-map';
|
||||
|
||||
describe('tierFor', () => {
|
||||
it.each([
|
||||
['mysql', 'db'],
|
||||
['elasticsearch', 'search'],
|
||||
['opensearch', 'search'],
|
||||
['rabbitmq', 'queue'],
|
||||
['redis', 'cache'],
|
||||
['valkey', 'cache'],
|
||||
])('maps %s to %s', (name, tier) => {
|
||||
expect(tierFor(name)).toBe(tier);
|
||||
});
|
||||
|
||||
it('returns undefined for unknown names', () => {
|
||||
expect(tierFor('foobar')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* A category of services that Magento can choose between. Each tier
|
||||
* has one or more concrete implementations (e.g. the `search` tier
|
||||
* resolves to either `opensearch` or `elasticsearch`).
|
||||
*/
|
||||
export type Tier = 'db' | 'search' | 'queue' | 'cache';
|
||||
|
||||
/**
|
||||
* Caller-expressed preference of "which implementation to use for which tier."
|
||||
* Produced by `parseServicePreferences` from the `service_preferences`
|
||||
* action input, then threaded into `buildServicesForEntry` to override
|
||||
* the per-tier default pick. Partial because callers only need to name
|
||||
* the tiers they care about; unset tiers fall back to the version's
|
||||
* default implementation.
|
||||
*/
|
||||
export type ServicePreferences = Partial<Record<Tier, string>>;
|
||||
|
||||
/**
|
||||
* Reverse lookup: which tier does each known service implementation
|
||||
* belong to. The keys of this map define the closed set of legal
|
||||
* service names in `service_preferences`; anything not listed here is
|
||||
* rejected as an unknown service. nginx and php-fpm are intentionally
|
||||
* absent — they're complementary (the `web` tier emits both together),
|
||||
* not alternatives, so picking one via preferences would be meaningless.
|
||||
*/
|
||||
export const SERVICE_TIER_MAP: Record<string, Tier> = {
|
||||
mysql: 'db',
|
||||
elasticsearch: 'search',
|
||||
opensearch: 'search',
|
||||
rabbitmq: 'queue',
|
||||
redis: 'cache',
|
||||
valkey: 'cache',
|
||||
};
|
||||
|
||||
/**
|
||||
* Forward lookup: the implementations available within each tier,
|
||||
* ordered by default preference (first entry wins when no caller
|
||||
* preference is supplied). Used by the validator to render
|
||||
* "supported: a, b" lists in compatibility errors and by
|
||||
* `buildServicesForEntry` to know what to fall back to.
|
||||
*/
|
||||
export const TIER_IMPLEMENTATIONS: Record<Tier, string[]> = {
|
||||
db: ['mysql'],
|
||||
search: ['opensearch', 'elasticsearch'],
|
||||
queue: ['rabbitmq'],
|
||||
cache: ['valkey', 'redis'],
|
||||
};
|
||||
|
||||
/**
|
||||
* Flat list of every legal service name in `service_preferences`.
|
||||
* Surfaced in unknown-service error messages so the caller sees
|
||||
* exactly what's accepted without having to read source.
|
||||
*/
|
||||
export const KNOWN_SERVICE_NAMES = Object.keys(SERVICE_TIER_MAP);
|
||||
|
||||
/**
|
||||
* Returns the tier a service name belongs to, or `undefined` if the
|
||||
* name isn't recognized. Callers use the undefined return as the
|
||||
* signal to reject the input as unknown.
|
||||
*/
|
||||
export const tierFor = (serviceName: string): Tier | undefined => {
|
||||
return SERVICE_TIER_MAP[serviceName];
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
import { validatePreferencesAgainstMatrix } from './validate-preferences-against-matrix';
|
||||
import { PackageMatrixVersion } from '../../matrix/matrix-type';
|
||||
|
||||
const baseEntry = (overrides: Partial<PackageMatrixVersion> = {}): PackageMatrixVersion => ({
|
||||
magento: 'magento/project-community-edition:2.4.7',
|
||||
version: '2.4.7',
|
||||
php: '8.3',
|
||||
composer: '2.7.4',
|
||||
mysql: 'mysql:8.4',
|
||||
elasticsearch: 'elasticsearch:8.11.4',
|
||||
opensearch: 'opensearchproject/opensearch:2.19.1',
|
||||
rabbitmq: 'rabbitmq:4.0-management',
|
||||
redis: 'redis:7.2',
|
||||
varnish: 'varnish:7.5',
|
||||
valkey: 'valkey:8.0',
|
||||
nginx: 'nginx:1.26',
|
||||
os: 'ubuntu-latest',
|
||||
release: '2024-04-09T00:00:00+0000',
|
||||
eol: '2027-04-09T00:00:00+0000',
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe('validatePreferencesAgainstMatrix', () => {
|
||||
it('does not throw when all entries support the preference', () => {
|
||||
const entries = [baseEntry({ version: '2.4.7' }), baseEntry({ version: '2.4.6' })];
|
||||
expect(() => validatePreferencesAgainstMatrix({ search: 'opensearch' }, entries)).not.toThrow();
|
||||
});
|
||||
|
||||
it('throws when an entry lacks the preferenced implementation, listing the offender', () => {
|
||||
const entries = [
|
||||
baseEntry({ version: '2.4.7' }),
|
||||
baseEntry({ version: '2.4.5', opensearch: '' }),
|
||||
];
|
||||
expect(() => validatePreferencesAgainstMatrix({ search: 'opensearch' }, entries)).toThrowError(
|
||||
/not satisfied for:\n\s+- magento 2\.4\.5 \(supported: elasticsearch\)/
|
||||
);
|
||||
});
|
||||
|
||||
it('lists all offenders, not just the first', () => {
|
||||
const entries = [
|
||||
baseEntry({ version: '2.4.5', opensearch: '' }),
|
||||
baseEntry({ version: '2.4.4', opensearch: '' }),
|
||||
];
|
||||
expect(() => validatePreferencesAgainstMatrix({ search: 'opensearch' }, entries)).toThrowError(
|
||||
/magento 2\.4\.5[\s\S]*magento 2\.4\.4/
|
||||
);
|
||||
});
|
||||
|
||||
it('reports "<none>" when the entry supports nothing in the tier', () => {
|
||||
const entries = [baseEntry({ version: '2.4.0', opensearch: '', elasticsearch: '' })];
|
||||
expect(() => validatePreferencesAgainstMatrix({ search: 'opensearch' }, entries)).toThrowError(
|
||||
/magento 2\.4\.0 \(supported: <none>\)/
|
||||
);
|
||||
});
|
||||
|
||||
it('reports violations across multiple tiers', () => {
|
||||
const entries = [baseEntry({ version: '2.4.5', opensearch: '', valkey: '' })];
|
||||
let captured: Error | null = null;
|
||||
try {
|
||||
validatePreferencesAgainstMatrix({ search: 'opensearch', cache: 'valkey' }, entries);
|
||||
} catch (e) {
|
||||
captured = e as Error;
|
||||
}
|
||||
expect(captured).not.toBeNull();
|
||||
expect(captured!.message).toMatch(/opensearch/);
|
||||
expect(captured!.message).toMatch(/valkey/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
import { PackageMatrixVersion } from '../../matrix/matrix-type';
|
||||
import { ServicePreferences, Tier, TIER_IMPLEMENTATIONS } from './tier-map';
|
||||
|
||||
/**
|
||||
* Verifies that every preferenced implementation is supported by every
|
||||
* matrix entry. Collects all violations and throws a single
|
||||
* consolidated error.
|
||||
*/
|
||||
export const validatePreferencesAgainstMatrix = (
|
||||
preferences: ServicePreferences,
|
||||
entries: PackageMatrixVersion[]
|
||||
): void => {
|
||||
const errors: string[] = [];
|
||||
|
||||
for (const tierKey of Object.keys(preferences) as Tier[]) {
|
||||
const implementation = preferences[tierKey]!;
|
||||
const tierImplementations = TIER_IMPLEMENTATIONS[tierKey];
|
||||
|
||||
const offenders: { version: string; supported: string[] }[] = [];
|
||||
for (const entry of entries) {
|
||||
const value = entry[implementation as keyof PackageMatrixVersion];
|
||||
const isSupported = typeof value === 'string' && value.trim() !== '';
|
||||
if (!isSupported) {
|
||||
const supported = tierImplementations.filter(name => {
|
||||
const v = entry[name as keyof PackageMatrixVersion];
|
||||
return typeof v === 'string' && v.trim() !== '';
|
||||
});
|
||||
offenders.push({ version: entry.version, supported });
|
||||
}
|
||||
}
|
||||
|
||||
if (offenders.length > 0) {
|
||||
const list = offenders
|
||||
.map(o => ` - magento ${o.version} (supported: ${o.supported.length > 0 ? o.supported.join(', ') : '<none>'})`)
|
||||
.join('\n');
|
||||
errors.push(`service_preferences "${implementation}" is not satisfied for:\n${list}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw new Error(errors.join('\n\n'));
|
||||
}
|
||||
}
|
||||
@@ -81,3 +81,28 @@ export const valkeyConfig: ServiceTemplate = {
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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,6 +5,7 @@ describe('getIndividialVersionsForProject', () => {
|
||||
it('returns individual versions matrix for magento-open-source', () => {
|
||||
expect(Object.keys(getIndividualVersionsForProject("magento-open-source")).length).toBeGreaterThan(0)
|
||||
expect(Object.keys(getIndividualVersionsForProject("mage-os")).length).toBeGreaterThan(0)
|
||||
expect(Object.keys(getIndividualVersionsForProject("mage-os-minimal")).length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('throws error if no individual versions are specified for given project', () => {
|
||||
@@ -16,6 +17,7 @@ describe('getCompositeVersionsForProject', () => {
|
||||
it('returns composite versions matrix for magento-open-source', () => {
|
||||
expect(Object.keys(getCompositeVersionsForProject("magento-open-source")).length).toBeGreaterThan(0)
|
||||
expect(Object.keys(getCompositeVersionsForProject("mage-os")).length).toBeGreaterThan(0)
|
||||
expect(Object.keys(getCompositeVersionsForProject("mage-os-minimal")).length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('throws error if no composite versions are specified for given project', () => {
|
||||
|
||||
@@ -3,11 +3,13 @@ import { PackageMatrixVersion } from "../matrix/matrix-type";
|
||||
|
||||
const individual = {
|
||||
'mage-os': require('./mage-os/individual.json'),
|
||||
'mage-os-minimal': require('./mage-os-minimal/individual.json'),
|
||||
'magento-open-source': require('./magento-open-source/individual.json')
|
||||
}
|
||||
|
||||
const composite = {
|
||||
'mage-os': require('./mage-os/composite.json'),
|
||||
'mage-os-minimal': require('./mage-os-minimal/composite.json'),
|
||||
'magento-open-source': require('./magento-open-source/composite.json')
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"mage-os/project-minimal-edition": {
|
||||
"magento": "mage-os/project-minimal-edition",
|
||||
"php": 8.5,
|
||||
"composer": "2.9.8",
|
||||
"mysql": "mysql:8.4",
|
||||
"opensearch": "opensearchproject/opensearch:3",
|
||||
"valkey": "valkey/valkey:9",
|
||||
"varnish": "varnish:8",
|
||||
"nginx": "nginx:1.28",
|
||||
"os": "ubuntu-latest",
|
||||
"release": "2026-05-19T00:00:00+0000",
|
||||
"eol": "2029-05-19T00:00:00+0000"
|
||||
},
|
||||
"mage-os/project-minimal-edition:next": {
|
||||
"magento": "mage-os/project-minimal-edition:next",
|
||||
"php": 8.5,
|
||||
"composer": "2.9.8",
|
||||
"mysql": "mysql:8.4",
|
||||
"opensearch": "opensearchproject/opensearch:3",
|
||||
"valkey": "valkey/valkey:9",
|
||||
"varnish": "varnish:8",
|
||||
"nginx": "nginx:1.28",
|
||||
"os": "ubuntu-latest",
|
||||
"release": "2026-05-19T00:00:00+0000",
|
||||
"eol": "2029-05-19T00:00:00+0000"
|
||||
},
|
||||
"mage-os/project-minimal-edition:>=3.0 <3.1": {
|
||||
"magento": "mage-os/project-minimal-edition:>=3.0 <3.1",
|
||||
"php": 8.5,
|
||||
"composer": "2.9.8",
|
||||
"mysql": "mysql:8.4",
|
||||
"opensearch": "opensearchproject/opensearch:3",
|
||||
"valkey": "valkey/valkey:9",
|
||||
"varnish": "varnish:8",
|
||||
"nginx": "nginx:1.28",
|
||||
"os": "ubuntu-latest",
|
||||
"release": "2026-05-19T00:00:00+0000",
|
||||
"eol": "2029-05-19T00:00:00+0000"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"mage-os/project-minimal-edition:3.0.0": {
|
||||
"magento": "mage-os/project-minimal-edition:3.0.0",
|
||||
"upstream": "2.4.9",
|
||||
"php": 8.5,
|
||||
"composer": "2.9.8",
|
||||
"mysql": "mysql:8.4",
|
||||
"opensearch": "opensearchproject/opensearch:3",
|
||||
"valkey": "valkey/valkey:9",
|
||||
"varnish": "varnish:8",
|
||||
"nginx": "nginx:1.28",
|
||||
"os": "ubuntu-latest",
|
||||
"release": "2026-05-19T00:00:00+0000",
|
||||
"eol": "2029-05-19T00:00:00+0000"
|
||||
}
|
||||
}
|
||||
@@ -2,37 +2,35 @@
|
||||
"mage-os/project-community-edition": {
|
||||
"magento": "mage-os/project-community-edition",
|
||||
"php": 8.4,
|
||||
"composer": "2.9.7",
|
||||
"composer": "2.9.8",
|
||||
"mysql": "mysql:8.4",
|
||||
"opensearch": "opensearchproject/opensearch:3",
|
||||
"rabbitmq": "rabbitmq:4.1-management",
|
||||
"valkey": "valkey/valkey:8.0",
|
||||
"redis": "redis:7.2",
|
||||
"valkey": "valkey/valkey:8",
|
||||
"varnish": "varnish:7.7",
|
||||
"nginx": "nginx:1.28",
|
||||
"os": "ubuntu-latest",
|
||||
"release": "2026-04-15T00:00:00+0000",
|
||||
"eol": "2029-04-15T00:00:00+0000"
|
||||
"release": "2026-05-19T00:00:00+0000",
|
||||
"eol": "2029-05-19T00:00:00+0000"
|
||||
},
|
||||
"mage-os/project-community-edition:next": {
|
||||
"magento": "mage-os/project-community-edition:next",
|
||||
"php": 8.4,
|
||||
"composer": "2.9.7",
|
||||
"composer": "2.9.8",
|
||||
"mysql": "mysql:8.4",
|
||||
"opensearch": "opensearchproject/opensearch:3",
|
||||
"rabbitmq": "rabbitmq:4.1-management",
|
||||
"valkey": "valkey/valkey:8.0",
|
||||
"redis": "redis:7.2",
|
||||
"valkey": "valkey/valkey:8",
|
||||
"varnish": "varnish:7.7",
|
||||
"nginx": "nginx:1.28",
|
||||
"os": "ubuntu-latest",
|
||||
"release": "2026-04-15T00:00:00+0000",
|
||||
"eol": "2029-04-15T00:00:00+0000"
|
||||
"release": "2026-05-19T00:00:00+0000",
|
||||
"eol": "2029-05-19T00:00:00+0000"
|
||||
},
|
||||
"mage-os/project-community-edition:>=1.0 <1.1": {
|
||||
"magento": "mage-os/project-community-edition:>=1.0 <1.1",
|
||||
"php": 8.3,
|
||||
"composer": "2.9.5",
|
||||
"composer": "2.9.8",
|
||||
"mysql": "mariadb:10.6",
|
||||
"elasticsearch": "elasticsearch:8.11.4",
|
||||
"rabbitmq": "rabbitmq:3.13-management",
|
||||
@@ -46,7 +44,7 @@
|
||||
"mage-os/project-community-edition:>=1.1 <1.2": {
|
||||
"magento": "mage-os/project-community-edition:>=1.1 <1.2",
|
||||
"php": 8.4,
|
||||
"composer": "2.9.5",
|
||||
"composer": "2.9.8",
|
||||
"mysql": "mysql:8.4",
|
||||
"opensearch": "opensearchproject/opensearch:2.19.1",
|
||||
"rabbitmq": "rabbitmq:4.0-management",
|
||||
@@ -60,7 +58,7 @@
|
||||
"mage-os/project-community-edition:>=1.2 <1.3": {
|
||||
"magento": "mage-os/project-community-edition:>=1.2 <1.3",
|
||||
"php": 8.4,
|
||||
"composer": "2.9.5",
|
||||
"composer": "2.9.8",
|
||||
"mysql": "mysql:8.4",
|
||||
"opensearch": "opensearchproject/opensearch:2.19.1",
|
||||
"rabbitmq": "rabbitmq:4.0-management",
|
||||
@@ -74,7 +72,7 @@
|
||||
"mage-os/project-community-edition:>=1.3 <1.4": {
|
||||
"magento": "mage-os/project-community-edition:>=1.2 <1.3",
|
||||
"php": 8.4,
|
||||
"composer": "2.9.5",
|
||||
"composer": "2.9.8",
|
||||
"mysql": "mysql:8.4",
|
||||
"opensearch": "opensearchproject/opensearch:2.19.1",
|
||||
"rabbitmq": "rabbitmq:4.0-management",
|
||||
@@ -88,7 +86,7 @@
|
||||
"mage-os/project-community-edition:>=2.0 <2.1": {
|
||||
"magento": "mage-os/project-community-edition:>=2.0 <2.1",
|
||||
"php": 8.4,
|
||||
"composer": "2.9.5",
|
||||
"composer": "2.9.8",
|
||||
"mysql": "mysql:8.4",
|
||||
"opensearch": "opensearchproject/opensearch:2.19.1",
|
||||
"rabbitmq": "rabbitmq:4.0-management",
|
||||
@@ -102,7 +100,7 @@
|
||||
"mage-os/project-community-edition:>=2.1 <2.2": {
|
||||
"magento": "mage-os/project-community-edition:>=2.1 <2.2",
|
||||
"php": 8.4,
|
||||
"composer": "2.9.5",
|
||||
"composer": "2.9.8",
|
||||
"mysql": "mysql:8.4",
|
||||
"opensearch": "opensearchproject/opensearch:2.19.1",
|
||||
"rabbitmq": "rabbitmq:4.0-management",
|
||||
@@ -116,7 +114,7 @@
|
||||
"mage-os/project-community-edition:>=2.2 <2.3": {
|
||||
"magento": "mage-os/project-community-edition:>=2.2 <2.3",
|
||||
"php": 8.4,
|
||||
"composer": "2.9.3",
|
||||
"composer": "2.9.8",
|
||||
"mysql": "mysql:8.4",
|
||||
"opensearch": "opensearchproject/opensearch:3",
|
||||
"rabbitmq": "rabbitmq:4.1-management",
|
||||
@@ -126,6 +124,34 @@
|
||||
"nginx": "nginx:1.28",
|
||||
"os": "ubuntu-latest",
|
||||
"release": "2026-03-10T00:00:00+0000",
|
||||
"eol": "2029-04-15T00:00:00+0000"
|
||||
"eol": "2026-05-13T00:00:00+0000"
|
||||
},
|
||||
"mage-os/project-community-edition:>=2.3 <2.4": {
|
||||
"magento": "mage-os/project-community-edition:>=2.3 <2.4",
|
||||
"php": 8.4,
|
||||
"composer": "2.9.8",
|
||||
"mysql": "mysql:8.4",
|
||||
"opensearch": "opensearchproject/opensearch:2.19.1",
|
||||
"rabbitmq": "rabbitmq:4.0-management",
|
||||
"redis": "redis:7.2",
|
||||
"varnish": "varnish:7.6",
|
||||
"nginx": "nginx:1.26",
|
||||
"os": "ubuntu-latest",
|
||||
"release": "2026-05-13T00:00:00+0000",
|
||||
"eol": "2026-05-19T00:00:00+0000"
|
||||
},
|
||||
"mage-os/project-community-edition:>=3.0 <3.1": {
|
||||
"magento": "mage-os/project-community-edition:>=3.0 <3.1",
|
||||
"php": 8.5,
|
||||
"composer": "2.9.8",
|
||||
"mysql": "mysql:8.4",
|
||||
"opensearch": "opensearchproject/opensearch:3",
|
||||
"rabbitmq": "rabbitmq:4.2-management",
|
||||
"valkey": "valkey/valkey:9",
|
||||
"varnish": "varnish:8",
|
||||
"nginx": "nginx:1.28",
|
||||
"os": "ubuntu-latest",
|
||||
"release": "2026-05-19T00:00:00+0000",
|
||||
"eol": "2029-05-19T00:00:00+0000"
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
"magento": "mage-os/project-community-edition:1.0.0",
|
||||
"upstream": "2.4.6-p3",
|
||||
"php": 8.1,
|
||||
"composer": "2.2.21",
|
||||
"composer": "2.2.28",
|
||||
"mysql": "mysql:8.0",
|
||||
"elasticsearch": "elasticsearch:8.5.3",
|
||||
"rabbitmq": "rabbitmq:3.9-management",
|
||||
@@ -18,7 +18,7 @@
|
||||
"magento": "mage-os/project-community-edition:1.0.1",
|
||||
"upstream": "2.4.6-p3",
|
||||
"php": 8.1,
|
||||
"composer": "2.2.21",
|
||||
"composer": "2.2.28",
|
||||
"mysql": "mysql:8.0",
|
||||
"elasticsearch": "elasticsearch:8.5.3",
|
||||
"rabbitmq": "rabbitmq:3.9-management",
|
||||
@@ -33,7 +33,7 @@
|
||||
"magento": "mage-os/project-community-edition:1.0.2",
|
||||
"upstream": "2.4.7-p1",
|
||||
"php": 8.3,
|
||||
"composer": "2.7.4",
|
||||
"composer": "2.9.8",
|
||||
"mysql": "mariadb:10.6",
|
||||
"elasticsearch": "elasticsearch:8.11.4",
|
||||
"rabbitmq": "rabbitmq:3.13-management",
|
||||
@@ -48,7 +48,7 @@
|
||||
"magento": "mage-os/project-community-edition:1.0.3",
|
||||
"upstream": "2.4.7-p1",
|
||||
"php": 8.3,
|
||||
"composer": "2.7.4",
|
||||
"composer": "2.9.8",
|
||||
"mysql": "mariadb:10.6",
|
||||
"elasticsearch": "elasticsearch:8.11.4",
|
||||
"rabbitmq": "rabbitmq:3.13-management",
|
||||
@@ -63,7 +63,7 @@
|
||||
"magento": "mage-os/project-community-edition:1.0.4",
|
||||
"upstream": "2.4.7-p2",
|
||||
"php": 8.3,
|
||||
"composer": "2.7.4",
|
||||
"composer": "2.9.8",
|
||||
"mysql": "mariadb:10.6",
|
||||
"elasticsearch": "elasticsearch:8.11.4",
|
||||
"rabbitmq": "rabbitmq:3.13-management",
|
||||
@@ -78,7 +78,7 @@
|
||||
"magento": "mage-os/project-community-edition:1.0.5",
|
||||
"upstream": "2.4.7-p3",
|
||||
"php": 8.3,
|
||||
"composer": "2.7.4",
|
||||
"composer": "2.9.8",
|
||||
"mysql": "mariadb:10.6",
|
||||
"elasticsearch": "elasticsearch:8.11.4",
|
||||
"rabbitmq": "rabbitmq:3.13-management",
|
||||
@@ -93,7 +93,7 @@
|
||||
"magento": "mage-os/project-community-edition:1.0.6",
|
||||
"upstream": "2.4.7-p4",
|
||||
"php": 8.3,
|
||||
"composer": "2.7.4",
|
||||
"composer": "2.9.8",
|
||||
"mysql": "mariadb:10.6",
|
||||
"elasticsearch": "elasticsearch:8.11.4",
|
||||
"rabbitmq": "rabbitmq:3.13-management",
|
||||
@@ -108,7 +108,7 @@
|
||||
"magento": "mage-os/project-community-edition:1.1.0",
|
||||
"upstream": "2.4.8",
|
||||
"php": 8.4,
|
||||
"composer": "2.9.5",
|
||||
"composer": "2.9.8",
|
||||
"mysql": "mysql:8.4",
|
||||
"opensearch": "opensearchproject/opensearch:2.19.1",
|
||||
"rabbitmq": "rabbitmq:4.0-management",
|
||||
@@ -123,7 +123,7 @@
|
||||
"magento": "mage-os/project-community-edition:1.1.1",
|
||||
"upstream": "2.4.8",
|
||||
"php": 8.4,
|
||||
"composer": "2.9.5",
|
||||
"composer": "2.9.8",
|
||||
"mysql": "mysql:8.4",
|
||||
"opensearch": "opensearchproject/opensearch:2.19.1",
|
||||
"rabbitmq": "rabbitmq:4.0-management",
|
||||
@@ -138,7 +138,7 @@
|
||||
"magento": "mage-os/project-community-edition:1.2.0",
|
||||
"upstream": "2.4.8-p1",
|
||||
"php": 8.4,
|
||||
"composer": "2.9.5",
|
||||
"composer": "2.9.8",
|
||||
"mysql": "mysql:8.4",
|
||||
"opensearch": "opensearchproject/opensearch:2.19.1",
|
||||
"rabbitmq": "rabbitmq:4.0-management",
|
||||
@@ -153,7 +153,7 @@
|
||||
"magento": "mage-os/project-community-edition:1.3.0",
|
||||
"upstream": "2.4.8-p2",
|
||||
"php": 8.4,
|
||||
"composer": "2.9.5",
|
||||
"composer": "2.9.8",
|
||||
"mysql": "mysql:8.4",
|
||||
"opensearch": "opensearchproject/opensearch:2.19.1",
|
||||
"rabbitmq": "rabbitmq:4.0-management",
|
||||
@@ -168,7 +168,7 @@
|
||||
"magento": "mage-os/project-community-edition:1.3.1",
|
||||
"upstream": "2.4.8-p2",
|
||||
"php": 8.4,
|
||||
"composer": "2.9.5",
|
||||
"composer": "2.9.8",
|
||||
"mysql": "mysql:8.4",
|
||||
"opensearch": "opensearchproject/opensearch:2.19.1",
|
||||
"rabbitmq": "rabbitmq:4.0-management",
|
||||
@@ -183,7 +183,7 @@
|
||||
"magento": "mage-os/project-community-edition:2.0.0",
|
||||
"upstream": "2.4.8-p3",
|
||||
"php": 8.4,
|
||||
"composer": "2.9.5",
|
||||
"composer": "2.9.8",
|
||||
"mysql": "mysql:8.4",
|
||||
"opensearch": "opensearchproject/opensearch:2.19.1",
|
||||
"rabbitmq": "rabbitmq:4.0-management",
|
||||
@@ -198,7 +198,7 @@
|
||||
"magento": "mage-os/project-community-edition:2.1.0",
|
||||
"upstream": "2.4.8-p3",
|
||||
"php": 8.4,
|
||||
"composer": "2.9.5",
|
||||
"composer": "2.9.8",
|
||||
"mysql": "mysql:8.4",
|
||||
"opensearch": "opensearchproject/opensearch:2.19.1",
|
||||
"rabbitmq": "rabbitmq:4.0-management",
|
||||
@@ -213,7 +213,7 @@
|
||||
"magento": "mage-os/project-community-edition:2.2.0",
|
||||
"upstream": "2.4.8-p4",
|
||||
"php": 8.4,
|
||||
"composer": "2.9.3",
|
||||
"composer": "2.9.8",
|
||||
"mysql": "mysql:8.4",
|
||||
"opensearch": "opensearchproject/opensearch:3",
|
||||
"rabbitmq": "rabbitmq:4.1-management",
|
||||
@@ -228,7 +228,7 @@
|
||||
"magento": "mage-os/project-community-edition:2.2.1",
|
||||
"upstream": "2.4.8-p4",
|
||||
"php": 8.4,
|
||||
"composer": "2.9.5",
|
||||
"composer": "2.9.8",
|
||||
"mysql": "mysql:8.4",
|
||||
"opensearch": "opensearchproject/opensearch:3",
|
||||
"rabbitmq": "rabbitmq:4.1-management",
|
||||
@@ -243,7 +243,7 @@
|
||||
"magento": "mage-os/project-community-edition:2.2.2",
|
||||
"upstream": "2.4.8-p4",
|
||||
"php": 8.4,
|
||||
"composer": "2.9.7",
|
||||
"composer": "2.9.8",
|
||||
"mysql": "mysql:8.4",
|
||||
"opensearch": "opensearchproject/opensearch:3",
|
||||
"rabbitmq": "rabbitmq:4.1-management",
|
||||
@@ -253,6 +253,36 @@
|
||||
"nginx": "nginx:1.28",
|
||||
"os": "ubuntu-latest",
|
||||
"release": "2026-04-15T00:00:00+0000",
|
||||
"eol": "2029-04-15T00:00:00+0000"
|
||||
"eol": "2026-05-13T00:00:00+0000"
|
||||
},
|
||||
"mage-os/project-community-edition:2.3.0": {
|
||||
"magento": "mage-os/project-community-edition:2.3.0",
|
||||
"upstream": "2.4.8-p5",
|
||||
"php": 8.4,
|
||||
"composer": "2.9.8",
|
||||
"mysql": "mysql:8.4",
|
||||
"opensearch": "opensearchproject/opensearch:2.19.1",
|
||||
"rabbitmq": "rabbitmq:4.0-management",
|
||||
"redis": "redis:7.2",
|
||||
"varnish": "varnish:7.6",
|
||||
"nginx": "nginx:1.26",
|
||||
"os": "ubuntu-latest",
|
||||
"release": "2026-05-13T00:00:00+0000",
|
||||
"eol": "2026-05-19T00:00:00+0000"
|
||||
},
|
||||
"mage-os/project-community-edition:3.0.0": {
|
||||
"magento": "mage-os/project-community-edition:3.0.0",
|
||||
"upstream": "2.4.9",
|
||||
"php": 8.5,
|
||||
"composer": "2.9.8",
|
||||
"mysql": "mysql:8.4",
|
||||
"opensearch": "opensearchproject/opensearch:3",
|
||||
"rabbitmq": "rabbitmq:4.2-management",
|
||||
"valkey": "valkey/valkey:9",
|
||||
"varnish": "varnish:8",
|
||||
"nginx": "nginx:1.28",
|
||||
"os": "ubuntu-latest",
|
||||
"release": "2026-05-19T00:00:00+0000",
|
||||
"eol": "2029-05-19T00:00:00+0000"
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user