name: CI on: push: pull_request: workflow_dispatch: permissions: contents: read # Protected runs get their own group; publication is queued at job level. concurrency: group: ci-${{ github.workflow }}-${{ github.ref }}-${{ github.event_name != 'pull_request' && github.ref_protected && github.run_id || 'checks' }} cancel-in-progress: ${{ github.event_name == 'pull_request' || !github.ref_protected }} defaults: run: shell: bash env: DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} jobs: python-tests: runs-on: ubuntu-24.04 timeout-minutes: 15 env: PIP_DISABLE_PIP_VERSION_CHECK: "1" steps: - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 with: persist-credentials: false - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 with: python-version: "3.13" cache: pip cache-dependency-path: pyproject.toml - name: Run Python tests run: sh ci/python-tests.sh - name: Upload JUnit report if: ${{ always() }} uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: python-test-results path: reports/pytest.xml retention-days: 7 if-no-files-found: warn javascript-check: runs-on: ubuntu-24.04 timeout-minutes: 5 steps: - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 with: persist-credentials: false - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 with: node-version: "24" package-manager-cache: false - name: Check JavaScript syntax run: node --check provisioner/static/app.js container-policy: runs-on: ubuntu-24.04 timeout-minutes: 5 permissions: {} outputs: publish: ${{ steps.policy.outputs.publish }} steps: # Ref names are read from the runner environment, never inserted into shell code. # The container script independently enforces these publication rules. - name: Select verification or publication id: policy run: | publish=false if [[ "$GITHUB_REF_PROTECTED" == true ]] && [[ "$GITHUB_EVENT_NAME" == push || "$GITHUB_EVENT_NAME" == workflow_dispatch ]]; then if [[ "$GITHUB_REF_TYPE" == branch && -n "$DEFAULT_BRANCH" && "$GITHUB_REF_NAME" == "$DEFAULT_BRANCH" ]] || [[ "$GITHUB_REF_TYPE" == tag && "$GITHUB_REF_NAME" =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then publish=true fi fi printf 'publish=%s\n' "$publish" >> "$GITHUB_OUTPUT" container-verify: needs: [python-tests, javascript-check, container-policy] if: ${{ needs.container-policy.outputs.publish == 'false' }} runs-on: ubuntu-24.04 timeout-minutes: 30 env: DOCKER_HOST: unix:///var/run/docker.sock DOCKER_BUILDKIT: "1" steps: - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 with: persist-credentials: false - name: Check Docker tools run: | docker info docker buildx version - name: Build and smoke-test image run: sh ci/container.sh verify - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: container-build path: build.env retention-days: 7 if-no-files-found: error container-publish: needs: [python-tests, javascript-check, container-policy] if: ${{ needs.container-policy.outputs.publish == 'true' }} runs-on: ubuntu-24.04 timeout-minutes: 30 permissions: contents: read packages: write concurrency: group: ghcr-container-publish cancel-in-progress: false queue: max env: DOCKER_HOST: unix:///var/run/docker.sock DOCKER_BUILDKIT: "1" steps: - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 with: persist-credentials: false - name: Check Docker tools run: | docker info docker buildx version - name: Build, smoke-test and publish image env: GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: sh ci/container.sh publish - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: container-deploy path: | build.env deploy.env retention-days: 30 if-no-files-found: error