ci: cut queue depth with path filters and smarter concurrency
Skip heavy jobs for docs-only and out-of-scope paths while keeping Product CI as the branch-protection gate. Cancel superseded PR image validation runs; on main cancel stale builds but serialize publish so attest/manifest steps are never interrupted. Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
This commit is contained in:
parent
6fc734b0ae
commit
eeec01cd0e
71
.github/workflows/ci.yml
vendored
71
.github/workflows/ci.yml
vendored
@ -5,6 +5,20 @@ name: ci
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- "apps/**"
|
||||
- "packages/**"
|
||||
- "infra/**"
|
||||
- "scripts/**"
|
||||
- "ops/**"
|
||||
- ".github/**"
|
||||
- "package.json"
|
||||
- "pnpm-lock.yaml"
|
||||
- "pnpm-workspace.yaml"
|
||||
- "turbo.json"
|
||||
- "biome.json"
|
||||
- "vitest.config.ts"
|
||||
- "tsconfig*.json"
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
@ -15,8 +29,39 @@ concurrency:
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
name: Detect product changes
|
||||
runs-on: [self-hosted, evoo-ci, Linux]
|
||||
outputs:
|
||||
product: ${{ steps.filter.outputs.product }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
- uses: dorny/paths-filter@451258540fd6e81b87561f5d5adbd9863fc87cee # v3.0.2
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
product:
|
||||
- 'apps/**'
|
||||
- 'packages/**'
|
||||
- 'infra/**'
|
||||
- 'scripts/**'
|
||||
- 'ops/**'
|
||||
- '.github/**'
|
||||
- 'package.json'
|
||||
- 'pnpm-lock.yaml'
|
||||
- 'pnpm-workspace.yaml'
|
||||
- 'turbo.json'
|
||||
- 'biome.json'
|
||||
- 'vitest.config.ts'
|
||||
- 'tsconfig*.json'
|
||||
|
||||
lint:
|
||||
name: Lint
|
||||
needs: changes
|
||||
if: needs.changes.outputs.product == 'true'
|
||||
runs-on: [self-hosted, evoo-ci, Linux]
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
@ -51,6 +96,8 @@ jobs:
|
||||
|
||||
check:
|
||||
name: Typecheck
|
||||
needs: changes
|
||||
if: needs.changes.outputs.product == 'true'
|
||||
runs-on: [self-hosted, evoo-ci, Linux]
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
@ -88,6 +135,8 @@ jobs:
|
||||
|
||||
build:
|
||||
name: Production builds
|
||||
needs: changes
|
||||
if: needs.changes.outputs.product == 'true'
|
||||
runs-on: [self-hosted, evoo-ci, Linux]
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
@ -138,6 +187,8 @@ jobs:
|
||||
|
||||
test:
|
||||
name: Unit tests
|
||||
needs: changes
|
||||
if: needs.changes.outputs.product == 'true'
|
||||
runs-on: [self-hosted, evoo-ci, Linux]
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
@ -175,6 +226,8 @@ jobs:
|
||||
|
||||
test-integration:
|
||||
name: Postgres journeys
|
||||
needs: changes
|
||||
if: needs.changes.outputs.product == 'true'
|
||||
runs-on: [self-hosted, evoo-ci, Linux]
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
@ -210,6 +263,8 @@ jobs:
|
||||
|
||||
test-e2e:
|
||||
name: Web E2E
|
||||
needs: changes
|
||||
if: needs.changes.outputs.product == 'true'
|
||||
uses: ./.github/workflows/playwright.yml
|
||||
with:
|
||||
upload_artifacts: true
|
||||
@ -217,11 +272,12 @@ jobs:
|
||||
product-gate:
|
||||
name: Product CI
|
||||
if: always()
|
||||
needs: [lint, check, build, test, test-integration, test-e2e]
|
||||
needs: [changes, lint, check, build, test, test-integration, test-e2e]
|
||||
runs-on: [self-hosted, evoo-ci, Linux]
|
||||
steps:
|
||||
- name: Verify product checks
|
||||
env:
|
||||
PRODUCT: ${{ needs.changes.outputs.product }}
|
||||
LINT: ${{ needs.lint.result }}
|
||||
CHECK: ${{ needs.check.result }}
|
||||
BUILD: ${{ needs.build.result }}
|
||||
@ -230,11 +286,18 @@ jobs:
|
||||
TEST_E2E: ${{ needs.test-e2e.result }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ "$PRODUCT" != "true" ]]; then
|
||||
echo "No product-impacting paths changed; Product CI satisfied."
|
||||
exit 0
|
||||
fi
|
||||
failed=0
|
||||
for name in LINT CHECK BUILD TEST TEST_INTEGRATION TEST_E2E; do
|
||||
result="${!name}"
|
||||
printf '%s=%s\n' "$name" "$result"
|
||||
if [[ "$result" != "success" ]]; then
|
||||
if [[ "$result" == "skipped" ]]; then
|
||||
echo "$name was skipped although product paths changed." >&2
|
||||
failed=1
|
||||
elif [[ "$result" != "success" ]]; then
|
||||
failed=1
|
||||
fi
|
||||
done
|
||||
@ -246,9 +309,9 @@ jobs:
|
||||
|
||||
publish-mobile-update:
|
||||
name: Publish compatible mobile update
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.changes.outputs.product == 'true'
|
||||
continue-on-error: true
|
||||
needs: [lint, check, build, test, test-integration, test-e2e]
|
||||
needs: [changes, lint, check, build, test, test-integration, test-e2e]
|
||||
runs-on: [self-hosted, evoo-ci, Linux]
|
||||
timeout-minutes: 20
|
||||
concurrency:
|
||||
|
||||
69
.github/workflows/publish-server-image.yml
vendored
69
.github/workflows/publish-server-image.yml
vendored
@ -6,6 +6,17 @@ name: publish-server-image
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- ".dockerignore"
|
||||
- ".github/workflows/publish-server-image.yml"
|
||||
- "apps/**"
|
||||
- "infra/compose/**"
|
||||
- "infra/sandboxes/computer/**"
|
||||
- "infra/updater/**"
|
||||
- "packages/**"
|
||||
- "package.json"
|
||||
- "pnpm-lock.yaml"
|
||||
- "pnpm-workspace.yaml"
|
||||
tags: ["v*"]
|
||||
pull_request:
|
||||
paths:
|
||||
@ -31,10 +42,51 @@ env:
|
||||
DOCKER_BUILD_RECORD_UPLOAD: false
|
||||
|
||||
concurrency:
|
||||
group: publish-server-image-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
group: publish-server-image-${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
name: Detect image publish inputs
|
||||
runs-on: [self-hosted, evoo-ci, Linux]
|
||||
outputs:
|
||||
publish: ${{ steps.decide.outputs.publish }}
|
||||
steps:
|
||||
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
- uses: dorny/paths-filter@451258540fd6e81b87561f5d5adbd9863fc87cee # v3.0.2
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
publish:
|
||||
- '.dockerignore'
|
||||
- '.github/workflows/publish-server-image.yml'
|
||||
- 'apps/**'
|
||||
- 'infra/compose/**'
|
||||
- 'infra/sandboxes/computer/**'
|
||||
- 'infra/updater/**'
|
||||
- 'packages/**'
|
||||
- 'package.json'
|
||||
- 'pnpm-lock.yaml'
|
||||
- 'pnpm-workspace.yaml'
|
||||
- name: Decide whether release jobs should run
|
||||
id: decide
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]] || [[ "${{ github.ref }}" == refs/tags/v* ]]; then
|
||||
echo "publish=true" >> "$GITHUB_OUTPUT"
|
||||
elif [[ "${{ github.event_name }}" == "pull_request" ]]; then
|
||||
echo "publish=true" >> "$GITHUB_OUTPUT"
|
||||
elif [[ "${{ steps.filter.outputs.publish }}" == "true" ]]; then
|
||||
echo "publish=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "publish=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
installer-smoke:
|
||||
name: Compose installer smokes
|
||||
if: github.event_name == 'pull_request'
|
||||
@ -97,13 +149,17 @@ jobs:
|
||||
|
||||
build:
|
||||
name: Build ${{ matrix.name }} ${{ matrix.arch }}
|
||||
if: github.event_name != 'pull_request'
|
||||
needs: changes
|
||||
if: github.event_name != 'pull_request' && needs.changes.outputs.publish == 'true'
|
||||
# Each architecture builds natively on its own runner and pushes by digest only.
|
||||
# amd64 on evo Linux runners. arm64 is paused: the macOS runners have no Docker buildx,
|
||||
# and QEMU emulation took hours for the desktop-sized computer image.
|
||||
# See docs/self-host.md (Published images and tags).
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 90
|
||||
concurrency:
|
||||
group: publish-server-image-build-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref == 'refs/heads/main' }}
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
@ -174,10 +230,13 @@ jobs:
|
||||
|
||||
publish:
|
||||
name: Publish ${{ matrix.name }} image
|
||||
if: github.event_name != 'pull_request'
|
||||
needs: build
|
||||
if: github.event_name != 'pull_request' && needs.changes.outputs.publish == 'true'
|
||||
needs: [changes, build]
|
||||
runs-on: [self-hosted, evoo-ci, Linux]
|
||||
timeout-minutes: 15
|
||||
concurrency:
|
||||
group: publish-server-image-publish-${{ github.ref == 'refs/heads/main' && 'main' || github.ref }}
|
||||
cancel-in-progress: false
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
@ -22,6 +22,7 @@ describe("ci workflow merge gate", () => {
|
||||
it("aggregates only product jobs in Product CI", () => {
|
||||
const productGate = workflow.jobs["product-gate"];
|
||||
expect(productGate?.needs).toEqual([
|
||||
"changes",
|
||||
"lint",
|
||||
"check",
|
||||
"build",
|
||||
@ -29,6 +30,8 @@ describe("ci workflow merge gate", () => {
|
||||
"test-integration",
|
||||
"test-e2e",
|
||||
]);
|
||||
expect(workflowText).toContain("needs.changes.outputs.product == 'true'");
|
||||
expect(workflowText).toContain("No product-impacting paths changed; Product CI satisfied.");
|
||||
expect(productGate?.if).toBe("always()");
|
||||
});
|
||||
|
||||
|
||||
@ -35,7 +35,7 @@ describe("server image publish workflow", () => {
|
||||
|
||||
it("publishes one verified manifest per image after the build", () => {
|
||||
const publish = workflow.jobs.publish;
|
||||
expect(publish.needs).toBe("build");
|
||||
expect(publish.needs).toEqual(["changes", "build"]);
|
||||
expect(workflowText).toContain("push-by-digest=true");
|
||||
expect(workflowText).toContain("docker buildx imagetools create");
|
||||
expect(workflowText).toContain(`if [[ "\${#sources[@]}" -ne 1 ]]`);
|
||||
@ -75,8 +75,15 @@ describe("server image publish workflow", () => {
|
||||
const build = workflow.jobs.build;
|
||||
const publish = workflow.jobs.publish;
|
||||
expect(validate.if).toBe("github.event_name == 'pull_request'");
|
||||
expect(build.if).toBe("github.event_name != 'pull_request'");
|
||||
expect(publish.if).toBe("github.event_name != 'pull_request'");
|
||||
expect(build.if).toBe(
|
||||
"github.event_name != 'pull_request' && needs.changes.outputs.publish == 'true'",
|
||||
);
|
||||
expect(publish.if).toBe(
|
||||
"github.event_name != 'pull_request' && needs.changes.outputs.publish == 'true'",
|
||||
);
|
||||
expect(workflowText).toContain("cancel-in-progress: ${{ github.event_name == 'pull_request' }}");
|
||||
expect(workflowText).toContain("publish-server-image-build-${{ github.ref }}");
|
||||
expect(workflowText).toContain("publish-server-image-publish-${{ github.ref == 'refs/heads/main' && 'main' || github.ref }}");
|
||||
expect(workflowText).toContain("push: false");
|
||||
for (const match of workflowText.matchAll(/uses:\s+([^\s#]+)/g)) {
|
||||
expect(match[1], match[1]).toMatch(/@[0-9a-f]{40}$/);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user