diff --git a/.github/workflows/build-php-laravel.yaml b/.github/workflows/build-php-laravel.yaml new file mode 100644 index 0000000..ae8b6dc --- /dev/null +++ b/.github/workflows/build-php-laravel.yaml @@ -0,0 +1,107 @@ +name: Build PHP (Laravel) +on: + workflow_call: + inputs: + php_version: + required: false + type: string + default: "8.3" + php_extensions: + required: false + type: string + default: "bcmath intl gd" + build_cli_image: + required: false + type: boolean + default: false + dockerfile_app_path: + required: false + type: string + default: "./build/Dockerfile-app" + dockerfile_webserver_path: + required: false + type: string + default: "./build/Dockerfile-nginx" + webserver_tag_prefix: + required: false + type: string + default: "webserver-" + dockerfile_cli_path: + required: false + type: string + default: "./build/Dockerfile-cli" + release_branches: + required: false + type: string + default: "stage,hotfix,rc" + secrets: + packagist_username: + required: true + packagist_password: + required: true + gh_token: + required: true + outputs: + tag: + description: "The released tag" + value: ${{ jobs.tag-and-release.outputs.tag }} + +jobs: + calculate-tag: + name: Calculate Build Tag + runs-on: ubuntu-latest + outputs: + tag: ${{ steps.dry_tag_version.outputs.new_tag }} + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + - id: dry_tag_version + uses: mathieudutour/github-tag-action@v6.1 + with: + github_token: ${{ secrets.gh_token }} + release_branches: ${{ inputs.release_branches }} + fetch_all_tags: true + dry_run: true + + build: + needs: [calculate-tag] + uses: ./.github/workflows/php-laravel-build-push.yaml + with: + php_version: ${{ inputs.php_version }} + php_extensions: ${{ inputs.php_extensions }} + build_app_image: true + build_webserver_image: true + build_cli_image: ${{ inputs.build_cli_image }} + dockerfile_app_path: ${{ inputs.dockerfile_app_path }} + dockerfile_webserver_path: ${{ inputs.dockerfile_webserver_path }} + webserver_tag_prefix: ${{ inputs.webserver_tag_prefix }} + dockerfile_cli_path: ${{ inputs.dockerfile_cli_path }} + new_tag: ${{ needs.calculate-tag.outputs.tag }} + secrets: + packagist_username: ${{ secrets.packagist_username }} + packagist_password: ${{ secrets.packagist_password }} + gh_token: ${{ secrets.gh_token }} + + tag-and-release: + name: Github Tag and Release + needs: [build] + runs-on: ubuntu-latest + outputs: + tag: ${{ steps.tag_version.outputs.new_tag }} + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + - id: tag_version + uses: mathieudutour/github-tag-action@v6.1 + with: + github_token: ${{ secrets.gh_token }} + release_branches: ${{ inputs.release_branches }} + fetch_all_tags: true + - uses: ncipollo/release-action@v1 + with: + tag: ${{ steps.tag_version.outputs.new_tag }} + name: Prerelease ${{ steps.tag_version.outputs.new_tag }} + body: ${{ steps.tag_version.outputs.changelog }} + prerelease: true diff --git a/.github/workflows/build-php-v1.yaml b/.github/workflows/build-php-v1.yaml new file mode 100644 index 0000000..96cf2b9 --- /dev/null +++ b/.github/workflows/build-php-v1.yaml @@ -0,0 +1,92 @@ +name: Build PHP (v1) +on: + workflow_call: + inputs: + images: + description: 'JSON array of {image_name?, dockerfile, target?, tag_prefix, extra_tag}' + required: true + type: string + php_version: + required: false + type: string + default: "8.3" + release_branches: + required: false + type: string + default: "stage,hotfix,rc" + cache_type: + required: false + type: string + default: "gha" + secrets: + packagist_username: + required: true + packagist_password: + required: true + gh_token: + required: true + outputs: + tag: + description: "The released tag" + value: ${{ jobs.tag-and-release.outputs.tag }} + +jobs: + calculate-tag: + name: Calculate Build Tag + runs-on: ubuntu-latest + outputs: + tag: ${{ steps.dry_tag_version.outputs.new_tag }} + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + - id: dry_tag_version + uses: mathieudutour/github-tag-action@v6.1 + with: + github_token: ${{ secrets.gh_token }} + release_branches: ${{ inputs.release_branches }} + fetch_all_tags: true + dry_run: true + + build: + name: Build ${{ matrix.image.tag_prefix }}image + needs: [calculate-tag] + strategy: + matrix: + image: ${{ fromJSON(inputs.images) }} + uses: ./.github/workflows/php-build-push.yaml + with: + php_version: ${{ inputs.php_version }} + image_name: ${{ matrix.image.image_name }} + dockerfile: ${{ matrix.image.dockerfile }} + build_target: ${{ matrix.image.target }} + tag: ${{ matrix.image.tag_prefix }}${{ needs.calculate-tag.outputs.tag }} + extra_tag: ${{ matrix.image.extra_tag }} + cache_type: ${{ inputs.cache_type }} + secrets: + packagist_username: ${{ secrets.packagist_username }} + packagist_password: ${{ secrets.packagist_password }} + gh_token: ${{ secrets.gh_token }} + + tag-and-release: + name: Github Tag and Release + needs: [build] + runs-on: ubuntu-latest + outputs: + tag: ${{ steps.tag_version.outputs.new_tag }} + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + - id: tag_version + uses: mathieudutour/github-tag-action@v6.1 + with: + github_token: ${{ secrets.gh_token }} + release_branches: ${{ inputs.release_branches }} + fetch_all_tags: true + - uses: ncipollo/release-action@v1 + with: + tag: ${{ steps.tag_version.outputs.new_tag }} + name: Prerelease ${{ steps.tag_version.outputs.new_tag }} + body: ${{ steps.tag_version.outputs.changelog }} + prerelease: true diff --git a/.github/workflows/php-build-push.yaml b/.github/workflows/php-build-push.yaml index 76bc740..9922f7a 100644 --- a/.github/workflows/php-build-push.yaml +++ b/.github/workflows/php-build-push.yaml @@ -25,6 +25,21 @@ on: required: false type: string default: "app" + image_name: + description: "Image repo path override for separate images, e.g. -profiler. Empty = github.repository." + required: false + type: string + default: "" + extra_tag: + description: "Optional companion tag to also push (e.g. latest, nginx-latest)." + required: false + type: string + default: "" + cache_type: + description: "Buildx cache backend: gha or registry." + required: false + type: string + default: "registry" secrets: packagist_username: required: true @@ -79,26 +94,55 @@ jobs: } - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 - + uses: docker/setup-buildx-action@v3 - name: Login to GitHub Container Registry - uses: docker/login-action@v1 + uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.gh_token }} - - - name: Docker Build and Push App - uses: docker/build-push-action@v2 + - name: Resolve image name and tags + id: meta env: - REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository }} + IMAGE_NAME_INPUT: ${{ inputs.image_name }} + OWNER: ${{ github.repository_owner }} + DEFAULT_NAME: ${{ github.repository }} + TAG: ${{ inputs.tag }} + EXTRA: ${{ inputs.extra_tag }} + CACHE_TYPE: ${{ inputs.cache_type }} + DOCKERFILE: ${{ inputs.dockerfile }} + BUILD_TARGET: ${{ inputs.build_target }} + run: | + name="$IMAGE_NAME_INPUT" + if [ -z "$name" ]; then + name="$DEFAULT_NAME" + elif [ "${name#*/}" = "$name" ]; then + # bare name (no slash) → scope it under the repo owner, e.g. rp_api-profiler → encodium/rp_api-profiler + name="${OWNER}/${name}" + fi + ref="ghcr.io/${name}" + tags="${ref}:${TAG}" + [ -n "$EXTRA" ] && tags="${tags},${ref}:${EXTRA}" + echo "tags=${tags}" >> "$GITHUB_OUTPUT" + # Cache key = image + dockerfile + target, so matrix legs that share an image repo + # (e.g. app/nginx/apache all under /) don't collide on one cache. + disc="$(printf '%s-%s' "$DOCKERFILE" "$BUILD_TARGET" | tr -cs 'A-Za-z0-9' '-' | sed 's/^-*//;s/-*$//')" + if [ "$CACHE_TYPE" = "gha" ]; then + scope="${name//\//-}-${disc}" + echo "cache_from=type=gha,scope=${scope}" >> "$GITHUB_OUTPUT" + echo "cache_to=type=gha,scope=${scope},mode=max" >> "$GITHUB_OUTPUT" + else + echo "cache_from=type=registry,ref=${ref}:buildcache-${disc}" >> "$GITHUB_OUTPUT" + echo "cache_to=type=registry,ref=${ref}:buildcache-${disc},mode=max" >> "$GITHUB_OUTPUT" + fi + - name: Docker Build and Push + uses: docker/build-push-action@v6 with: context: . file: ${{ inputs.dockerfile }} push: true platforms: linux/amd64 target: ${{ inputs.build_target }} - tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ inputs.tag }} - cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache - cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache,mode=max \ No newline at end of file + tags: ${{ steps.meta.outputs.tags }} + cache-from: ${{ steps.meta.outputs.cache_from }} + cache-to: ${{ steps.meta.outputs.cache_to }} \ No newline at end of file diff --git a/.github/workflows/php-laravel-build-push.yaml b/.github/workflows/php-laravel-build-push.yaml index 7e720f8..9d23744 100644 --- a/.github/workflows/php-laravel-build-push.yaml +++ b/.github/workflows/php-laravel-build-push.yaml @@ -42,6 +42,11 @@ on: required: false type: string default: "./build/Dockerfile-nginx" + webserver_tag_prefix: + description: "Tag prefix for the webserver image (e.g. webserver- or nginx-)." + required: false + type: string + default: "webserver-" new_tag: type: string required: true @@ -145,7 +150,7 @@ jobs: push: true file: ${{ inputs.dockerfile_webserver_path }} platforms: linux/amd64 - tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:webserver-${{ inputs.new_tag }},${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:webserver-latest + tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ inputs.webserver_tag_prefix }}${{ inputs.new_tag }},${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ inputs.webserver_tag_prefix }}latest cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache-webserver cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache-webserver,mode=max build-and-push-cli-image: diff --git a/docs/superpowers/plans/2026-06-08-unify-php-build-workflows.md b/docs/superpowers/plans/2026-06-08-unify-php-build-workflows.md new file mode 100644 index 0000000..ab6b652 --- /dev/null +++ b/docs/superpowers/plans/2026-06-08-unify-php-build-workflows.md @@ -0,0 +1,839 @@ +# Unify PHP Build Workflows Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Replace the copy-pasted per-repo `Build.yaml` across 9 PHP services with two shared reusable orchestrators in `encodium/.github`, so fleet-wide build changes happen in one place. + +**Architecture:** Two reusable workflows — `build-php-v1.yaml` (matrix over the generic `php-build-push.yaml`, one call per image) and `build-php-laravel.yaml` (wraps `php-laravel-build-push.yaml`, artisan-cached). Both implement the spine `calculate-tag → build → tag-and-release` and expose a `tag` output. The deploy job stays in each repo's thin caller, because a reusable workflow cannot reference the caller's local `./.github/workflows/...` deploy file. + +**Tech Stack:** GitHub Actions reusable workflows (`workflow_call`), `docker/build-push-action`, `mathieudutour/github-tag-action`, `ncipollo/release-action`, `ghcr.io`. + +**Spec:** `docs/superpowers/specs/2026-06-08-unify-php-build-workflows-design.md` +**Tickets:** DEVEX-1630 (this plan); DEVEX-1629 (Phase 0 gate, separate, lands first). + +--- + +## How "tests" work here + +There is no unit-test harness for workflows. Each change is verified by: +1. **Static lint** — the repo's `action-lint.yml` / `actionlint` catches schema errors. +2. **Real run** — trigger via `gh workflow run` and inspect the result with `gh run view`. +3. **Tag-fidelity diff** — after a build run, list the pushed image tags and confirm they + **exactly match** the baseline tags the old `Build.yaml` produced. + +Reusable workflows are referenced by git ref. During development, callers reference the +orchestrator on the feature branch (`@DEVEX-1630-unify-php-build-workflows`); after the +shared-workflow PR merges to `main`, callers are flipped to `@main`. + +**Baseline capture (do once, before any change):** for each repo, record the current image +tags so you can diff later. + +```bash +for r in rp_api internal_api catalog_api license_api radmin webstore returns-api accounts-api vin_decoder_service; do + echo "== $r =="; gh api "repos/encodium/$r/packages/container/$r/versions" --jq '.[0:3][].metadata.container.tags' 2>/dev/null +done +``` + +--- + +## File Structure + +**`encodium/.github` (shared, merged first):** +- Modify: `.github/workflows/php-build-push.yaml` — add `image_name`, `extra_tag`, `cache_type` inputs; bump action versions. +- Create: `.github/workflows/build-php-v1.yaml` — v1 orchestrator. +- Create: `.github/workflows/build-php-laravel.yaml` — Laravel orchestrator. + +**Per service repo (one PR each):** +- Modify: `.github/workflows/Build.yaml` (or `build.yaml`) — collapse to thin caller + preserved deploy job. + +--- + +## Phase 0 — Integration deploy gate (DEVEX-1629, ships first) + +> Tracked under DEVEX-1629. Included here because it is the prerequisite and the gated job +> is what survives into the Phase 1 thin caller. If DEVEX-1629 is already merged, skip. + +Affected (integration-deploying repos): rp_api, catalog_api, internal_api, license_api, +returns-api, accounts-api, webstore. (radmin, vin_decoder_service deploy to staging — not +gated here.) + +### Task 0.1: Gate each integration-deploy job + +**Files:** Modify `.github/workflows/Build.yaml` (lowercase `build.yaml` for accounts-api) in each of the 7 repos. + +- [ ] **Step 1: Add the trigger gate.** In each repo's `integration-deploy` job, add the + `if:` line directly under the job key: + +```yaml + integration-deploy: + if: ${{ github.event_name == 'push' }} + # ...rest unchanged... +``` + +- [ ] **Step 2: Lint.** Run `actionlint .github/workflows/Build.yaml` (no errors). + +- [ ] **Step 3: Verify dispatch skips deploy.** Trigger a build via dispatch and confirm the + deploy job is skipped: + +```bash +gh workflow run "Build.yaml" --repo encodium/ --ref main +# wait, then: +gh run view --repo encodium/ --json jobs --jq '.jobs[] | {name,conclusion}' +# Expected: integration-deploy → "skipped" +``` + +- [ ] **Step 4: Commit (per repo, on a branch, PR per CLAUDE.md).** + +```bash +git commit -am "fix: gate integration deploy to push events (DEVEX-1629)" +``` + +--- + +## Phase 1A — Shared workflow foundation (`encodium/.github`) + +Work on branch `DEVEX-1630-unify-php-build-workflows`. + +### Task 1: Find existing consumers of `php-build-push.yaml` + +**Files:** none (investigation). + +- [ ] **Step 1: Grep the org for callers** so the modernization stays backward-compatible. + +```bash +gh search code --owner encodium "php-build-push.yaml@" 2>/dev/null +``` + +Expected: note every caller. The changes in Task 2 are additive (new optional inputs with +defaults) + version bumps, so existing callers keep working. `cache_type` defaults to the +**current** value (`registry`) to avoid changing their behavior; only our new orchestrator +passes `gha`. + +### Task 2: Modernize `php-build-push.yaml` + +**Files:** Modify `.github/workflows/php-build-push.yaml`. + +- [ ] **Step 1: Add inputs.** Under `on.workflow_call.inputs`, add (all optional; `image_name` + defaults to empty and is resolved to `github.repository` inside the job, since + expressions are not allowed in input defaults): + +```yaml + image_name: + description: "Image repo path override for separate images, e.g. -profiler. Empty = github.repository." + required: false + type: string + default: "" + extra_tag: + description: "Optional companion tag to also push (e.g. latest, nginx-latest)." + required: false + type: string + default: "" + cache_type: + description: "Buildx cache backend: gha or registry." + required: false + type: string + default: "registry" +``` + +- [ ] **Step 2: Replace the build job env + build step.** Resolve `image_name`, build a tags + list including the optional `extra_tag`, choose the cache backend. Replace the + `Docker Build and Push App` step and its surrounding `setup-buildx`/`login` with: + +```yaml + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.gh_token }} + - name: Resolve image name and tags + id: meta + env: + IMAGE_NAME_INPUT: ${{ inputs.image_name }} + DEFAULT_IMAGE: ghcr.io/${{ github.repository }} + TAG: ${{ inputs.tag }} + EXTRA: ${{ inputs.extra_tag }} + run: | + name="$IMAGE_NAME_INPUT"; [ -z "$name" ] && name="${{ github.repository }}" + ref="ghcr.io/${name}" + tags="${ref}:${TAG}" + [ -n "$EXTRA" ] && tags="${tags},${ref}:${EXTRA}" + echo "tags=${tags}" >> "$GITHUB_OUTPUT" + - name: Docker Build and Push + uses: docker/build-push-action@v6 + with: + context: . + file: ${{ inputs.dockerfile }} + push: true + platforms: linux/amd64 + target: ${{ inputs.build_target }} + tags: ${{ steps.meta.outputs.tags }} + cache-from: ${{ inputs.cache_type == 'gha' && 'type=gha' || format('type=registry,ref=ghcr.io/{0}:buildcache', github.repository) }} + cache-to: ${{ inputs.cache_type == 'gha' && 'type=gha,mode=max' || format('type=registry,ref=ghcr.io/{0}:buildcache,mode=max', github.repository) }} +``` + +> `build_target` may be empty (proxy/nginx images have no named stage). `build-push-action` +> treats an empty `target:` as unset — validated on the license_api pilot (Task 5). + +- [ ] **Step 3: Lint.** `actionlint .github/workflows/php-build-push.yaml` → no errors. + +- [ ] **Step 4: Commit.** + +```bash +git add .github/workflows/php-build-push.yaml +git commit -m "feat: add image_name/extra_tag/cache_type inputs to php-build-push, modernize actions" +``` + +### Task 3: Create `build-php-v1.yaml` + +**Files:** Create `.github/workflows/build-php-v1.yaml`. + +- [ ] **Step 1: Write the orchestrator.** + +```yaml +name: Build PHP (v1) +on: + workflow_call: + inputs: + images: + description: 'JSON array of {image_name?, dockerfile, target?, tag_prefix, extra_tag}' + required: true + type: string + php_version: + required: false + type: string + default: "8.3" + release_branches: + required: false + type: string + default: "stage,hotfix,rc" + cache_type: + required: false + type: string + default: "gha" + secrets: + packagist_username: { required: true } + packagist_password: { required: true } + gh_token: { required: true } + outputs: + tag: + description: "The released tag" + value: ${{ jobs.tag-and-release.outputs.tag }} + +jobs: + calculate-tag: + name: Calculate Build Tag + runs-on: ubuntu-latest + outputs: + tag: ${{ steps.dry_tag_version.outputs.new_tag }} + steps: + - uses: actions/checkout@v6 + with: { fetch-depth: 0 } + - id: dry_tag_version + uses: mathieudutour/github-tag-action@v6.1 + with: + github_token: ${{ secrets.gh_token }} + release_branches: ${{ inputs.release_branches }} + fetch_all_tags: true + dry_run: true + + build: + name: Build ${{ matrix.image.tag_prefix }}image + needs: [calculate-tag] + strategy: + matrix: + image: ${{ fromJSON(inputs.images) }} + uses: ./.github/workflows/php-build-push.yaml # local path: nested call resolves at the orchestrator's own ref + with: + php_version: ${{ inputs.php_version }} + image_name: ${{ matrix.image.image_name }} + dockerfile: ${{ matrix.image.dockerfile }} + build_target: ${{ matrix.image.target }} + tag: ${{ matrix.image.tag_prefix }}${{ needs.calculate-tag.outputs.tag }} + extra_tag: ${{ matrix.image.extra_tag }} + cache_type: ${{ inputs.cache_type }} + secrets: + packagist_username: ${{ secrets.packagist_username }} + packagist_password: ${{ secrets.packagist_password }} + gh_token: ${{ secrets.gh_token }} + + tag-and-release: + name: Github Tag and Release + needs: [build] + runs-on: ubuntu-latest + outputs: + tag: ${{ steps.tag_version.outputs.new_tag }} + steps: + - uses: actions/checkout@v6 + with: { fetch-depth: 0 } + - id: tag_version + uses: mathieudutour/github-tag-action@v6.1 + with: + github_token: ${{ secrets.gh_token }} + release_branches: ${{ inputs.release_branches }} + fetch_all_tags: true + - uses: ncipollo/release-action@v1 + with: + tag: ${{ steps.tag_version.outputs.new_tag }} + name: Prerelease ${{ steps.tag_version.outputs.new_tag }} + body: ${{ steps.tag_version.outputs.changelog }} + prerelease: true +``` + +> The matrix `image_name` is empty for app/nginx/apache (php-build-push falls back to +> `github.repository`) and `-profiler` for the profiler image. `target` is empty for +> nginx/apache. + +- [ ] **Step 2: Lint.** `actionlint .github/workflows/build-php-v1.yaml` → no errors. + +- [ ] **Step 3: Commit.** + +```bash +git add .github/workflows/build-php-v1.yaml +git commit -m "feat: add build-php-v1 reusable orchestrator" +``` + +### Task 4: Create `build-php-laravel.yaml` + +**Files:** Create `.github/workflows/build-php-laravel.yaml`. + +- [ ] **Step 1: Write the orchestrator** (wraps the existing Laravel build helper). + +```yaml +name: Build PHP (Laravel) +on: + workflow_call: + inputs: + php_version: + required: false + type: string + default: "8.3" + php_extensions: + required: false + type: string + default: "bcmath intl gd" + build_cli_image: + required: false + type: boolean + default: false + dockerfile_app_path: + required: false + type: string + default: "./build/Dockerfile-app" + dockerfile_webserver_path: + required: false + type: string + default: "./build/Dockerfile-nginx" + dockerfile_cli_path: + required: false + type: string + default: "./build/Dockerfile-cli" + release_branches: + required: false + type: string + default: "stage,hotfix,rc" + secrets: + packagist_username: { required: true } + packagist_password: { required: true } + gh_token: { required: true } + outputs: + tag: + value: ${{ jobs.tag-and-release.outputs.tag }} + +jobs: + calculate-tag: + name: Calculate Build Tag + runs-on: ubuntu-latest + outputs: + tag: ${{ steps.dry_tag_version.outputs.new_tag }} + steps: + - uses: actions/checkout@v6 + with: { fetch-depth: 0 } + - id: dry_tag_version + uses: mathieudutour/github-tag-action@v6.1 + with: + github_token: ${{ secrets.gh_token }} + release_branches: ${{ inputs.release_branches }} + fetch_all_tags: true + dry_run: true + + build: + needs: [calculate-tag] + uses: ./.github/workflows/php-laravel-build-push.yaml # local path: nested call resolves at the orchestrator's own ref + with: + php_version: ${{ inputs.php_version }} + php_extensions: ${{ inputs.php_extensions }} + build_app_image: true + build_webserver_image: true + build_cli_image: ${{ inputs.build_cli_image }} + dockerfile_app_path: ${{ inputs.dockerfile_app_path }} + dockerfile_webserver_path: ${{ inputs.dockerfile_webserver_path }} + dockerfile_cli_path: ${{ inputs.dockerfile_cli_path }} + new_tag: ${{ needs.calculate-tag.outputs.tag }} + secrets: + packagist_username: ${{ secrets.packagist_username }} + packagist_password: ${{ secrets.packagist_password }} + gh_token: ${{ secrets.gh_token }} + + tag-and-release: + name: Github Tag and Release + needs: [build] + runs-on: ubuntu-latest + outputs: + tag: ${{ steps.tag_version.outputs.new_tag }} + steps: + - uses: actions/checkout@v6 + with: { fetch-depth: 0 } + - id: tag_version + uses: mathieudutour/github-tag-action@v6.1 + with: + github_token: ${{ secrets.gh_token }} + release_branches: ${{ inputs.release_branches }} + fetch_all_tags: true + - uses: ncipollo/release-action@v1 + with: + tag: ${{ steps.tag_version.outputs.new_tag }} + name: Prerelease ${{ steps.tag_version.outputs.new_tag }} + body: ${{ steps.tag_version.outputs.changelog }} + prerelease: true +``` + +> The Laravel helper's webserver tag prefix is configurable via `webserver_tag_prefix` +> (default `webserver-`). build-php-laravel.yaml exposes the same input and threads it +> through. returns-api and vin_decoder_service pass `webserver_tag_prefix: nginx-` to keep +> their exact current proxy tags — so NO deployment.yaml or SRT changes (Tasks 6, 13). + +- [ ] **Step 2: Lint.** `actionlint .github/workflows/build-php-laravel.yaml` → no errors. + +- [ ] **Step 3: Commit, push the branch, open the shared-workflow PR.** + +```bash +git add .github/workflows/build-php-laravel.yaml +git commit -m "feat: add build-php-laravel reusable orchestrator" +git push -u origin DEVEX-1630-unify-php-build-workflows +gh pr create --repo encodium/.github --draft --base main \ + --title "DEVEX-1630: shared PHP build orchestrators" \ + --body "Adds build-php-v1 + build-php-laravel and modernizes php-build-push. See docs/superpowers/specs/2026-06-08-unify-php-build-workflows-design.md" +``` + +--- + +## Phase 1B — Pilots (validate before fan-out) + +Pilots reference the orchestrator on the **feature branch** until the foundation PR merges. + +### Task 5: Pilot v1 — license_api + +**Files:** Modify `license_api/.github/workflows/Build.yaml`. + +- [ ] **Step 1: Replace the whole file with the thin caller.** (license_api builds app + nginx, no profiler.) + +```yaml +name: Build +on: + push: { branches: [main] } + workflow_dispatch: + +concurrency: + group: ${{ github.repository }}-build + cancel-in-progress: false + +jobs: + build: + uses: encodium/.github/.github/workflows/build-php-v1.yaml@DEVEX-1630-unify-php-build-workflows + with: + images: >- + [ + {"image_name":"","dockerfile":"./build/app/Dockerfile","target":"app","tag_prefix":"","extra_tag":"latest"}, + {"image_name":"","dockerfile":"./build/nginx/Dockerfile","target":"","tag_prefix":"nginx-","extra_tag":"nginx-latest"} + ] + secrets: + packagist_username: ${{ secrets.PACKAGIST_USERNAME }} + packagist_password: ${{ secrets.PACKAGIST_PASSWORD }} + gh_token: ${{ secrets.GITHUB_TOKEN }} + + integration-deploy: + name: Deploy to Integration + if: ${{ github.event_name == 'push' }} + needs: [build] + uses: ./.github/workflows/Integration EKS Deploy.yaml + with: + image_tag: ${{ needs.build.outputs.tag }} + secrets: inherit +``` + +- [ ] **Step 2: Lint.** `actionlint .github/workflows/Build.yaml` → no errors. + +- [ ] **Step 3: Trigger a dispatch build and verify.** + +```bash +gh workflow run "Build.yaml" --repo encodium/license_api --ref +gh run view --repo encodium/license_api --json jobs --jq '.jobs[] | {name,conclusion}' +``` +Expected: `calculate-tag`, both `build` matrix legs, `tag-and-release` succeed; +`integration-deploy` **skipped** (dispatch). + +- [ ] **Step 4: Tag-fidelity diff.** Confirm the new package versions carry exactly + `:`, `:latest`, `:nginx-`, `:nginx-latest` — identical shape to the baseline + captured earlier. **This validates the empty-`target` nginx build.** + +```bash +gh api repos/encodium/license_api/packages/container/license_api/versions --jq '.[0].metadata.container.tags' +``` + +- [ ] **Step 5: Commit on a branch, open draft PR.** + +```bash +git commit -am "DEVEX-1630: migrate Build.yaml to shared build-php-v1 orchestrator" +``` + +### Task 6: Pilot Laravel — returns-api + +**Files:** Modify `returns-api/.github/workflows/Build.yaml` only. The proxy prefix is kept +as `nginx-` via the `webserver_tag_prefix` input, so **no deployment.yaml or SRT changes**. + +- [ ] **Step 1: Replace Build.yaml with the thin caller.** Pass `webserver_tag_prefix: nginx-` + so the proxy image keeps publishing `nginx-`/`nginx-latest` (matching + `deployments/templates/deployment.yaml` and `Start Release Train.yaml`'s + `image_tag_prefixes`). + +```yaml +name: Build +on: + push: { branches: [main] } + workflow_dispatch: + +concurrency: + group: ${{ github.repository }}-build + cancel-in-progress: false + +jobs: + build: + uses: encodium/.github/.github/workflows/build-php-laravel.yaml@DEVEX-1630-unify-php-build-workflows + with: + dockerfile_app_path: ./build/Dockerfile-app + dockerfile_webserver_path: ./build/Dockerfile-nginx + webserver_tag_prefix: nginx- + secrets: + packagist_username: ${{ secrets.PACKAGIST_USERNAME }} + packagist_password: ${{ secrets.PACKAGIST_PASSWORD }} + gh_token: ${{ secrets.GITHUB_TOKEN }} + + integration-deploy: + if: ${{ github.event_name == 'push' }} + needs: [build] + uses: ./.github/workflows/Integration EKS Deploy.yaml + with: + image_tag: ${{ needs.build.outputs.tag }} + secrets: inherit +``` + +- [ ] **Step 2: Lint.** `actionlint .github/workflows/Build.yaml` → no errors. + +- [ ] **Step 3: Trigger build, verify jobs + tags.** Same commands as Task 5 Steps 3-4; + expect app `:`/`:latest` and proxy `nginx-`/`nginx-latest` (UNCHANGED from + baseline), and confirm the **artisan cache steps ran** in the build helper logs without + error: + +```bash +gh run view --repo encodium/returns-api --log | grep -i "artisan" +``` + +- [ ] **Step 4: Boot-check the app image** (artisan caching is new for returns-api) — pull + the new app image in the dev sandbox and confirm it starts and serves a health route. + +- [ ] **Step 5: Commit on a branch, open draft PR.** + +```bash +git commit -am "DEVEX-1630: migrate Build.yaml to shared build-php-laravel orchestrator" +``` + +### Task 7: Merge foundation, flip pilots to `@main` + +- [ ] **Step 1:** Mark the `encodium/.github` PR ready and merge it. +- [ ] **Step 2:** In both pilot Build.yaml files, change + `@DEVEX-1630-unify-php-build-workflows` → `@main`. Commit. +- [ ] **Step 3:** Re-run a dispatch build on each pilot to confirm `@main` resolves and + produces identical tags. Merge both pilot PRs. + +--- + +## Phase 1C — Fan-out (one PR per repo, all reference `@main`) + +For every repo: replace `Build.yaml` with a thin caller mirroring the matching pilot, using +the repo's exact `images` array (v1) and preserving its existing deploy job verbatim except +`needs:` → `[build]` and (integration repos) the `if: github.event_name == 'push'` gate. +Verify each with the Task 5 Step 3-4 commands (tag-fidelity diff against baseline). + +### Task 8: catalog_api (v1, integration) + +**Files:** Modify `catalog_api/.github/workflows/Build.yaml`. +- [ ] **Step 1:** Replace the file with the thin caller (app + nginx): + +```yaml +name: Build +on: + push: { branches: [main] } + workflow_dispatch: + +concurrency: + group: ${{ github.repository }}-build + cancel-in-progress: false + +jobs: + build: + uses: encodium/.github/.github/workflows/build-php-v1.yaml@main + with: + images: >- + [ + {"image_name":"","dockerfile":"./build/app/Dockerfile","target":"app","tag_prefix":"","extra_tag":"latest"}, + {"image_name":"","dockerfile":"./build/nginx/Dockerfile","target":"","tag_prefix":"nginx-","extra_tag":"nginx-latest"} + ] + secrets: + packagist_username: ${{ secrets.PACKAGIST_USERNAME }} + packagist_password: ${{ secrets.PACKAGIST_PASSWORD }} + gh_token: ${{ secrets.GITHUB_TOKEN }} + + integration-deploy: + name: Deploy to Integration + if: ${{ github.event_name == 'push' }} + needs: [build] + uses: ./.github/workflows/Integration EKS Deploy.yaml + with: + image_tag: ${{ needs.build.outputs.tag }} + secrets: inherit +``` +- [ ] **Step 2:** Lint, trigger, tag-diff (expect `:`, `:latest`, `:nginx-`, `:nginx-latest`). +- [ ] **Step 3:** Commit on branch, draft PR. + +### Task 9: rp_api (v1, integration, **profiler**) + +**Files:** Modify `rp_api/.github/workflows/Build.yaml`. +- [ ] **Step 1:** Thin caller with the 3-image array: + +```yaml + images: >- + [ + {"image_name":"","dockerfile":"./build/app/Dockerfile","target":"app","tag_prefix":"","extra_tag":"latest"}, + {"image_name":"rp_api-profiler","dockerfile":"./build/app/Dockerfile","target":"app-profiler","tag_prefix":"","extra_tag":"latest"}, + {"image_name":"","dockerfile":"./build/nginx/Dockerfile","target":"","tag_prefix":"nginx-","extra_tag":"nginx-latest"} + ] +``` + Keep rp_api's gated `integration-deploy` (`needs: [build]`). +- [ ] **Step 2:** Lint, trigger, tag-diff. **Confirm `ghcr.io/encodium/rp_api-profiler:` + and `:latest` are pushed** (separate image), plus app + nginx tags. +- [ ] **Step 3:** Commit on branch, draft PR. + +### Task 10: internal_api (v1, integration, **profiler**) + +**Files:** Modify `internal_api/.github/workflows/Build.yaml`. +- [ ] **Step 1:** Replace the file with the thin caller (app + profiler + nginx). Note the + **hyphenated** deploy filename for this repo: + +```yaml +name: Build +on: + push: { branches: [main] } + workflow_dispatch: + +concurrency: + group: ${{ github.repository }}-build + cancel-in-progress: false + +jobs: + build: + uses: encodium/.github/.github/workflows/build-php-v1.yaml@main + with: + images: >- + [ + {"image_name":"","dockerfile":"./build/app/Dockerfile","target":"app","tag_prefix":"","extra_tag":"latest"}, + {"image_name":"internal_api-profiler","dockerfile":"./build/app/Dockerfile","target":"app-profiler","tag_prefix":"","extra_tag":"latest"}, + {"image_name":"","dockerfile":"./build/nginx/Dockerfile","target":"","tag_prefix":"nginx-","extra_tag":"nginx-latest"} + ] + secrets: + packagist_username: ${{ secrets.PACKAGIST_USERNAME }} + packagist_password: ${{ secrets.PACKAGIST_PASSWORD }} + gh_token: ${{ secrets.GITHUB_TOKEN }} + + integration-deploy: + name: Deploy to Integration + if: ${{ github.event_name == 'push' }} + needs: [build] + uses: ./.github/workflows/Integration-EKS-Deploy.yaml + with: + image_tag: ${{ needs.build.outputs.tag }} + secrets: inherit +``` +- [ ] **Step 2:** Lint, trigger, tag-diff (app + `internal_api-profiler` + nginx). +- [ ] **Step 3:** Commit on branch, draft PR. + +### Task 11: radmin (v1, **staging**) + +**Files:** Modify `radmin/.github/workflows/Build.yaml`. +- [ ] **Step 1:** Thin caller with `images` = app + nginx. **No gate.** Preserve radmin's + existing staging deploy verbatim, only changing `needs:`: + +```yaml + stage-eks-deploy: + uses: encodium/radmin/.github/workflows/deploy-eks.yaml@main + needs: [build] + with: + environment: stg + image_tag: ${{ needs.build.outputs.tag }} + values-file: ./deployments/stg-eks-values.yaml + secrets: + kubeconfig: ${{ secrets.RP_STG_EKS_KUBECONFIG }} + k8s_aws_access_id: ${{ secrets.RP_STG_EKS_ACCESS_KEY }} + k8s_aws_access_secret: ${{ secrets.RP_STG_EKS_SECRET_KEY }} + slack_webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }} + jellyfish_api_token: ${{ secrets.JELLYFISH_API_TOKEN }} +``` +- [ ] **Step 2:** Lint, trigger, tag-diff (app + nginx). +- [ ] **Step 3:** Commit on branch, draft PR. + +### Task 12: accounts-api (Laravel, integration) + +**Files:** Modify `accounts-api/.github/workflows/build.yaml` (lowercase). + +- [ ] **Step 1: Capture the jobs to preserve.** accounts-api has extra jobs (OpenAPI + `generate_client` dispatch, release artifact) not in the shared spine. Record them + verbatim before editing: + +```bash +gh api repos/encodium/accounts-api/contents/.github/workflows/build.yaml --jq '.content' | base64 -d > /tmp/accounts-build.yaml +``` + Note the exact `generate_client` job(s), their `if: steps.spec_changed.outputs.any_changed == 'true'` guards, and the `./deployments/files/openapi.yaml` artifact wiring. + +- [ ] **Step 2: Replace the build/tag/release jobs with the shared caller**, keeping the + preserved jobs. The build block: + +```yaml +name: Build +on: + push: { branches: [main] } + workflow_dispatch: + +concurrency: + group: ${{ github.repository }}-build + cancel-in-progress: false + +jobs: + build: + uses: encodium/.github/.github/workflows/build-php-laravel.yaml@main + secrets: + packagist_username: ${{ secrets.PACKAGIST_USERNAME }} + packagist_password: ${{ secrets.PACKAGIST_PASSWORD }} + gh_token: ${{ secrets.GITHUB_TOKEN }} + + integration-deploy: + if: ${{ github.event_name == 'push' }} + needs: [build] + uses: ./.github/workflows/Integration EKS Deploy.yaml + with: + image_tag: ${{ needs.build.outputs.tag }} + secrets: inherit +``` + Then re-attach the preserved `generate_client` job(s) from Step 1, repointing any + `needs:`/tag reference to `needs.build.outputs.tag`. accounts-api already publishes + `webserver-` so no deploy/helm consumer change is needed. + +- [ ] **Step 3:** Lint, trigger, tag-diff (app + `webserver-`); confirm `generate_client` + still runs when the OpenAPI spec changes. +- [ ] **Step 4:** Commit on branch, draft PR. + +### Task 13: vin_decoder_service (Laravel, **staging**) + +**Files:** Modify `vin_decoder_service/.github/workflows/Build.yaml` only. Proxy prefix kept +as `nginx-` via `webserver_tag_prefix` — **no deployment/SRT changes**. +- [ ] **Step 1:** Replace the file with the thin caller; pass `webserver_tag_prefix: nginx-`; + preserve the staging deploy verbatim except `needs: [build]`: + +```yaml +name: Build +on: + workflow_dispatch: + +concurrency: + group: ${{ github.repository }}-build + cancel-in-progress: false + +jobs: + build: + uses: encodium/.github/.github/workflows/build-php-laravel.yaml@main + with: + dockerfile_app_path: ./build/Dockerfile-app + dockerfile_webserver_path: ./build/Dockerfile-nginx + webserver_tag_prefix: nginx- + secrets: + packagist_username: ${{ secrets.PACKAGIST_USERNAME }} + packagist_password: ${{ secrets.PACKAGIST_PASSWORD }} + gh_token: ${{ secrets.GITHUB_TOKEN }} + + stage-deploy-eks: + uses: encodium/vin_decoder_service/.github/workflows/deploy-eks.yaml@main + needs: [build] + with: + environment: stg + image_tag: ${{ needs.build.outputs.tag }} + values-file: ./deployments/stg-eks-values.yaml + secrets: + kubeconfig: ${{ secrets.RP_STG_EKS_KUBECONFIG }} + k8s_aws_access_id: ${{ secrets.RP_STG_EKS_ACCESS_KEY }} + k8s_aws_access_secret: ${{ secrets.RP_STG_EKS_SECRET_KEY }} + slack_webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }} + jellyfish_api_token: ${{ secrets.JELLYFISH_API_TOKEN }} +``` + +> vin_decoder_service is `workflow_dispatch`-only today (no `push:` trigger) — preserved as-is. + +- [ ] **Step 2:** Lint, trigger, tag-diff (expect proxy `nginx-` UNCHANGED); boot-check + image (new artisan caching). +- [ ] **Step 3:** Commit on branch, draft PR. + +### Task 14: webstore (v1, integration, **profiler + apache + Node/S3 extraction**) — LAST + +**Files:** Modify `webstore/.github/workflows/Build.yaml`. + +- [ ] **Step 1: Extract the Node/S3 asset publish into its own caller job.** Lift the + current `build-app` job's Node steps (Setup node, node_modules cache, `npm ci`, + `npm rebuild`, `npm run build`, Configure AWS, gzip, `aws s3 cp` × N, remove dist) into a + standalone job `publish-assets` that runs from `calculate-tag` independently of `build`. + It needs no PHP/composer. Keep its AWS/npm secrets and the exact S3 paths. + +- [ ] **Step 2: Thin caller for the PHP images** (app + profiler + apache + nginx): + +```yaml + images: >- + [ + {"image_name":"","dockerfile":"./build/app/Dockerfile","target":"app","tag_prefix":"","extra_tag":"latest"}, + {"image_name":"webstore-profiler","dockerfile":"./build/app/Dockerfile","target":"app-profiler","tag_prefix":"","extra_tag":"latest"}, + {"image_name":"","dockerfile":"./build/nginx/Dockerfile","target":"","tag_prefix":"nginx-","extra_tag":"nginx-latest"}, + {"image_name":"","dockerfile":"./build/apache/Dockerfile","target":"","tag_prefix":"apache-","extra_tag":"apache-latest"} + ] +``` + Keep webstore's gated `integration-deploy` (hyphenated `Integration-EKS-Deploy.yaml`, + `needs: [build]`). + +- [ ] **Step 3: Verify CDN assets unchanged.** Trigger a build; confirm `publish-assets` + uploads the **same** S3 keys as before (compare `aws s3 ls` listing of the CDN prefix + pre/post). This is the highest-risk check — do not merge until identical. + +- [ ] **Step 4:** Lint, tag-diff (app + `webstore-profiler` + `nginx-` + `apache-`). + +- [ ] **Step 5:** Commit on branch, draft PR. + +--- + +## Done criteria + +- [ ] All 9 repos build via the shared orchestrators (`@main`); no repo retains inline + composer/docker build steps for its PHP images. +- [ ] Every repo's produced image tags match its pre-migration baseline (profiler images, + `nginx-`/`apache-` tags; returns-api/vin keep `nginx-` via `webserver_tag_prefix`, accounts-api keeps `webserver-`). +- [ ] Integration repos skip deploy on `workflow_dispatch`, deploy on `push` (Phase 0 gate + preserved in the thin callers). +- [ ] webstore CDN assets verified byte-path identical after Node/S3 extraction. +- [ ] `php-build-push.yaml` change is backward-compatible for any pre-existing callers. +``` diff --git a/docs/superpowers/specs/2026-06-08-unify-php-build-workflows-design.md b/docs/superpowers/specs/2026-06-08-unify-php-build-workflows-design.md new file mode 100644 index 0000000..0801bef --- /dev/null +++ b/docs/superpowers/specs/2026-06-08-unify-php-build-workflows-design.md @@ -0,0 +1,256 @@ +# Unify PHP build workflows into shared reusable orchestrators + +- **Date:** 2026-06-08 +- **Tickets:** DEVEX-1630 (Phase 1, this spec) — blocked by DEVEX-1629 (Phase 0, the gate fix) +- **Repos affected:** `encodium/.github` (shared workflows) + 6 PHP service repos + +## Problem + +The six PHP service repos each carry a full, copy-pasted `Build.yaml` +(`calculate-tag → build images → tag-and-release → integration-deploy`). They have +already drifted back together and are now near-identical, which means every +fleet-wide build change must be made six times. The immediate trigger was +DEVEX-1629 ("hotfix builds are being deployed to the integration environment"): +the same one-line fix has to land in six places. + +## Goal + +Move the shared build spine into reusable workflows in `encodium/.github` so the +next fleet-wide change is one edit, not six — without changing any image tags that +deploy/helm consumers depend on. + +## Scope + +In scope — nine PHP services, split by application class. Determined by an +org-wide sweep (`shivammathur/setup-php` in build workflows + root `artisan` marker): + +| Repo | Class (`artisan`?) | Build primitive | Images today | Deploy | +|---|---|---|---|---| +| rp_api | v1 (no) | `php-build-push` matrix | `app`, `app-profiler`, `nginx` | integration | +| internal_api | v1 (no) | `php-build-push` matrix | `app`, `app-profiler`, `nginx` | integration | +| catalog_api | v1 (no) | `php-build-push` matrix | `app`, `nginx` | integration | +| license_api | v1 (no) | `php-build-push` matrix | `app`, `nginx` | integration | +| radmin | v1 (no) | `php-build-push` matrix | `app`, `nginx` | **staging** | +| webstore | v1 (no) | `php-build-push` matrix | `app`, `app-profiler`, `nginx`, `apache` | integration | +| returns-api | Laravel (yes) | `php-laravel-build-push` | `app`, `nginx` | integration | +| accounts-api | Laravel (yes) | `php-laravel-build-push` | `app`, `webserver` (already migrated) | integration | +| vin_decoder_service | Laravel (yes) | `php-laravel-build-push` | `app`, `nginx` | **staging** | + +`radmin` and `vin_decoder_service` deploy to **staging** via their own +repo-specific `deploy-eks.yaml` (not integration). Since deploy stays in the caller +(below), this is just a different deploy job in those two callers; the build +unification applies unchanged. The Phase 0 integration gate (DEVEX-1629) does not +apply to them — their staging auto-deploy is a separate DEVEX-1087 concern. + +`webstore` is a v1 PHP build **plus** a Node asset-publish step; only the PHP image +builds are unified here (see "webstore" below). + +Out of scope: +- **batch** — builds/deploys to EC2 via SSM (`build-ec2.yaml`), an entirely different + model. Left untouched. +- **rp-cli-zero** — `build-and-release.yaml` only runs composer + `action-gh-release`; + builds no deployable image. +- **checkout, manage** — Node/frontend, no PHP. +- The deploy engine (`helm-deploy-eks.yaml`) and standalone deploy workflows — unchanged. + +## Phase 0 — the gate (DEVEX-1629, lands first) + +Add a trigger gate to the existing `integration-deploy` job in all six repos: + +```yaml +integration-deploy: + needs: [tag-and-release] + if: ${{ github.event_name == 'push' }} # keep push→integration CD; skip on hotfix dispatch + uses: ./.github/workflows/Integration EKS Deploy.yaml # (hyphenated in internal_api) + with: { image_tag: ${{ needs.tag-and-release.outputs.tag }} } + secrets: inherit +``` + +Push to `main` still deploys to integration; hotfix `workflow_dispatch` builds, +tags, and releases but does not deploy. Intentional integration deploys use the +standalone `Integration EKS Deploy.yaml` dispatch. Six small PRs, very low risk. + +This is the same job that survives into Phase 1 (see below), so Phase 0 is not +throwaway. + +## Phase 1 — unification design + +### Two orchestrators (not one parametrized file) + +Mirrors the existing helper split and avoids skipped-job / `needs` gymnastics: + +- **`encodium/.github/.github/workflows/build-php-v1.yaml`** — for the four v1 repos. +- **`encodium/.github/.github/workflows/build-php-laravel.yaml`** — for the two Laravel repos. + +Both implement the identical spine and expose a `tag` output: + +``` +calculate-tag (dry run, release_branches: stage,hotfix,rc, fetch_all_tags: true) + → build (differs per orchestrator — see below) + → tag-and-release (real bump + GitHub prerelease; output: tag) +``` + +### Build job — v1 orchestrator + +Fan out over an `images` JSON array, one call to `php-build-push.yaml` per image, +via a matrix on the reusable-workflow `uses:`: + +```yaml +build: + needs: [calculate-tag] + strategy: + matrix: + image: ${{ fromJSON(inputs.images) }} + uses: encodium/.github/.github/workflows/php-build-push.yaml@main + with: + image_name: ${{ matrix.image.image_name }} # default github.repository + dockerfile: ${{ matrix.image.dockerfile }} + build_target: ${{ matrix.image.target }} + tag: ${{ matrix.image.tag_prefix }}${{ needs.calculate-tag.outputs.tag }} + secrets: inherit +``` + +Per-repo `images` reproduce **today's exact tags** (verified against `main`): + +| Image | `image_name` | `dockerfile` | `target` | tag | extra | repos | +|---|---|---|---|---|---|---| +| app | `` | `./build/app/Dockerfile` | `app` | `` | `+ :latest` | all v1 | +| nginx | `` | `./build/nginx/Dockerfile` | _(none)_ | `nginx-` | `+ :nginx-latest` | all v1 | +| profiler | `-profiler` | `./build/app/Dockerfile` | `app-profiler` | `` | `+ :latest` | rp_api, internal_api, webstore | +| apache | `` | `./build/apache/Dockerfile` | _(none)_ | `apache-` | `+ :apache-latest` | webstore only | + +> Profiler is a **separate image name** (`ghcr.io/-profiler:`), not a tag +> prefix. `nginx`/`apache` are prefixed tags on the same image. The matrix carries +> only the images a given repo declares — extra image types (profiler, apache) need no +> orchestrator change, just additional matrix entries. + +### `php-build-push.yaml` modernization (prerequisite for the v1 path) + +The current helper cannot reproduce these tags as-is. Required changes: + +1. Add `image_name` input (default `${{ github.repository }}`) so profiler can target `-profiler`. +2. Push the `latest` companion tag (`:latest` or `:latest`) — add an + `extra_tag`/`push_latest` input; repos currently push both. +3. Bump action versions to match the rest of the fleet: + `setup-buildx-action@v1→v3`, `login-action@v1→v3`, `build-push-action@v2→v6`. +4. Cache: current helper uses `type=registry` buildcache; the repos' inline builds + use `type=gha`. Pick one in the pilot (lean `type=gha` to match current behavior) + and apply consistently. + +Before editing, grep for other consumers of `php-build-push.yaml` across the org; +bump conservatively and verify none break. + +### webstore (special case, v1) + +webstore's current `build-app` job interleaves a **Node asset publish** (npm build → +gzip → `aws s3 cp` to CDN → **`rm -rf dist`**) with the PHP image build. Because the +`dist` folder is removed *before* `docker build`, the app image does **not** bake in +Node assets — they are CDN-served. So the PHP images (app, profiler, nginx, apache) +delegate to `build-php-v1.yaml` like any other v1 repo, and the Node/S3 asset publish +is extracted into a **standalone job in webstore's caller** (no PHP/composer; needs the +existing AWS/S3 + npm secrets). The asset-publish job and the build orchestrator both +fan from `calculate-tag`; neither depends on the other. + +This makes webstore the most involved v1 migration — do it **last** in the v1 fan-out, +after the matrix path is proven on simpler repos. + +### Build job — Laravel orchestrator + +Single call to the existing `php-laravel-build-push.yaml` (app + webserver toggles, +runs `artisan config:cache/route:cache/view:cache`). accounts-api already uses it. +Its `dockerfile_app_path`/`dockerfile_webserver_path` defaults +(`./build/Dockerfile-app`, `./build/Dockerfile-nginx`) already match returns-api and +vin_decoder_service. + +**Proxy tag prefix — DECIDED: parametrize, keep each repo's current prefix.** The +Laravel helper previously hardcoded `webserver-`, but returns-api and +vin_decoder_service publish `nginx-` and consume that prefix in **two** places each +(`deployments/templates/deployment.yaml` image line + `Start Release Train.yaml` +`image_tag_prefixes`). To avoid that blast radius, `php-laravel-build-push.yaml` now +takes an optional `webserver_tag_prefix` input (default `"webserver-"`, so its many +existing callers — nexus, shopping, shipping, accounts-api, … — are byte-for-byte +unaffected); `build-php-laravel.yaml` threads it through. returns-api and +vin_decoder_service pass `webserver_tag_prefix: "nginx-"` and thus keep their exact +current tags — **no deployment.yaml or SRT change needed**. + +**Artisan caching is newly introduced** for returns-api/vin_decoder_service. Their +current inline builds do not run `artisan *:cache`; the helper does. Laravel +`config:cache` can fail if build-time env is missing; accounts-api proves it is +solvable. Verify the image boots correctly after each repo's first build. + +### Deploy stays in the thin caller (hard constraint) + +A reusable workflow's `uses: ./.github/workflows/...` resolves inside +`encodium/.github`, **not** the caller — so the orchestrators cannot invoke a repo's +local `Integration EKS Deploy.yaml`. Therefore the gated `integration-deploy` job +**remains in each repo's `Build.yaml`**, consuming the orchestrator's `tag` output: + +```yaml +jobs: + build: + uses: encodium/.github/.github/workflows/build-php-v1.yaml@main # or -laravel + with: + images: '[ ... per-repo ... ]' # v1 only + secrets: inherit + + integration-deploy: + needs: [build] + if: ${{ github.event_name == 'push' }} # the Phase 0 gate, unchanged + uses: ./.github/workflows/Integration EKS Deploy.yaml + with: { image_tag: ${{ needs.build.outputs.tag }} } + secrets: inherit +``` + +Each `Build.yaml` collapses from ~130 lines to a ~25-line caller plus this gated +deploy job. Caller-specific jobs stay in the caller: + +- **radmin, vin_decoder_service** keep their existing `stage-eks-deploy` + (`encodium//.github/workflows/deploy-eks.yaml@main`, `stg-eks-values.yaml`) + instead of an integration-deploy job — no Phase 0 gate. +- **webstore** keeps the extracted Node/S3 asset-publish job. +- **accounts-api** keeps its OpenAPI client-generation jobs. + +## Rollout + +1. Modernize `php-build-push.yaml` (additive inputs + version bumps); confirm no other consumers break. +2. Author `build-php-v1.yaml` and `build-php-laravel.yaml`. +3. **Pilot v1** on **license_api** (simplest: app + nginx, no profiler). Verify a + `push` build produces identical tags and the integration deploy runs; verify a + `workflow_dispatch` build skips deploy. +4. **Pilot Laravel** on **returns-api** (passes `webserver_tag_prefix: nginx-` to keep + its tags; validates new artisan caching). Verify image boots. +5. Fan out v1: rp_api, internal_api, catalog_api (incl. profiler validation on + rp_api/internal_api), radmin (staging deploy), then **webstore last** (Node/S3 + asset-publish extraction + apache image). +6. Fan out Laravel: vin_decoder_service (also passes `webserver_tag_prefix: nginx-`; + same artisan change as the returns-api pilot, staging deploy), accounts-api (keeps + default `webserver-`; mostly a thin-caller refactor since it already calls the helper). + +Each repo is its own PR; the shared-workflow PR(s) to `encodium/.github` merge first +and are referenced `@main`. + +## Risks + +- **Tag fidelity** → reproduced verbatim via the `images` matrix and the + `php-build-push` `image_name`/latest-tag inputs; pilots diff produced tags against + current before fan-out. +- **Matrix + reusable `uses:` + `secrets: inherit`** → supported by Actions; validated + on the v1 pilot before fan-out. +- **Laravel artisan caching** → newly introduced for returns-api and vin_decoder_service; + verified per-repo via an image boot-check (returns-api first as the Laravel pilot). The + proxy prefix is preserved per-repo via the `webserver_tag_prefix` input (default + `webserver-`), so no deploy/SRT consumer changes and no risk to existing helper callers. +- **webstore Node/S3 extraction** → the asset-publish must keep publishing identical + CDN paths after being split into its own job; verify CDN assets land unchanged before + removing the old inline steps. Highest-risk single migration → scheduled last. +- **Shared `php-build-push.yaml` edit blast radius** → grep consumers first; changes are + additive (new optional inputs) + conservative version bumps. + +## Out of scope + +See the exclusions table under **Scope** (batch, rp-cli-zero, checkout, manage). Also: + +- The deploy engine (`helm-deploy-eks.yaml`) and the standalone deploy workflows. +- webstore's Node build itself — only its PHP image builds are unified; the Node/S3 + asset publish is relocated, not redesigned. +- Staging-deploy decoupling (DEVEX-1087) — related but separate.