Skip to content

Allow Odoo target replacement to repair failed lanes #3342

Allow Odoo target replacement to repair failed lanes

Allow Odoo target replacement to repair failed lanes #3342

Workflow file for this run

---
name: CI
"on":
push:
branches:
- main
pull_request:
permissions:
contents: read
jobs:
static_checks:
if: >-
github.event_name != 'pull_request' ||
github.event.pull_request.head.repo.full_name == github.repository
runs-on:
- self-hosted
- ${{ vars.LAUNCHPLANE_RUNNER_LABEL }}
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install uv
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
with:
enable-cache: true
cache-dependency-glob: uv.lock
- name: Install Python
run: uv python install 3.13
- name: Run Ruff
run: uv run --extra dev ruff check .
- name: Run mypy
run: uv run --extra dev mypy control_plane tests
- name: Audit Python dependencies
run: |
# PyJWT is only used for GitHub OIDC RS256 verification; this
# disputed advisory has no fixed release and concerns app-chosen
# weak symmetric keys.
uv run --with pip-audit pip-audit --ignore-vuln PYSEC-2025-183
static_checks_fork:
if: >-
github.event_name == 'pull_request' &&
github.event.pull_request.head.repo.full_name != github.repository
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install uv
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
with:
enable-cache: true
cache-dependency-glob: uv.lock
- name: Install Python
run: uv python install 3.13
- name: Run Ruff
run: uv run --extra dev ruff check .
- name: Run mypy
run: uv run --extra dev mypy control_plane tests
- name: Audit Python dependencies
run: |
# PyJWT is only used for GitHub OIDC RS256 verification; this
# disputed advisory has no fixed release and concerns app-chosen
# weak symmetric keys.
uv run --with pip-audit pip-audit --ignore-vuln PYSEC-2025-183
container_scan:
if: >-
github.event_name != 'pull_request' ||
github.event.pull_request.head.repo.full_name == github.repository
runs-on:
- self-hosted
- ${{ vars.LAUNCHPLANE_RUNNER_LABEL }}
permissions:
contents: read
packages: read
env:
LAUNCHPLANE_CI_IMAGE_TAG: >-
launchplane-ci:${{ github.run_id }}-${{ github.run_attempt }}
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
with:
name: launchplane-ci-${{ github.run_id }}-${{ github.run_attempt }}
- name: Log in to GHCR for build cache
uses: docker/login-action@06fb636fac595d6fb4b28a5dfcb21a6f5091859c # v4.5.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
- name: Build runtime image
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
builder: ${{ steps.buildx.outputs.name }}
context: .
load: true
tags: ${{ env.LAUNCHPLANE_CI_IMAGE_TAG }}
cache-from: >-
type=gha,scope=launchplane-ci
cache-to: >-
type=gha,scope=launchplane-ci,mode=max
- name: Restore Trivy cache
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ runner.temp }}/trivy-cache
key: ${{ runner.os }}-trivy-${{ github.run_id }}
restore-keys: |
${{ runner.os }}-trivy-
- name: Scan runtime image
env:
LAUNCHPLANE_TRIVY_CACHE_DIR: ${{ runner.temp }}/trivy-cache
run: |
mkdir -p "$LAUNCHPLANE_TRIVY_CACHE_DIR"
docker run --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
-v "${LAUNCHPLANE_TRIVY_CACHE_DIR}:/root/.cache" \
ghcr.io/aquasecurity/trivy:0.70.0@sha256:be1190afcb28352bfddc4ddeb71470835d16462af68d310f9f4bca710961a41e \
image \
--scanners vulnerability \
--ignore-unfixed \
--severity HIGH,CRITICAL \
--exit-code 1 \
"$LAUNCHPLANE_CI_IMAGE_TAG"
- name: Remove scanned runtime image
if: always()
run: docker image rm "$LAUNCHPLANE_CI_IMAGE_TAG" || true
container_scan_fork:
if: >-
github.event_name == 'pull_request' &&
github.event.pull_request.head.repo.full_name != github.repository
runs-on: ubuntu-latest
permissions:
contents: read
env:
LAUNCHPLANE_CI_IMAGE_TAG: >-
launchplane-ci:${{ github.run_id }}-${{ github.run_attempt }}
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
with:
name: launchplane-ci-${{ github.run_id }}-${{ github.run_attempt }}
- name: Build runtime image
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
builder: ${{ steps.buildx.outputs.name }}
context: .
load: true
tags: ${{ env.LAUNCHPLANE_CI_IMAGE_TAG }}
cache-from: >-
type=gha,scope=launchplane-ci
- name: Restore Trivy cache
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ runner.temp }}/trivy-cache
key: ${{ runner.os }}-trivy-${{ github.run_id }}
restore-keys: |
${{ runner.os }}-trivy-
- name: Scan runtime image
env:
LAUNCHPLANE_TRIVY_CACHE_DIR: ${{ runner.temp }}/trivy-cache
run: |
mkdir -p "$LAUNCHPLANE_TRIVY_CACHE_DIR"
docker run --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
-v "${LAUNCHPLANE_TRIVY_CACHE_DIR}:/root/.cache" \
ghcr.io/aquasecurity/trivy:0.70.0@sha256:be1190afcb28352bfddc4ddeb71470835d16462af68d310f9f4bca710961a41e \
image \
--scanners vulnerability \
--ignore-unfixed \
--severity HIGH,CRITICAL \
--exit-code 1 \
"$LAUNCHPLANE_CI_IMAGE_TAG"
- name: Remove scanned runtime image
if: always()
run: docker image rm "$LAUNCHPLANE_CI_IMAGE_TAG" || true
frontend_validate:
if: >-
github.event_name != 'pull_request' ||
github.event.pull_request.head.repo.full_name == github.repository
runs-on:
- self-hosted
- ${{ vars.LAUNCHPLANE_RUNNER_LABEL }}
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "22"
- name: Install uv
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
with:
enable-cache: true
cache-dependency-glob: uv.lock
- name: Install Python
run: uv python install 3.13
- name: Enable pnpm
run: |
corepack enable
corepack prepare pnpm@10.10.0 --activate
- name: Resolve pnpm store path
id: pnpm-store
run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
- name: Restore pnpm store
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ steps.pnpm-store.outputs.path }}
key: >-
${{ runner.os }}-node-22-pnpm-${{ hashFiles('frontend/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-node-22-pnpm-
- name: Install frontend dependencies
run: pnpm --dir frontend install --frozen-lockfile
- name: Validate frontend
run: pnpm --dir frontend validate
frontend_validate_fork:
if: >-
github.event_name == 'pull_request' &&
github.event.pull_request.head.repo.full_name != github.repository
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "22"
- name: Install uv
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
with:
enable-cache: true
cache-dependency-glob: uv.lock
- name: Install Python
run: uv python install 3.13
- name: Enable pnpm
run: |
corepack enable
corepack prepare pnpm@10.10.0 --activate
- name: Resolve pnpm store path
id: pnpm-store
run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
- name: Restore pnpm store
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ steps.pnpm-store.outputs.path }}
key: >-
${{ runner.os }}-node-22-pnpm-${{ hashFiles('frontend/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-node-22-pnpm-
- name: Install frontend dependencies
run: pnpm --dir frontend install --frozen-lockfile
- name: Validate frontend
run: pnpm --dir frontend validate
frontend_browser_smoke:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "22"
- name: Enable pnpm
run: |
corepack enable
corepack prepare pnpm@10.10.0 --activate
- name: Resolve pnpm store path
id: pnpm-store
run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
- name: Restore pnpm store
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ steps.pnpm-store.outputs.path }}
key: >-
${{ runner.os }}-node-22-pnpm-${{ hashFiles('frontend/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-node-22-pnpm-
- name: Install frontend dependencies
run: pnpm --dir frontend install --frozen-lockfile
- name: Install Chromium
run: pnpm --dir frontend exec playwright install --with-deps chromium
- name: Run browser smoke
run: pnpm --dir frontend test:browser
- name: Upload browser evidence
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: browser-smoke-${{ github.run_attempt }}
path: tmp/browser-smoke
if-no-files-found: error
retention-days: 14
test_timing_snapshot:
if: >-
github.event_name != 'pull_request' ||
github.event.pull_request.head.repo.full_name == github.repository
runs-on:
- self-hosted
- ${{ vars.LAUNCHPLANE_RUNNER_LABEL }}
env:
UNITTEST_TIMINGS_CACHE: .ci-cache/unittest-timings/history.json
steps:
- name: Restore unittest timings
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ env.UNITTEST_TIMINGS_CACHE }}
key: >-
${{ runner.os }}-python-3.13-unittest-timings-${{ github.ref_name }}-${{ github.run_id }}-${{ github.run_attempt }}
restore-keys: |
${{ runner.os }}-python-3.13-unittest-timings-${{ github.ref_name }}-${{ github.run_id }}-
${{ runner.os }}-python-3.13-unittest-timings-${{ github.ref_name }}-
${{ runner.os }}-python-3.13-unittest-timings-
- name: Freeze unittest timing snapshot
run: |
set -euo pipefail
snapshot_directory="${RUNNER_TEMP}/unittest-timing-snapshot"
mkdir -p "${snapshot_directory}"
if [ -f "${UNITTEST_TIMINGS_CACHE}" ]; then
cp "${UNITTEST_TIMINGS_CACHE}" "${snapshot_directory}/history.json"
fi
printf '%s:%s\n' "${GITHUB_RUN_ID}" "${GITHUB_RUN_ATTEMPT}" \
> "${snapshot_directory}/snapshot-id"
- name: Upload unittest timing snapshot
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: unittest-timing-snapshot
path: ${{ runner.temp }}/unittest-timing-snapshot
if-no-files-found: error
overwrite: true
retention-days: 30
test_shards:
needs: test_timing_snapshot
if: >-
github.event_name != 'pull_request' ||
github.event.pull_request.head.repo.full_name == github.repository
name: test_shard (${{ matrix.shard_index }})
env:
UNITTEST_SHARD_COUNT: "12"
UNITTEST_MAX_TESTS_PER_TARGET: "20"
UNITTEST_MAX_SECONDS_PER_TARGET: "30"
runs-on:
- self-hosted
- ${{ vars.LAUNCHPLANE_RUNNER_LABEL }}
strategy:
fail-fast: false
matrix:
shard_index: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install uv
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
with:
enable-cache: true
cache-dependency-glob: uv.lock
- name: Install Python
run: uv python install 3.13
- name: Download unittest timing snapshot
# v7 downloads one named artifact; the following v8 step needs merge-multiple.
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: unittest-timing-snapshot
path: ${{ runner.temp }}/unittest-timing-snapshot
- name: Show unit test shard plan
run: >-
uv run --extra dev launchplane ci unittest-shard plan
--shard-count "${UNITTEST_SHARD_COUNT}"
--timings-file "${RUNNER_TEMP}/unittest-timing-snapshot/history.json"
--max-tests-per-target "${UNITTEST_MAX_TESTS_PER_TARGET}"
--max-seconds-per-target "${UNITTEST_MAX_SECONDS_PER_TARGET}"
- name: Run unit test shard
run: >-
uv run --extra dev launchplane ci unittest-shard run
--shard-count "${UNITTEST_SHARD_COUNT}"
--shard-index ${{ matrix.shard_index }}
--timings-file "${RUNNER_TEMP}/unittest-timing-snapshot/history.json"
--max-tests-per-target "${UNITTEST_MAX_TESTS_PER_TARGET}"
--max-seconds-per-target "${UNITTEST_MAX_SECONDS_PER_TARGET}"
--timings-output "${RUNNER_TEMP}/unittest-timings/shard-${{ matrix.shard_index }}.json"
- name: Upload unittest timings
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: unittest-timings-${{ matrix.shard_index }}
path: ${{ runner.temp }}/unittest-timings/shard-${{ matrix.shard_index }}.json
if-no-files-found: error
overwrite: true
retention-days: 30
test:
needs: [test_timing_snapshot, test_shards]
if: >-
always() &&
(github.event_name != 'pull_request' ||
github.event.pull_request.head.repo.full_name == github.repository)
runs-on:
- self-hosted
- ${{ vars.LAUNCHPLANE_RUNNER_LABEL }}
env:
UNITTEST_TIMINGS_CACHE: .ci-cache/unittest-timings/history.json
UNITTEST_SHARD_COUNT: "12"
UNITTEST_MAX_TESTS_PER_TARGET: "20"
UNITTEST_MAX_SECONDS_PER_TARGET: "30"
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install uv
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
with:
enable-cache: true
cache-dependency-glob: uv.lock
- name: Install Python
run: uv python install 3.13
- name: Require successful test shards
run: |
if [ "${{ needs.test_timing_snapshot.result }}" != "success" ]; then
echo "test_timing_snapshot result: ${{ needs.test_timing_snapshot.result }}" >&2
exit 1
fi
if [ "${{ needs.test_shards.result }}" != "success" ]; then
echo "test_shards result: ${{ needs.test_shards.result }}" >&2
exit 1
fi
- name: Download unittest timing snapshot
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: unittest-timing-snapshot
path: ${{ runner.temp }}/unittest-timing-snapshot
- name: Download unittest timings
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: unittest-timings-*
path: ${{ runner.temp }}/unittest-timings
merge-multiple: true
- name: Aggregate unittest timings
run: >-
uv run --extra dev launchplane ci unittest-shard aggregate
--shard-count "${UNITTEST_SHARD_COUNT}"
--results-dir "${RUNNER_TEMP}/unittest-timings"
--timings-file "${RUNNER_TEMP}/unittest-timing-snapshot/history.json"
--max-tests-per-target "${UNITTEST_MAX_TESTS_PER_TARGET}"
--max-seconds-per-target "${UNITTEST_MAX_SECONDS_PER_TARGET}"
--timings-output "${UNITTEST_TIMINGS_CACHE}"
- name: Save unittest timings
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ env.UNITTEST_TIMINGS_CACHE }}
key: >-
${{ runner.os }}-python-3.13-unittest-timings-${{ github.ref_name }}-${{ github.run_id }}-${{ github.run_attempt }}
test_fork:
if: >-
github.event_name == 'pull_request' &&
github.event.pull_request.head.repo.full_name != github.repository
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install uv
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
with:
enable-cache: true
cache-dependency-glob: uv.lock
- name: Install Python
run: uv python install 3.13
- name: Run unit tests
run: uv run --extra dev python -m unittest
postgres_integration:
if: >-
github.event_name != 'pull_request' ||
github.event.pull_request.head.repo.full_name == github.repository
runs-on:
- self-hosted
- ${{ vars.LAUNCHPLANE_RUNNER_LABEL }}
services:
postgres:
image: postgres:17@sha256:0af65001d05296a2ead57ac4a6412433d8913d1bb5d0c88435a7d1e1ee5cb04b
env:
POSTGRES_USER: launchplane_test
POSTGRES_DB: launchplane_root
POSTGRES_HOST_AUTH_METHOD: trust
ports:
- "127.0.0.1::5432"
options: >-
--health-cmd "pg_isready -U launchplane_test -d launchplane_root"
--health-interval 5s
--health-timeout 5s
--health-retries 20
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install uv
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
with:
enable-cache: true
cache-dependency-glob: uv.lock
- name: Install Python
run: uv python install 3.13
- name: Run PostgreSQL integration tests
env:
LAUNCHPLANE_TEST_POSTGRES_URL: >-
postgresql+psycopg://launchplane_test@127.0.0.1:${{ job.services.postgres.ports['5432'] }}/launchplane_root
run: uv run --extra dev launchplane ci postgres-integration
ci_gate:
name: ci-gate
runs-on: ubuntu-latest
timeout-minutes: 5
needs:
- static_checks
- static_checks_fork
- container_scan
- container_scan_fork
- frontend_validate
- frontend_validate_fork
- frontend_browser_smoke
- test
- test_fork
- postgres_integration
if: always() && github.event_name == 'pull_request'
permissions:
contents: read
steps:
- name: Require successful CI path
env:
BASE_REPOSITORY: ${{ github.repository }}
HEAD_REPOSITORY: ${{ github.event.pull_request.head.repo.full_name }}
STATIC_CHECKS_RESULT: ${{ needs.static_checks.result }}
STATIC_CHECKS_FORK_RESULT: ${{ needs.static_checks_fork.result }}
CONTAINER_SCAN_RESULT: ${{ needs.container_scan.result }}
CONTAINER_SCAN_FORK_RESULT: ${{ needs.container_scan_fork.result }}
FRONTEND_VALIDATE_RESULT: ${{ needs.frontend_validate.result }}
FRONTEND_VALIDATE_FORK_RESULT: ${{ needs.frontend_validate_fork.result }}
FRONTEND_BROWSER_SMOKE_RESULT: ${{ needs.frontend_browser_smoke.result }}
TEST_RESULT: ${{ needs.test.result }}
TEST_FORK_RESULT: ${{ needs.test_fork.result }}
POSTGRES_INTEGRATION_RESULT: ${{ needs.postgres_integration.result }}
run: |
set -euo pipefail
require_success() {
check_name="$1"
check_result="$2"
if [ "${check_result}" != "success" ]; then
echo "${check_name} concluded with: ${check_result}" >&2
exit 1
fi
}
if [ "${HEAD_REPOSITORY}" = "${BASE_REPOSITORY}" ]; then
require_success static_checks "${STATIC_CHECKS_RESULT}"
require_success container_scan "${CONTAINER_SCAN_RESULT}"
require_success frontend_validate "${FRONTEND_VALIDATE_RESULT}"
require_success frontend_browser_smoke "${FRONTEND_BROWSER_SMOKE_RESULT}"
require_success test "${TEST_RESULT}"
require_success postgres_integration "${POSTGRES_INTEGRATION_RESULT}"
else
require_success static_checks_fork "${STATIC_CHECKS_FORK_RESULT}"
require_success container_scan_fork "${CONTAINER_SCAN_FORK_RESULT}"
require_success frontend_validate_fork "${FRONTEND_VALIDATE_FORK_RESULT}"
require_success frontend_browser_smoke "${FRONTEND_BROWSER_SMOKE_RESULT}"
require_success test_fork "${TEST_FORK_RESULT}"
fi