mirror of
https://github.com/graycoreio/github-actions-magento2.git
synced 2026-06-08 19:46:41 +00:00
feat(cache-magento): add stamp caching for vendor/ directory (#245)
Adds an opt-in `stamp` mode that caches the extracted `vendor/` directory in addition to the Composer download cache. On a warm hit, `composer install` is effectively a no-op and shaves 2–5 minutes off a full Magento install.
This commit is contained in:
+64
-25
@@ -4,35 +4,74 @@ A Github Action that creates a composer cache for a Magento extension or store.
|
||||
|
||||
## Inputs
|
||||
|
||||
|
||||
See the [action.yml](./action.yml)
|
||||
|
||||
| Input | Description | Required | Default |
|
||||
| ------------------ | -------------------------------------------------------------------------------------- | -------- | ------------ |
|
||||
| composer_cache_key | A key to version the composer cache. Can be incremented if you need to bust the cache. | false | '__mageos' |
|
||||
| -------------------- | -------------------------------------------------------------------------------------- | -------- | ---------- |
|
||||
| `composer_cache_key` | A key to version the composer cache. Can be incremented if you need to bust the cache. | false | `__mageos` |
|
||||
| `working-directory` | The directory where Magento is installed (location of `vendor/` and `composer.lock`). | false | `.` |
|
||||
| `stamp` | Cache the `vendor/` directory in addition to the Composer download cache. | false | `false` |
|
||||
|
||||
### Usage
|
||||
## Cache keys
|
||||
|
||||
The download cache key has the format:
|
||||
|
||||
```
|
||||
composer | v5.8 | <os> | <composer_cache_key> | <composer-version> | <php-version>
|
||||
```
|
||||
|
||||
When `stamp: true`, the `vendor/` cache key has the format:
|
||||
|
||||
```
|
||||
composer | stamp | v5.8 | <os> | <composer_cache_key> | <composer-version> | <php-version> | <composer.lock-hash>
|
||||
```
|
||||
|
||||
The `composer.lock` hash is derived from `working-directory/composer.lock` using `hashFiles`. The download key also gains the hash suffix when a Magento product package is detected at `working-directory`.
|
||||
|
||||
## Usage
|
||||
|
||||
### Extension (download cache only)
|
||||
|
||||
```yml
|
||||
name: Magento Cache
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
showcase_cache:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: graycoreio/github-actions-magento2/cache-magento@main
|
||||
id: cache-magento
|
||||
|
||||
- run: composer install
|
||||
shell: bash
|
||||
name: Composer install
|
||||
- uses: graycoreio/github-actions-magento2/cache-magento@main
|
||||
with:
|
||||
composer_cache_key: ${{ inputs.composer_cache_key }}
|
||||
```
|
||||
|
||||
### Extension or store (download + vendor stamp cache)
|
||||
|
||||
```yml
|
||||
- uses: graycoreio/github-actions-magento2/setup-magento@main
|
||||
id: setup-magento
|
||||
with:
|
||||
mode: extension # or store
|
||||
# ...
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/cache-magento@main
|
||||
with:
|
||||
composer_cache_key: ${{ inputs.composer_cache_key }}
|
||||
working-directory: ${{ steps.setup-magento.outputs.path }}
|
||||
stamp: true
|
||||
|
||||
- run: composer install
|
||||
working-directory: ${{ steps.setup-magento.outputs.path }}
|
||||
```
|
||||
|
||||
### Stamp Mode
|
||||
|
||||
On a warm cache hit, `composer install` completes in ~0s because `vendor/` is already present — Composer sees everything installed and exits immediately. For a full Magento install this saves 2–5 minutes of package extraction per job.
|
||||
|
||||
The trade-off is size. The `vendor/` directory for a Magento project runs 300–600 MB, so a frequent cache miss means you are consistently paying the upload cost without recouping it on the next run.
|
||||
|
||||
As such, use `stamp: true` when `composer.lock` is stable across most runs — a store on a release branch, or extension CI against a pinned Magento version. Skip it when the lock changes often or when runner storage is constrained.
|
||||
|
||||
> [!WARNING]
|
||||
> **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@main
|
||||
> with:
|
||||
> stamp: ${{ github.actor != 'dependabot[bot]' }}
|
||||
> ```
|
||||
>
|
||||
> If you use Renovate, check its bot account name and adjust the condition accordingly. Dependabot PRs will pay full `composer install` time on every run, which is acceptable — nobody is waiting on them.
|
||||
|
||||
@@ -7,11 +7,31 @@ inputs:
|
||||
required: false
|
||||
default: "__mageos"
|
||||
description: A key to version the composer cache. Can be incremented if you need to bust the cache.
|
||||
working-directory:
|
||||
required: false
|
||||
default: "."
|
||||
description: "The working directory where Magento is installed (location of vendor/ and composer.lock)"
|
||||
stamp:
|
||||
required: false
|
||||
default: "false"
|
||||
description: "Cache the vendor/ directory in addition to the Composer download cache"
|
||||
exclude-from-stamp:
|
||||
required: false
|
||||
default: ""
|
||||
description: |
|
||||
Newline-separated list of Composer package names to exclude from the stamp cache (e.g. magento/module-foo).
|
||||
magento/magento2-base and mage-os/magento2-base are always excluded regardless of this input.
|
||||
|
||||
outputs:
|
||||
cache-hit:
|
||||
description: "A boolean value to indicate an exact match was found for the key"
|
||||
value: ${{ steps.cache-magento-cache.outputs.cache-hit }}
|
||||
key:
|
||||
description: "The cache key used for the Composer download cache"
|
||||
value: ${{ steps.cache-magento-keys.outputs.download-key }}
|
||||
stamp-key:
|
||||
description: "The cache key used for the vendor/ stamp cache (only set when stamp: true)"
|
||||
value: ${{ steps.cache-magento-keys.outputs.stamp-key }}
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
@@ -31,6 +51,31 @@ runs:
|
||||
name: Compute Composer Version
|
||||
id: cache-magento-get-composer-version
|
||||
|
||||
- name: Validate working-directory is inside GITHUB_WORKSPACE
|
||||
if: inputs.stamp == 'true'
|
||||
shell: bash
|
||||
run: |
|
||||
WORKING_DIR="$(realpath '${{ inputs.working-directory }}')"
|
||||
WORKSPACE="$(realpath '${{ github.workspace }}')"
|
||||
if [[ "$WORKING_DIR" != "$WORKSPACE"* ]]; then
|
||||
echo "::error::cache-magento: working-directory '${{ inputs.working-directory }}' resolves outside GITHUB_WORKSPACE. You cannot use the `stamp` cache in folders outside the workspace."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- uses: graycoreio/github-actions-magento2/get-magento-version@main
|
||||
id: cache-magento-get-magento-version
|
||||
with:
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
|
||||
- name: Validate composer.lock exists for stamp cache (store mode)
|
||||
if: inputs.stamp == 'true' && steps.cache-magento-get-magento-version.outputs.project != ''
|
||||
shell: bash
|
||||
run: |
|
||||
if [[ ! -f "${{ inputs.working-directory }}/composer.lock" ]]; then
|
||||
echo "::error::cache-magento: stamp: true on a store requires a '${{ inputs.working-directory }}/composer.lock' to exist."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Compute cache keys
|
||||
id: cache-magento-keys
|
||||
shell: bash
|
||||
@@ -40,6 +85,8 @@ runs:
|
||||
"${{ runner.os }}" \
|
||||
"${{ steps.cache-magento-get-php-version.outputs.version }}" \
|
||||
"${{ steps.cache-magento-get-composer-version.outputs.version }}" \
|
||||
"${{ steps.cache-magento-get-magento-version.outputs.project }}" \
|
||||
"${{ hashFiles(format('{0}/composer.lock', inputs.working-directory)) }}" \
|
||||
>> $GITHUB_OUTPUT
|
||||
|
||||
- name: "Cache Composer Packages"
|
||||
@@ -47,8 +94,33 @@ runs:
|
||||
id: cache-magento-cache
|
||||
with:
|
||||
key: ${{ steps.cache-magento-keys.outputs.download-key }}
|
||||
restore-keys: |
|
||||
${{ steps.cache-magento-keys.outputs.download-restore-key }}
|
||||
path: ${{ steps.cache-magento-composer-cache.outputs.dir }}
|
||||
|
||||
- name: Compute stamp paths
|
||||
id: cache-magento-stamp-paths
|
||||
if: inputs.stamp == 'true'
|
||||
shell: bash
|
||||
env:
|
||||
EXCLUDE_FROM_STAMP: ${{ inputs.exclude-from-stamp }}
|
||||
WORKING_DIR: ${{ inputs.working-directory }}
|
||||
run: |
|
||||
PATHS=$(bash "${{ github.action_path }}/compute-stamp-paths.sh" "$(realpath "$WORKING_DIR")" "$EXCLUDE_FROM_STAMP")
|
||||
{
|
||||
echo "paths<<EOF"
|
||||
echo "$PATHS"
|
||||
echo "EOF"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: "Cache Vendor (Stamp)"
|
||||
uses: actions/cache@v5
|
||||
if: inputs.stamp == 'true'
|
||||
with:
|
||||
key: ${{ steps.cache-magento-keys.outputs.stamp-key }}
|
||||
path: |
|
||||
${{ steps.cache-magento-stamp-paths.outputs.paths }}
|
||||
|
||||
branding:
|
||||
icon: "code"
|
||||
color: "green"
|
||||
|
||||
@@ -1,8 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
# Args: composer_cache_key os php_version composer_version
|
||||
# Args: composer_cache_key os php_version composer_version project lock_hash
|
||||
COMPOSER_CACHE_KEY="$1"
|
||||
OS="$2"
|
||||
PHP_VERSION="$3"
|
||||
COMPOSER_VERSION="$4"
|
||||
PROJECT="$5"
|
||||
LOCK_HASH="${6:-}"
|
||||
|
||||
echo "download-key=composer | v5.8 | ${OS} | ${COMPOSER_CACHE_KEY} | ${COMPOSER_VERSION} | ${PHP_VERSION}"
|
||||
MODE="extension"
|
||||
SUFFIX=""
|
||||
if [ -n "$PROJECT" ]; then
|
||||
MODE="store"
|
||||
[ -n "$LOCK_HASH" ] && SUFFIX=" | $LOCK_HASH"
|
||||
fi
|
||||
|
||||
BASE="composer | v5.8 | ${OS} | ${MODE} | ${COMPOSER_CACHE_KEY} | ${COMPOSER_VERSION} | ${PHP_VERSION}"
|
||||
echo "download-key=${BASE}${SUFFIX}"
|
||||
echo "download-restore-key=${BASE}"
|
||||
echo "stamp-key=composer | stamp | v5.8 | ${OS} | ${MODE} | ${COMPOSER_CACHE_KEY} | ${COMPOSER_VERSION} | ${PHP_VERSION}${SUFFIX}"
|
||||
|
||||
Executable
+18
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env bash
|
||||
# Args: working_directory exclude_from_stamp
|
||||
# working_directory: absolute path (caller is responsible for realpath resolution)
|
||||
# exclude_from_stamp: newline-separated list of composer package names to exclude
|
||||
WORKING_DIR="$1"
|
||||
EXCLUDE_FROM_STAMP="${2:-}"
|
||||
VENDOR="${WORKING_DIR}/vendor"
|
||||
|
||||
PATHS="${VENDOR}/**"$'\n'"!${VENDOR}/**/"$'\n'"!${VENDOR}/magento/magento2-base"$'\n'"!${VENDOR}/magento/magento2-base/**"$'\n'"!${VENDOR}/mage-os/magento2-base"$'\n'"!${VENDOR}/mage-os/magento2-base/**"
|
||||
|
||||
while IFS= read -r pkg; do
|
||||
pkg="${pkg#"${pkg%%[![:space:]]*}"}"
|
||||
pkg="${pkg%"${pkg##*[![:space:]]}"}"
|
||||
[[ -z "$pkg" ]] && continue
|
||||
PATHS="${PATHS}"$'\n'"!${VENDOR}/${pkg}"$'\n'"!${VENDOR}/${pkg}/**"
|
||||
done <<< "$EXCLUDE_FROM_STAMP"
|
||||
|
||||
echo "$PATHS"
|
||||
Generated
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"packages": [
|
||||
{
|
||||
"name": "magento/product-community-edition",
|
||||
"version": "2.4.7"
|
||||
}
|
||||
],
|
||||
"packages-dev": []
|
||||
}
|
||||
+82
-11
@@ -2,6 +2,8 @@
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
SCRIPT="$SCRIPT_DIR/compute-cache-keys.sh"
|
||||
SCRIPT_STAMP="$SCRIPT_DIR/compute-stamp-paths.sh"
|
||||
LOCK_HASH=$(sha256sum "$SCRIPT_DIR/fixtures/composer.lock" | cut -d' ' -f1)
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
@@ -22,23 +24,92 @@ field() {
|
||||
echo "$1" | grep "^${2}=" | cut -d= -f2-
|
||||
}
|
||||
|
||||
# Default cache key (Linux)
|
||||
OUT=$(bash "$SCRIPT" "_mageos" "Linux" "8.3.0" "2.2.6")
|
||||
assert_eq "linux: download-key" \
|
||||
"composer | v5.8 | Linux | _mageos | 2.2.6 | 8.3.0" \
|
||||
# Extension mode: no project resolved, mode derived as "extension", no lock suffix
|
||||
OUT=$(bash "$SCRIPT" "_mageos" "Linux" "8.3.0" "2.2.6" "" "")
|
||||
assert_eq "extension: download-key" \
|
||||
"composer | v5.8 | Linux | extension | _mageos | 2.2.6 | 8.3.0" \
|
||||
"$(field "$OUT" download-key)"
|
||||
assert_eq "extension: download-restore-key" \
|
||||
"composer | v5.8 | Linux | extension | _mageos | 2.2.6 | 8.3.0" \
|
||||
"$(field "$OUT" download-restore-key)"
|
||||
assert_eq "extension: stamp-key" \
|
||||
"composer | stamp | v5.8 | Linux | extension | _mageos | 2.2.6 | 8.3.0" \
|
||||
"$(field "$OUT" stamp-key)"
|
||||
|
||||
# OS segment differentiates macOS from Linux
|
||||
OUT=$(bash "$SCRIPT" "_mageos" "macOS" "8.3.0" "2.2.6")
|
||||
assert_eq "macos: download-key" \
|
||||
"composer | v5.8 | macOS | _mageos | 2.2.6 | 8.3.0" \
|
||||
# Store mode: project resolved, mode derived as "store", lock hash appended (restore-key drops lock for prefix match)
|
||||
OUT=$(bash "$SCRIPT" "_mageos" "Linux" "8.3.0" "2.2.6" "magento/project-community-edition" "$LOCK_HASH")
|
||||
assert_eq "store: download-key" \
|
||||
"composer | v5.8 | Linux | store | _mageos | 2.2.6 | 8.3.0 | $LOCK_HASH" \
|
||||
"$(field "$OUT" download-key)"
|
||||
assert_eq "store: download-restore-key" \
|
||||
"composer | v5.8 | Linux | store | _mageos | 2.2.6 | 8.3.0" \
|
||||
"$(field "$OUT" download-restore-key)"
|
||||
assert_eq "store: stamp-key" \
|
||||
"composer | stamp | v5.8 | Linux | store | _mageos | 2.2.6 | 8.3.0 | $LOCK_HASH" \
|
||||
"$(field "$OUT" stamp-key)"
|
||||
|
||||
# Custom composer_cache_key
|
||||
OUT=$(bash "$SCRIPT" "custom-v2" "Linux" "8.1.5" "2.4.2")
|
||||
# Store mode without composer.lock (e.g. stamp=false on a store before `composer install`):
|
||||
# lock_hash is empty, so keys must not carry a trailing " | " with an empty hash slot.
|
||||
OUT=$(bash "$SCRIPT" "_mageos" "Linux" "8.3.0" "2.2.6" "magento/project-community-edition" "")
|
||||
assert_eq "store no-lock: download-key" \
|
||||
"composer | v5.8 | Linux | store | _mageos | 2.2.6 | 8.3.0" \
|
||||
"$(field "$OUT" download-key)"
|
||||
assert_eq "store no-lock: download-restore-key" \
|
||||
"composer | v5.8 | Linux | store | _mageos | 2.2.6 | 8.3.0" \
|
||||
"$(field "$OUT" download-restore-key)"
|
||||
assert_eq "store no-lock: stamp-key" \
|
||||
"composer | stamp | v5.8 | Linux | store | _mageos | 2.2.6 | 8.3.0" \
|
||||
"$(field "$OUT" stamp-key)"
|
||||
|
||||
# Custom composer_cache_key, no project resolved
|
||||
OUT=$(bash "$SCRIPT" "custom-v2" "Linux" "8.1.5" "2.4.2" "" "")
|
||||
assert_eq "custom key: download-key" \
|
||||
"composer | v5.8 | Linux | custom-v2 | 2.4.2 | 8.1.5" \
|
||||
"composer | v5.8 | Linux | extension | custom-v2 | 2.4.2 | 8.1.5" \
|
||||
"$(field "$OUT" download-key)"
|
||||
assert_eq "custom key: download-restore-key" \
|
||||
"composer | v5.8 | Linux | extension | custom-v2 | 2.4.2 | 8.1.5" \
|
||||
"$(field "$OUT" download-restore-key)"
|
||||
assert_eq "custom key: stamp-key" \
|
||||
"composer | stamp | v5.8 | Linux | extension | custom-v2 | 2.4.2 | 8.1.5" \
|
||||
"$(field "$OUT" stamp-key)"
|
||||
|
||||
# Stamp paths: no excludes — base paths only, with magento2-base always excluded
|
||||
OUT=$(bash "$SCRIPT_STAMP" "/work" "")
|
||||
EXPECTED="/work/vendor/**
|
||||
!/work/vendor/**/
|
||||
!/work/vendor/magento/magento2-base
|
||||
!/work/vendor/magento/magento2-base/**
|
||||
!/work/vendor/mage-os/magento2-base
|
||||
!/work/vendor/mage-os/magento2-base/**"
|
||||
assert_eq "stamp paths: no excludes" "$EXPECTED" "$OUT"
|
||||
|
||||
# Stamp paths: single exclude appended after the always-excluded base entries
|
||||
OUT=$(bash "$SCRIPT_STAMP" "/work" "magento/module-foo")
|
||||
EXPECTED="/work/vendor/**
|
||||
!/work/vendor/**/
|
||||
!/work/vendor/magento/magento2-base
|
||||
!/work/vendor/magento/magento2-base/**
|
||||
!/work/vendor/mage-os/magento2-base
|
||||
!/work/vendor/mage-os/magento2-base/**
|
||||
!/work/vendor/magento/module-foo
|
||||
!/work/vendor/magento/module-foo/**"
|
||||
assert_eq "stamp paths: single exclude" "$EXPECTED" "$OUT"
|
||||
|
||||
# Stamp paths: multiple excludes, with whitespace and blank lines tolerated
|
||||
OUT=$(bash "$SCRIPT_STAMP" "/work" "$(printf 'magento/module-foo\n magento/module-bar \n\nvendor/pkg-baz\n')")
|
||||
EXPECTED="/work/vendor/**
|
||||
!/work/vendor/**/
|
||||
!/work/vendor/magento/magento2-base
|
||||
!/work/vendor/magento/magento2-base/**
|
||||
!/work/vendor/mage-os/magento2-base
|
||||
!/work/vendor/mage-os/magento2-base/**
|
||||
!/work/vendor/magento/module-foo
|
||||
!/work/vendor/magento/module-foo/**
|
||||
!/work/vendor/magento/module-bar
|
||||
!/work/vendor/magento/module-bar/**
|
||||
!/work/vendor/vendor/pkg-baz
|
||||
!/work/vendor/vendor/pkg-baz/**"
|
||||
assert_eq "stamp paths: multiple excludes with whitespace and blank lines" "$EXPECTED" "$OUT"
|
||||
|
||||
echo ""
|
||||
echo "$PASS passed, $FAIL failed"
|
||||
|
||||
Reference in New Issue
Block a user