From 32e17253e5e280264c067ff5000ee0d41f2a1e46 Mon Sep 17 00:00:00 2001 From: Malaber Date: Thu, 23 Apr 2026 13:21:25 +0200 Subject: [PATCH 1/7] feat: switch image builds to buildx --- .gitlab-ci.yml | 66 ++++++++++++++++++++++- Dockerfile | 100 +++++++++++++++++++++++++++++++++++ docker/production/Dockerfile | 19 ------- docker/test/Dockerfile | 13 ----- tasks.py | 36 +++++++++++-- 5 files changed, 197 insertions(+), 37 deletions(-) create mode 100644 Dockerfile delete mode 100644 docker/production/Dockerfile delete mode 100644 docker/test/Dockerfile diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e43e9cf..03ec3d1 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -4,16 +4,74 @@ stages: - deploy variables: - DOCKER_IMAGE_ENVS: "production test" + # Disable the shared image build jobs from turniere-infra; this repo owns buildx builds below. + DOCKER_IMAGE_ENVS: "" + DOCKER_BUILDKIT: "1" + BUILDX_PLATFORMS: "linux/amd64,linux/arm64" include: - project: 'turniere/turniere-infra' file: '/ci/pipeline.yaml' +.buildx-image: + stage: build + tags: + - shell + before_script: + - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY" + - export BUILDX_BUILDER_NAME="turniere-buildx-$CI_PIPELINE_ID-$CI_JOB_ID" + - docker buildx create --name "$BUILDX_BUILDER_NAME" --driver docker-container --use + - docker buildx inspect --builder "$BUILDX_BUILDER_NAME" --bootstrap + after_script: + - export BUILDX_BUILDER_NAME="turniere-buildx-$CI_PIPELINE_ID-$CI_JOB_ID" + - docker buildx rm "$BUILDX_BUILDER_NAME" || true + - docker logout "$CI_REGISTRY" || true + +build production image: + extends: .buildx-image + script: + - >- + docker buildx build + --builder "$BUILDX_BUILDER_NAME" + --file Dockerfile + --target production + --platform "$BUILDX_PLATFORMS" + --cache-from "type=registry,ref=$CI_REGISTRY_IMAGE/buildcache:production" + --cache-to "type=registry,ref=$CI_REGISTRY_IMAGE/buildcache:production,mode=max" + --tag "$CI_REGISTRY_IMAGE/production/commits:$CI_COMMIT_SHA" + --push + . + rules: + - if: $CI_PIPELINE_SOURCE != "push" + when: never + - when: always + +build test image: + extends: .buildx-image + script: + - >- + docker buildx build + --builder "$BUILDX_BUILDER_NAME" + --file Dockerfile + --target test + --platform "$BUILDX_PLATFORMS" + --cache-from "type=registry,ref=$CI_REGISTRY_IMAGE/buildcache:production" + --cache-from "type=registry,ref=$CI_REGISTRY_IMAGE/buildcache:test" + --cache-to "type=registry,ref=$CI_REGISTRY_IMAGE/buildcache:test,mode=max" + --tag "$CI_REGISTRY_IMAGE/test/commits:$CI_COMMIT_SHA" + --push + . + rules: + - if: $CI_PIPELINE_SOURCE != "push" + when: never + - when: always + rails spec: stage: test tags: - docker + needs: + - build test image image: $CI_REGISTRY_IMAGE/test/commits:$CI_COMMIT_SHA parallel: matrix: @@ -42,6 +100,9 @@ rails e2e spec: stage: test tags: - docker + needs: + - build production image + - build test image image: $CI_REGISTRY_IMAGE/test/commits:$CI_COMMIT_SHA services: - name: postgres:16 @@ -93,6 +154,9 @@ rails follow sync e2e spec: stage: test tags: - docker + needs: + - build production image + - build test image image: $CI_REGISTRY_IMAGE/test/commits:$CI_COMMIT_SHA services: - name: postgres:16 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..00f2006 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,100 @@ +# syntax=docker/dockerfile:1.7 + +ARG RUBY_VERSION=3.1.2 +ARG BUNDLER_VERSION=2.3.13 + +FROM ruby:${RUBY_VERSION}-slim AS runtime-base + +ARG BUNDLER_VERSION + +ENV LANG=C.UTF-8 \ + BUNDLE_JOBS=4 \ + BUNDLE_RETRY=3 \ + BUNDLE_PATH=/usr/local/bundle + +WORKDIR /app + +RUN apt-get update -qq \ + && apt-get install -y --no-install-recommends \ + libpq5 \ + nodejs \ + openssl \ + && rm -rf /var/lib/apt/lists/* \ + && gem install bundler -v ${BUNDLER_VERSION} \ + && gem install tzinfo-data + +FROM runtime-base AS build-base + +RUN apt-get update -qq \ + && apt-get install -y --no-install-recommends \ + build-essential \ + git \ + libpq-dev \ + libsqlite3-dev \ + pkg-config \ + python3 \ + python3-pip \ + && rm -rf /var/lib/apt/lists/* + +COPY Gemfile Gemfile.lock ./ + +FROM build-base AS bundle-production + +RUN bundle config set deployment 'true' \ + && bundle config set without 'development test' \ + && bundle install \ + && rm -rf /usr/local/bundle/cache/*.gem + +FROM build-base AS bundle-test + +RUN bundle config set with 'development test' \ + && bundle install \ + && rm -rf /usr/local/bundle/cache/*.gem + +FROM runtime-base AS production + +ENV RAILS_ENV=production + +COPY --from=bundle-production /usr/local/bundle /usr/local/bundle +COPY app /app/app +COPY bin /app/bin +COPY config /app/config +COPY db /app/db +COPY public /app/public +COPY script /app/script +COPY config.ru Rakefile /app/ +# Duplicate production environment to beta environment +COPY config/environments/production.rb /app/config/environments/beta.rb + +EXPOSE 3000 + +CMD ["sh", "-lc", "bundle exec rails db:migrate && bundle exec rails s -p 3000 -b 0.0.0.0"] + +FROM runtime-base AS test + +ENV RAILS_ENV=test + +RUN apt-get update -qq \ + && apt-get install -y --no-install-recommends \ + libsqlite3-0 \ + python3 \ + python3-pip \ + && rm -rf /var/lib/apt/lists/* \ + && python3 -m pip install --no-cache-dir invoke + +COPY --from=bundle-test /usr/local/bundle /usr/local/bundle +COPY app /app/app +COPY bin /app/bin +COPY config /app/config +COPY db /app/db +COPY public /app/public +COPY script /app/script +COPY config.ru Rakefile /app/ +COPY config/environments/production.rb /app/config/environments/beta.rb +COPY tasks.py /app/tasks.py +COPY e2e /app/e2e +COPY lib /app/lib +COPY spec /app/spec +COPY .rspec /app/.rspec + +CMD ["sh", "-lc", "bundle exec rails db:migrate && bundle exec rails spec"] diff --git a/docker/production/Dockerfile b/docker/production/Dockerfile deleted file mode 100644 index f4d11ff..0000000 --- a/docker/production/Dockerfile +++ /dev/null @@ -1,19 +0,0 @@ -ARG base_commit -FROM ruby:3.1.2-slim -ENV RAILS_ENV production -# RUN apk add --no-cache build-base tzdata sqlite sqlite-dev postgresql-dev git && gem install tzinfo-data -RUN apt-get update -qq && apt-get install -y openssl git build-essential libpq-dev nodejs && apt-get clean && gem install tzinfo-data -WORKDIR /app -COPY Gemfile* /app/ -RUN gem install bundler:2.3.13 && bundle config set deployment 'true' && bundle install -COPY app /app/app -COPY bin /app/bin -COPY config /app/config -COPY db /app/db -COPY public /app/public -COPY script /app/script -COPY config.ru Rakefile /app/ -# Duplicate production environment to beta environment -COPY config/environments/production.rb config/environments/beta.rb -EXPOSE 3000 -CMD bundle exec rails db:migrate && bundle exec rails s -p 3000 -b 0.0.0.0 diff --git a/docker/test/Dockerfile b/docker/test/Dockerfile deleted file mode 100644 index a2446ba..0000000 --- a/docker/test/Dockerfile +++ /dev/null @@ -1,13 +0,0 @@ -# Build production container locally first with following tag -ARG base_commit -FROM registry.gitlab.com/turniere/turniere-backend/production/commits:$base_commit -WORKDIR /app -RUN apt-get update -qq && apt-get install -y python3 python3-pip && apt-get clean && python3 -m pip install --no-cache-dir invoke -RUN bundle config set with 'development test' && bundle install -COPY tasks.py /app/tasks.py -COPY e2e /app/e2e -COPY lib /app/lib -COPY spec /app/spec -COPY .rspec /app/.rspec -ENV RAILS_ENV test -CMD bundle exec rails db:migrate && bundle exec rails spec diff --git a/tasks.py b/tasks.py index 6fd9f3d..af6fa28 100644 --- a/tasks.py +++ b/tasks.py @@ -31,6 +31,9 @@ E2E_ALT_USERNAME = "e2e-alt-user" PRODUCTION_TAG = "registry.gitlab.com/turniere/turniere-backend/production/commits:local" TEST_TAG = "registry.gitlab.com/turniere/turniere-backend/test/commits:local" +DOCKERFILE_PATH = "Dockerfile" +PRODUCTION_DOCKER_TARGET = "production" +TEST_DOCKER_TARGET = "test" BLACKBOX_COMPOSE_FILE = "docker-compose.blackbox.yml" BLACKBOX_PROJECT = "turniere-blackbox" BLACKBOX_FOLLOW_COMPOSE_FILE = "docker-compose.blackbox-follow.yml" @@ -228,6 +231,31 @@ def _format_command(command): return " ".join(shlex.quote(part) for part in command) +def _docker_build_command(tag, target, platforms=None, push=False): + if platforms and "," in str(platforms) and not push: + raise ValueError("multi-platform buildx builds require push=True") + + if platforms or push: + command = [ + "docker", + "buildx", + "build", + "--target", + target, + "-t", + tag, + "-f", + DOCKERFILE_PATH, + ] + if platforms: + command.extend(["--platform", platforms]) + command.append("--push" if push else "--load") + command.append(".") + return _format_command(command) + + return f"docker build --target {target} -t {tag} -f {DOCKERFILE_PATH} ." + + def _rspec_command(*paths): command = ["bundle", "exec", "rspec", *paths] if os.environ.get("CI"): @@ -814,15 +842,15 @@ def scenario_render_profile( @task(name="docker-build-production") -def docker_build_production(c, tag=PRODUCTION_TAG): +def docker_build_production(c, tag=PRODUCTION_TAG, platforms=None, push=False): """Build the production Docker image.""" - c.run(f"docker build -t {tag} -f docker/production/Dockerfile .", pty=True) + c.run(_docker_build_command(tag, PRODUCTION_DOCKER_TARGET, platforms=platforms, push=push), pty=True) @task(name="docker-build-test") -def docker_build_test(c, tag=TEST_TAG): +def docker_build_test(c, tag=TEST_TAG, platforms=None, push=False): """Build the test Docker image.""" - c.run(f"docker build --build-arg base_commit=local -t {tag} -f docker/test/Dockerfile .", pty=True) + c.run(_docker_build_command(tag, TEST_DOCKER_TARGET, platforms=platforms, push=push), pty=True) @task(name="docker-build-all") From dfb67ac8500a65620483d1f056d6ebe28f2b096e Mon Sep 17 00:00:00 2001 From: Malaber Date: Thu, 23 Apr 2026 13:40:37 +0200 Subject: [PATCH 2/7] ci: test shared buildx pipeline --- .gitlab-ci.yml | 76 ++++------------------------------- doc/buildx_migration_notes.md | 60 +++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 68 deletions(-) create mode 100644 doc/buildx_migration_notes.md diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 216d302..7140384 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -5,76 +5,22 @@ stages: - deploy variables: - # Disable the shared image build jobs from turniere-infra; this repo owns buildx builds below. - DOCKER_IMAGE_ENVS: "" - DOCKER_BUILDKIT: "1" - BUILDX_PLATFORMS: "linux/amd64,linux/arm64" + DOCKER_IMAGE_ENVS: "production test" + DOCKER_IMAGE_PLATFORMS: "linux/amd64,linux/arm64" + DOCKERFILE_PATH: "Dockerfile" + DOCKER_BUILD_TARGETS: "production=production test=test" include: - project: 'turniere/turniere-infra' + ref: 'codex/buildx-multiarch-template' file: '/ci/pipeline.yaml' -.buildx-image: - stage: build - tags: - - shell - before_script: - - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY" - - export BUILDX_BUILDER_NAME="turniere-buildx-$CI_PIPELINE_ID-$CI_JOB_ID" - - docker buildx create --name "$BUILDX_BUILDER_NAME" --driver docker-container --use - - docker buildx inspect --builder "$BUILDX_BUILDER_NAME" --bootstrap - after_script: - - export BUILDX_BUILDER_NAME="turniere-buildx-$CI_PIPELINE_ID-$CI_JOB_ID" - - docker buildx rm "$BUILDX_BUILDER_NAME" || true - - docker logout "$CI_REGISTRY" || true - -build production image: - extends: .buildx-image - script: - - >- - docker buildx build - --builder "$BUILDX_BUILDER_NAME" - --file Dockerfile - --target production - --platform "$BUILDX_PLATFORMS" - --cache-from "type=registry,ref=$CI_REGISTRY_IMAGE/buildcache:production" - --cache-to "type=registry,ref=$CI_REGISTRY_IMAGE/buildcache:production,mode=max" - --tag "$CI_REGISTRY_IMAGE/production/commits:$CI_COMMIT_SHA" - --push - . - rules: - - if: $CI_PIPELINE_SOURCE != "push" - when: never - - when: always - -build test image: - extends: .buildx-image - script: - - >- - docker buildx build - --builder "$BUILDX_BUILDER_NAME" - --file Dockerfile - --target test - --platform "$BUILDX_PLATFORMS" - --cache-from "type=registry,ref=$CI_REGISTRY_IMAGE/buildcache:production" - --cache-from "type=registry,ref=$CI_REGISTRY_IMAGE/buildcache:test" - --cache-to "type=registry,ref=$CI_REGISTRY_IMAGE/buildcache:test,mode=max" - --tag "$CI_REGISTRY_IMAGE/test/commits:$CI_COMMIT_SHA" - --push - . - rules: - - if: $CI_PIPELINE_SOURCE != "push" - when: never - - when: always - rails spec: stage: test - needs: - - job: build_image tags: - docker needs: - - build test image + - job: build_image image: $CI_REGISTRY_IMAGE/test/commits:$CI_COMMIT_SHA parallel: matrix: @@ -101,13 +47,10 @@ rails spec: rails e2e spec: stage: e2e - needs: - - job: build_image tags: - docker needs: - - build production image - - build test image + - job: build_image image: $CI_REGISTRY_IMAGE/test/commits:$CI_COMMIT_SHA services: - name: postgres:16 @@ -157,13 +100,10 @@ rails e2e spec: rails follow sync e2e spec: stage: e2e - needs: - - job: build_image tags: - docker needs: - - build production image - - build test image + - job: build_image image: $CI_REGISTRY_IMAGE/test/commits:$CI_COMMIT_SHA services: - name: postgres:16 diff --git a/doc/buildx_migration_notes.md b/doc/buildx_migration_notes.md new file mode 100644 index 0000000..d6faa5e --- /dev/null +++ b/doc/buildx_migration_notes.md @@ -0,0 +1,60 @@ +# Buildx Migration Notes + +This backend branch temporarily pins the shared GitLab include to the infra branch +`codex/buildx-multiarch-template` so the new shared image build flow can be tested +end to end before the infra MR is merged. + +After the infra MR is merged: + +- remove the temporary `ref:` override from `.gitlab-ci.yml` +- keep the repo-level variables that describe how this repo maps images to a single + multi-stage `Dockerfile` + +## Backend pattern + +Backend now uses: + +- one root `Dockerfile` +- `DOCKER_IMAGE_ENVS: "production test"` +- `DOCKERFILE_PATH: "Dockerfile"` +- `DOCKER_BUILD_TARGETS: "production=production test=test"` +- `DOCKER_IMAGE_PLATFORMS: "linux/amd64,linux/arm64"` + +## turniere-frontend + +Frontend can move in two steps. + +1. No Dockerfile restructuring required for the shared buildx rollout itself. + Its current `docker/production/Dockerfile` already fits the shared template. +2. To add Raspberry Pi support, verify the current production image really builds on + `linux/arm64`. + The current file uses `node:16-alpine` and `alpine`; that should be checked in CI. + +Recommended frontend CI change after infra is merged: + +- keep `DOCKER_IMAGE_ENVS: "production"` +- add `DOCKER_IMAGE_PLATFORMS: "linux/amd64,linux/arm64"` + +Optional frontend follow-up: + +- if you want the same repo shape as backend, replace `docker/production/Dockerfile` + with a root multi-stage `Dockerfile`, then add: + `DOCKERFILE_PATH: "Dockerfile"` and + `DOCKER_BUILD_TARGETS: "production=production"` + +## turniere-match + +Match also does not need a Dockerfile restructuring for the shared buildx rollout. +Its current `docker/production/Dockerfile` already matches the default shared +template contract. + +Recommended match CI change after infra is merged: + +- keep `DOCKER_IMAGE_ENVS: "production"` +- add `DOCKER_IMAGE_PLATFORMS: "linux/amd64,linux/arm64"` + +Suggested follow-up: + +- run a multi-arch build smoke test for the Alpine-based Python image +- if packaging or native wheels become slow on arm64, consider a multi-stage + Dockerfile with a slimmer runtime image similar to the backend pattern From 5288d26fa4c92e65112398fb490124f8ec458c64 Mon Sep 17 00:00:00 2001 From: Malaber Date: Thu, 23 Apr 2026 14:28:27 +0200 Subject: [PATCH 4/7] ci: make arm builds on-demand --- .gitlab-ci.yml | 1 - doc/buildx_migration_notes.md | 7 ++++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7140384..581ea77 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -6,7 +6,6 @@ stages: variables: DOCKER_IMAGE_ENVS: "production test" - DOCKER_IMAGE_PLATFORMS: "linux/amd64,linux/arm64" DOCKERFILE_PATH: "Dockerfile" DOCKER_BUILD_TARGETS: "production=production test=test" diff --git a/doc/buildx_migration_notes.md b/doc/buildx_migration_notes.md index d6faa5e..0358c44 100644 --- a/doc/buildx_migration_notes.md +++ b/doc/buildx_migration_notes.md @@ -18,7 +18,8 @@ Backend now uses: - `DOCKER_IMAGE_ENVS: "production test"` - `DOCKERFILE_PATH: "Dockerfile"` - `DOCKER_BUILD_TARGETS: "production=production test=test"` -- `DOCKER_IMAGE_PLATFORMS: "linux/amd64,linux/arm64"` +- shared infra template builds amd64 automatically on push pipelines +- shared infra template builds arm64 manually on branch pipelines and automatically on default-branch pushes ## turniere-frontend @@ -33,7 +34,7 @@ Frontend can move in two steps. Recommended frontend CI change after infra is merged: - keep `DOCKER_IMAGE_ENVS: "production"` -- add `DOCKER_IMAGE_PLATFORMS: "linux/amd64,linux/arm64"` +- no extra platform variable needed if default shared behavior is acceptable Optional frontend follow-up: @@ -51,7 +52,7 @@ template contract. Recommended match CI change after infra is merged: - keep `DOCKER_IMAGE_ENVS: "production"` -- add `DOCKER_IMAGE_PLATFORMS: "linux/amd64,linux/arm64"` +- no extra platform variable needed if default shared behavior is acceptable Suggested follow-up: From 9d4cc21b7b9aaef1f0e94653337a76aa5f6cc4bd Mon Sep 17 00:00:00 2001 From: Malaber Date: Thu, 23 Apr 2026 14:50:45 +0200 Subject: [PATCH 5/7] fix: include Gemfile lock in images --- Dockerfile | 2 ++ tasks.py | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 00f2006..3ce651c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -56,6 +56,7 @@ FROM runtime-base AS production ENV RAILS_ENV=production COPY --from=bundle-production /usr/local/bundle /usr/local/bundle +COPY Gemfile Gemfile.lock /app/ COPY app /app/app COPY bin /app/bin COPY config /app/config @@ -83,6 +84,7 @@ RUN apt-get update -qq \ && python3 -m pip install --no-cache-dir invoke COPY --from=bundle-test /usr/local/bundle /usr/local/bundle +COPY Gemfile Gemfile.lock /app/ COPY app /app/app COPY bin /app/bin COPY config /app/config diff --git a/tasks.py b/tasks.py index af6fa28..96f2020 100644 --- a/tasks.py +++ b/tasks.py @@ -49,9 +49,10 @@ BLACKBOX_MAILGUN_API_KEY = "blackbox-test-api-key" BLACKBOX_MAILGUN_DOMAIN = "blackbox.example.com" SPEC_ROOT = Path("spec") E2E_SPEC_ROOT = SPEC_ROOT / "e2e" / "http" -BUNDLER_VERSION = Path("Gemfile.lock").read_text(encoding="utf-8").split("BUNDLED WITH", 1)[1].strip().splitlines()[-1].strip() ROOT = Path(__file__).resolve().parent DB_DIR = ROOT / "db" +GEMFILE_LOCK_PATH = ROOT / "Gemfile.lock" +BUNDLER_VERSION = GEMFILE_LOCK_PATH.read_text(encoding="utf-8").split("BUNDLED WITH", 1)[1].strip().splitlines()[-1].strip() def _env(**overrides): From e76f01b0eec7ab938c28b9d346af39d5a265cf5c Mon Sep 17 00:00:00 2001 From: Malaber Date: Thu, 23 Apr 2026 15:06:41 +0200 Subject: [PATCH 6/7] fix: keep gems in app image --- Dockerfile | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 3ce651c..9fb91e7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,7 +10,8 @@ ARG BUNDLER_VERSION ENV LANG=C.UTF-8 \ BUNDLE_JOBS=4 \ BUNDLE_RETRY=3 \ - BUNDLE_PATH=/usr/local/bundle + BUNDLE_PATH=/app/vendor/bundle \ + BUNDLE_APP_CONFIG=/app/vendor/bundle WORKDIR /app @@ -56,6 +57,7 @@ FROM runtime-base AS production ENV RAILS_ENV=production COPY --from=bundle-production /usr/local/bundle /usr/local/bundle +COPY --from=bundle-production /app/vendor/bundle /app/vendor/bundle COPY Gemfile Gemfile.lock /app/ COPY app /app/app COPY bin /app/bin @@ -84,6 +86,7 @@ RUN apt-get update -qq \ && python3 -m pip install --no-cache-dir invoke COPY --from=bundle-test /usr/local/bundle /usr/local/bundle +COPY --from=bundle-test /app/vendor/bundle /app/vendor/bundle COPY Gemfile Gemfile.lock /app/ COPY app /app/app COPY bin /app/bin From 7314394b69bf82e712639d72b519e6f45a18517d Mon Sep 17 00:00:00 2001 From: Malaber Date: Thu, 23 Apr 2026 16:40:12 +0200 Subject: [PATCH 7/7] ci: use shared infra pipeline from master --- .gitlab-ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 581ea77..02802aa 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -11,7 +11,6 @@ variables: include: - project: 'turniere/turniere-infra' - ref: 'codex/buildx-multiarch-template' file: '/ci/pipeline.yaml' rails spec: