Skip to content

SEP-1909: Decide where ty enforcement runs and how it scopes #10202

SEP-1909: Decide where ty enforcement runs and how it scopes

SEP-1909: Decide where ty enforcement runs and how it scopes #10202

Workflow file for this run

# Main CI workflow
name: CI
on:
pull_request:
branches:
- main
types:
- labeled
- ready_for_review
- synchronize
- unlabeled
workflow_call:
workflow_dispatch:
permissions:
contents: read
pull-requests: read
concurrency:
group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
label-gate:
runs-on: ubuntu-latest
steps:
- name: Block merge on disqualifying labels
if: >-
contains(github.event.pull_request.labels.*.name, 'do not merge') ||
contains(github.event.pull_request.labels.*.name, 'qa in progress') ||
contains(github.event.pull_request.labels.*.name, 'qa failed')
run: |
echo "::error::PR is blocked by label. Remove 'do not merge', 'qa in progress', or 'qa failed' to proceed."
exit 1
- name: Require QA approval label
if: >-
!contains(github.event.pull_request.labels.*.name, 'qa passed') &&
!contains(github.event.pull_request.labels.*.name, 'qa not required')
run: |
echo "::error::PR requires 'qa passed' or 'qa not required' label to merge."
exit 1
# Detect changed file types to gate downstream jobs.
# Python/frontend patterns mirror .github/labeler.yml; CI also tracks
# .pre-commit-config.yaml separately to trigger precommit-python / precommit-frontend.
changes:
if: ${{ github.event.pull_request.draft == false }}
runs-on: ubuntu-latest
outputs:
python: ${{ steps.filter.outputs.python }}
precommit: ${{ steps.filter.outputs.precommit }}
frontend: ${{ steps.filter.outputs.frontend }}
shell: ${{ steps.filter.outputs.shell }}
sidecar: ${{ steps.filter.outputs.sidecar }}
pipeline: ${{ steps.filter.outputs.pipeline }}
steps:
- name: Checkout codebase
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Detect changed files
id: filter
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
with:
filters: |
python:
- '**/**.py'
# Nomad payloads are extensionless, so they match no *.py glob and
# would otherwise skip the pytest tier that guards their invariants.
- 'app/sep/apps/**/*payload'
- 'pyproject.toml'
- 'alembic.ini'
- 'poetry.lock'
- 'poetry.toml'
- 'settings.yaml'
- 'sidecar/settings.yaml'
- 'sidecar/settings-env.sh'
- 'sidecar/supervisord.conf'
- 'sidecar/healthcheck.sh'
- 'sidecar/entrypoint.sh'
- 'sidecar/wait_for_schema.sh'
- 'sidecar/Containerfile.sidecar'
- 'Makefile'
- 'frontend/packages/api/specs/**'
- '.github/labeler.yml'
- '.github/workflows/ci.yml'
precommit:
- '.pre-commit-config.yaml'
- '.github/workflows/ci.yml'
shell:
- '**/*.sh'
frontend:
- 'frontend/**'
- 'package.json'
- '.github/actions/setup-frontend-job/**'
sidecar:
- 'sidecar/**'
- 'Containerfile.base'
- 'alembic.ini'
- 'Makefile'
- 'app/**'
- 'snippets/**'
- 'pyproject.toml'
- 'poetry.lock'
pipeline:
- 'build/**/*.pipeline'
- 'scripts/lint_pipelines.groovy'
precommit-light:
if: ${{ github.event.pull_request.draft == false }}
needs:
- label-gate
runs-on: ubuntu-latest
steps:
- name: Checkout codebase
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Python
uses: actions/setup-python@28f2168f4d98ee0445e3c6321f6e6616c83dd5ec # v6.2.0
with:
python-version: "3.11"
- name: Install pre-commit
run: python -m pip install pre-commit==4.5.1
- name: Cache pre-commit hook environments
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/.cache/pre-commit
key: pre-commit-${{ runner.os }}-${{ hashFiles('.pre-commit-config.yaml') }}
restore-keys: |
pre-commit-${{ runner.os }}-
- name: Write lightweight pre-commit config
run: |
cat > .pre-commit-light-config.yaml <<'EOF'
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: cef0300fd0fc4d2a87a85fa2093c6b283ea36f4b
hooks:
- id: check-yaml
- id: check-json
- id: check-toml
- id: check-merge-conflict
- id: check-case-conflict
- id: check-added-large-files
- id: end-of-file-fixer
- id: trailing-whitespace
- id: fix-byte-order-marker
EOF
- name: Perform lightweight pre-commit checks
run: |
pre-commit run --config .pre-commit-light-config.yaml --all-files
precommit-python:
if: >-
${{
github.event.pull_request.draft == false &&
(
needs.changes.outputs.python == 'true' ||
needs.changes.outputs.precommit == 'true' ||
needs.changes.outputs.sidecar == 'true' ||
needs.changes.outputs.frontend == 'true' ||
needs.changes.outputs.shell == 'true'
)
}}
needs:
- label-gate
- changes
runs-on: ubuntu-latest
steps:
- name: Checkout codebase
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Python
uses: actions/setup-python@28f2168f4d98ee0445e3c6321f6e6616c83dd5ec # v6.2.0
with:
python-version: "3.11"
- name: Install pre-commit
run: python -m pip install pre-commit==4.5.1
- name: Cache pre-commit hook environments
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/.cache/pre-commit
key: pre-commit-${{ runner.os }}-${{ hashFiles('.pre-commit-config.yaml') }}
restore-keys: |
pre-commit-${{ runner.os }}-
- name: Perform Python and system pre-commit checks
env:
SKIP: oxfmt,oxlint
run: pre-commit run --all-files
precommit-frontend:
if: >-
${{
github.event.pull_request.draft == false &&
(
needs.changes.outputs.frontend == 'true' ||
needs.changes.outputs.precommit == 'true'
)
}}
needs:
- label-gate
- changes
runs-on: ubuntu-latest
steps:
- name: Checkout codebase
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup frontend job
uses: ./.github/actions/setup-frontend-job
- name: Run oxfmt
run: pnpm run format:check
working-directory: frontend
- name: Run oxlint
run: pnpm run lint
working-directory: frontend
python:
# `lint-fmt-only`: precommit-python already re-runs the bumped linters.
if: >-
${{
github.event.pull_request.draft == false &&
needs.changes.outputs.python == 'true' &&
!contains(github.event.pull_request.labels.*.name, 'lint-fmt-only')
}}
uses: ./.github/workflows/python.yaml
needs:
- label-gate
- changes
frontend:
if: >-
${{
github.event.pull_request.draft == false &&
needs.changes.outputs.frontend == 'true' &&
!contains(github.event.pull_request.labels.*.name, 'lint-fmt-only')
}}
uses: ./.github/workflows/frontend.yaml
needs:
- label-gate
- changes
build:
# Runs concurrently with the python test matrix — the docker image build
# consumes no test output, so it no longer gates on `python`. It starts as
# soon as label-gate and changes succeed, while tests are still running.
# Merge correctness is preserved by ci-success below, which
# aggregates python, frontend, and build.
# `lint-fmt-only`: audit-group deps not in the production image.
if: >-
${{
github.event.pull_request.draft == false &&
needs.label-gate.result == 'success' &&
needs.changes.result == 'success' &&
needs.changes.outputs.sidecar == 'true' &&
!contains(github.event.pull_request.labels.*.name, 'lint-fmt-only')
}}
runs-on: ubuntu-latest
needs:
- label-gate
- changes
steps:
- name: Checkout codebase
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@a0c73a10bb6782f082cd6714666c87911e4fc7d3 # v4.0.0
- name: Build base image
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: .
file: Containerfile.base
tags: sep:builder
load: true
cache-from: type=gha,scope=base
cache-to: type=gha,mode=max,scope=base
- name: Prepare app bundle
run: make pack
- name: Build app image
run: >-
docker build
--build-context "localhost/sep:builder=docker-image://sep:builder"
-f sidecar/Containerfile.sidecar
--build-arg SEP_RESTRICT_APPS=1
-t "sep:HEAD" .
- name: Smoke test - verify binaries and config files
run: |
docker run --rm --entrypoint /bin/sh "sep:HEAD" -c '
set -e
command -v valkey-server
command -v valkey-cli
command -v supervisord
test -f /home/sep/app/supervisord.conf
test -f /home/sep/app/healthcheck.sh
test -x /home/sep/app/entrypoint.sh
test -x /home/sep/app/wait_for_schema.sh
test -f /home/sep/app/settings-env.sh
test -f /home/sep/app/settings.yaml
'
- name: Smoke test - verify HEALTHCHECK instruction
run: |
healthcheck_test=$(docker inspect --format='{{json .Config.Healthcheck.Test}}' "sep:HEAD")
if [ -z "$healthcheck_test" ] || [ "$healthcheck_test" = "null" ] || [ "$healthcheck_test" = "[\"NONE\"]" ]; then
echo "::error::HEALTHCHECK instruction is missing or disabled (HEALTHCHECK NONE) in the side-car image"
exit 1
fi
healthcheck=$(docker inspect --format='{{json .Config.Healthcheck}}' "sep:HEAD")
echo "HEALTHCHECK present: $healthcheck"
- name: Smoke test - verify ENTRYPOINT is the side-car wrapper
run: |
entrypoint=$(docker inspect --format='{{json .Config.Entrypoint}}' "sep:HEAD")
case "$entrypoint" in
*entrypoint.sh*) ;;
*)
echo "::error::ENTRYPOINT is not the side-car wrapper, so the broker credential is never minted: $entrypoint"
exit 1
;;
esac
echo "ENTRYPOINT present: $entrypoint"
- name: Smoke test - restricted image ships exactly the activated apps
run: sidecar/verify_image_apps.sh "sep:HEAD" restricted
- name: Verify the purge layer is the last package-manager operation
run: python3 scripts/check_sidecar_purge.py --check-ordering
- name: Smoke test - purged packages are absent from the built image
run: |
pkgs="$(python3 scripts/check_sidecar_purge.py --print-packages | tr '\n' ' ')"
if [ -z "$pkgs" ]; then
echo "::error::the purge checker named no packages, so this check cannot run"
exit 1
fi
docker run --rm --entrypoint /bin/sh -e PURGED="$pkgs" "sep:HEAD" -c '
command -v dpkg-query > /dev/null 2>&1 || {
echo "::error::dpkg-query is absent, so package presence cannot be established"
exit 1
}
status=0
for p in $PURGED; do
if dpkg-query -s "$p" 2>/dev/null | grep -q "^Status: install ok installed"; then
echo "::error::$p is still installed in the built image"
status=1
fi
done
if command -v perl > /dev/null 2>&1; then
echo "::error::a perl binary resolves on PATH"
status=1
fi
exit $status
'
pipeline-syntax:
if: >-
${{
github.event.pull_request.draft == false &&
needs.changes.outputs.pipeline == 'true'
}}
needs:
- label-gate
- changes
runs-on: ubuntu-latest
steps:
- name: Checkout codebase
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Parse Jenkins pipeline Groovy syntax
run: |
pipelines=()
while IFS= read -r pipeline; do
pipelines+=("$pipeline")
done < <(git ls-files -- ':(glob)build/**/*.pipeline')
((${#pipelines[@]})) || {
echo "::error::No build/**/*.pipeline files found."
exit 1
}
docker run --rm -v "$PWD:/w" -w /w \
groovy@sha256:fcfaaa9eba20ccc3b438a70cb599e721996758ecf22de70dc0a85302c97822aa \
groovy scripts/lint_pipelines.groovy \
"${pipelines[@]}"
ci-success:
if: always()
needs:
- label-gate
- changes
- precommit-light
- precommit-python
- precommit-frontend
- python
- frontend
- build
- pipeline-syntax
runs-on: ubuntu-latest
steps:
- name: Fail if any dependency failed or was cancelled
if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')
run: exit 1