Merge pull request #170 from cynarAI/cursor/ci-queue-reduction-197c
ci: reduce self-hosted queue depth (concurrency + path filters)
This commit is contained in:
commit
b8781a0081
7
.github/workflows/autopilot-auto-merge.yml
vendored
7
.github/workflows/autopilot-auto-merge.yml
vendored
@ -21,7 +21,7 @@ concurrency:
|
|||||||
jobs:
|
jobs:
|
||||||
auto-merge:
|
auto-merge:
|
||||||
name: Autopilot auto-merge
|
name: Autopilot auto-merge
|
||||||
runs-on: [self-hosted, evoo-ci, Linux]
|
runs-on: [self-hosted, evoo-ci]
|
||||||
steps:
|
steps:
|
||||||
- name: Resolve pull request
|
- name: Resolve pull request
|
||||||
id: pr
|
id: pr
|
||||||
@ -71,7 +71,10 @@ jobs:
|
|||||||
HEAD_SHA: ${{ steps.pr.outputs.head_sha }}
|
HEAD_SHA: ${{ steps.pr.outputs.head_sha }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
mapfile -t conclusions < <(
|
conclusions=()
|
||||||
|
while IFS= read -r conclusion; do
|
||||||
|
[[ -n "$conclusion" ]] && conclusions+=("$conclusion")
|
||||||
|
done < <(
|
||||||
gh api "repos/${GITHUB_REPOSITORY}/commits/${HEAD_SHA}/check-runs" --paginate \
|
gh api "repos/${GITHUB_REPOSITORY}/commits/${HEAD_SHA}/check-runs" --paginate \
|
||||||
--jq '.check_runs[] | select(.name == "Product CI") | .conclusion'
|
--jq '.check_runs[] | select(.name == "Product CI") | .conclusion'
|
||||||
)
|
)
|
||||||
|
|||||||
82
.github/workflows/ci.yml
vendored
82
.github/workflows/ci.yml
vendored
@ -5,19 +5,69 @@ name: ci
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
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:
|
pull_request:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
pull-requests: read
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ci-${{ github.event.pull_request.number || github.ref }}
|
group: ci-${{ github.event.pull_request.number || github.ref }}
|
||||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||||
|
|
||||||
|
# Jobs that only need Node/pnpm/bash use [self-hosted, evoo-ci] so macOS runners (air, mini)
|
||||||
|
# can share load. Linux-only work keeps the Linux label (Docker, xvfb, local Postgres :5433,
|
||||||
|
# Homebrew ffmpeg with libass/subtitles filter for media unit tests).
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
changes:
|
||||||
|
name: Detect product changes
|
||||||
|
runs-on: [self-hosted, evoo-ci]
|
||||||
|
outputs:
|
||||||
|
product: ${{ steps.filter.outputs.product }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v5
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
fetch-depth: 0
|
||||||
|
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # 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:
|
lint:
|
||||||
name: Lint
|
name: Lint
|
||||||
runs-on: [self-hosted, evoo-ci, Linux]
|
needs: changes
|
||||||
|
if: needs.changes.outputs.product == 'true'
|
||||||
|
runs-on: [self-hosted, evoo-ci]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
@ -51,7 +101,9 @@ jobs:
|
|||||||
|
|
||||||
check:
|
check:
|
||||||
name: Typecheck
|
name: Typecheck
|
||||||
runs-on: [self-hosted, evoo-ci, Linux]
|
needs: changes
|
||||||
|
if: needs.changes.outputs.product == 'true'
|
||||||
|
runs-on: [self-hosted, evoo-ci]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
@ -88,6 +140,8 @@ jobs:
|
|||||||
|
|
||||||
build:
|
build:
|
||||||
name: Production builds
|
name: Production builds
|
||||||
|
needs: changes
|
||||||
|
if: needs.changes.outputs.product == 'true'
|
||||||
runs-on: [self-hosted, evoo-ci, Linux]
|
runs-on: [self-hosted, evoo-ci, Linux]
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
steps:
|
steps:
|
||||||
@ -138,6 +192,8 @@ jobs:
|
|||||||
|
|
||||||
test:
|
test:
|
||||||
name: Unit tests
|
name: Unit tests
|
||||||
|
needs: changes
|
||||||
|
if: needs.changes.outputs.product == 'true'
|
||||||
runs-on: [self-hosted, evoo-ci, Linux]
|
runs-on: [self-hosted, evoo-ci, Linux]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v5
|
||||||
@ -175,6 +231,8 @@ jobs:
|
|||||||
|
|
||||||
test-integration:
|
test-integration:
|
||||||
name: Postgres journeys
|
name: Postgres journeys
|
||||||
|
needs: changes
|
||||||
|
if: needs.changes.outputs.product == 'true'
|
||||||
runs-on: [self-hosted, evoo-ci, Linux]
|
runs-on: [self-hosted, evoo-ci, Linux]
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
steps:
|
steps:
|
||||||
@ -210,6 +268,8 @@ jobs:
|
|||||||
|
|
||||||
test-e2e:
|
test-e2e:
|
||||||
name: Web E2E
|
name: Web E2E
|
||||||
|
needs: changes
|
||||||
|
if: needs.changes.outputs.product == 'true'
|
||||||
uses: ./.github/workflows/playwright.yml
|
uses: ./.github/workflows/playwright.yml
|
||||||
with:
|
with:
|
||||||
upload_artifacts: true
|
upload_artifacts: true
|
||||||
@ -217,11 +277,12 @@ jobs:
|
|||||||
product-gate:
|
product-gate:
|
||||||
name: Product CI
|
name: Product CI
|
||||||
if: always()
|
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]
|
runs-on: [self-hosted, evoo-ci]
|
||||||
steps:
|
steps:
|
||||||
- name: Verify product checks
|
- name: Verify product checks
|
||||||
env:
|
env:
|
||||||
|
PRODUCT: ${{ needs.changes.outputs.product }}
|
||||||
LINT: ${{ needs.lint.result }}
|
LINT: ${{ needs.lint.result }}
|
||||||
CHECK: ${{ needs.check.result }}
|
CHECK: ${{ needs.check.result }}
|
||||||
BUILD: ${{ needs.build.result }}
|
BUILD: ${{ needs.build.result }}
|
||||||
@ -230,11 +291,18 @@ jobs:
|
|||||||
TEST_E2E: ${{ needs.test-e2e.result }}
|
TEST_E2E: ${{ needs.test-e2e.result }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
if [[ "$PRODUCT" != "true" ]]; then
|
||||||
|
echo "No product-impacting paths changed; Product CI satisfied."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
failed=0
|
failed=0
|
||||||
for name in LINT CHECK BUILD TEST TEST_INTEGRATION TEST_E2E; do
|
for name in LINT CHECK BUILD TEST TEST_INTEGRATION TEST_E2E; do
|
||||||
result="${!name}"
|
result="${!name}"
|
||||||
printf '%s=%s\n' "$name" "$result"
|
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
|
failed=1
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
@ -246,9 +314,9 @@ jobs:
|
|||||||
|
|
||||||
publish-mobile-update:
|
publish-mobile-update:
|
||||||
name: Publish compatible 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
|
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]
|
runs-on: [self-hosted, evoo-ci, Linux]
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
concurrency:
|
concurrency:
|
||||||
|
|||||||
96
.github/workflows/publish-server-image.yml
vendored
96
.github/workflows/publish-server-image.yml
vendored
@ -3,9 +3,25 @@ name: publish-server-image
|
|||||||
# Release tags publish the source-addressed image used by in-app updates plus the human-readable
|
# Release tags publish the source-addressed image used by in-app updates plus the human-readable
|
||||||
# semver tags. Main publishes `edge`. Pull requests build with a read-only token and an isolated
|
# semver tags. Main publishes `edge`. Pull requests build with a read-only token and an isolated
|
||||||
# cache, so untrusted code can validate the images without receiving package-publish authority.
|
# cache, so untrusted code can validate the images without receiving package-publish authority.
|
||||||
|
# Self-hosted runners on one host often share ~/.docker; build/publish set DOCKER_CONFIG under
|
||||||
|
# RUNNER_TEMP so docker/login-action post-logout cannot evict a concurrent job's GHCR session.
|
||||||
|
# `ci-<run>-<image>-<arch>` handoff tags are extra aliases on the per-arch digest; GHCR does not
|
||||||
|
# expose a safe tag-only delete API (DELETE /v2/.../manifests/<tag> is unsupported or digest-scoped),
|
||||||
|
# so they are left in place and do not affect edge/sha-* manifests assembled in publish.
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
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*"]
|
tags: ["v*"]
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
@ -31,10 +47,51 @@ env:
|
|||||||
DOCKER_BUILD_RECORD_UPLOAD: false
|
DOCKER_BUILD_RECORD_UPLOAD: false
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: publish-server-image-${{ github.ref }}
|
group: publish-server-image-${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || github.ref }}
|
||||||
cancel-in-progress: false
|
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
changes:
|
||||||
|
name: Detect image publish inputs
|
||||||
|
runs-on: [self-hosted, evoo-ci]
|
||||||
|
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@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # 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:
|
installer-smoke:
|
||||||
name: Compose installer smokes
|
name: Compose installer smokes
|
||||||
if: github.event_name == 'pull_request'
|
if: github.event_name == 'pull_request'
|
||||||
@ -97,13 +154,17 @@ jobs:
|
|||||||
|
|
||||||
build:
|
build:
|
||||||
name: Build ${{ matrix.name }} ${{ matrix.arch }}
|
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.
|
# 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,
|
# 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.
|
# and QEMU emulation took hours for the desktop-sized computer image.
|
||||||
# See docs/self-host.md (Published images and tags).
|
# See docs/self-host.md (Published images and tags).
|
||||||
runs-on: ${{ matrix.runner }}
|
runs-on: ${{ matrix.runner }}
|
||||||
timeout-minutes: 90
|
timeout-minutes: 90
|
||||||
|
concurrency:
|
||||||
|
group: publish-server-image-build-${{ github.ref }}
|
||||||
|
cancel-in-progress: ${{ github.ref == 'refs/heads/main' }}
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
packages: write
|
packages: write
|
||||||
@ -130,6 +191,12 @@ jobs:
|
|||||||
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
|
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
- name: Prepare isolated Docker config directory
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
cfg="${RUNNER_TEMP}/docker-config"
|
||||||
|
mkdir -p "$cfg"
|
||||||
|
echo "DOCKER_CONFIG=${cfg}" >> "$GITHUB_ENV"
|
||||||
- uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
- uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||||
- name: Log in to GHCR
|
- name: Log in to GHCR
|
||||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||||
@ -174,10 +241,13 @@ jobs:
|
|||||||
|
|
||||||
publish:
|
publish:
|
||||||
name: Publish ${{ matrix.name }} image
|
name: Publish ${{ matrix.name }} image
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request' && needs.changes.outputs.publish == 'true'
|
||||||
needs: build
|
needs: [changes, build]
|
||||||
runs-on: [self-hosted, evoo-ci, Linux]
|
runs-on: [self-hosted, evoo-ci, Linux]
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
|
concurrency:
|
||||||
|
group: publish-server-image-publish-${{ github.ref == 'refs/heads/main' && 'main' || github.ref }}
|
||||||
|
cancel-in-progress: false
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
packages: write
|
packages: write
|
||||||
@ -191,6 +261,12 @@ jobs:
|
|||||||
- name: updater
|
- name: updater
|
||||||
- name: computer
|
- name: computer
|
||||||
steps:
|
steps:
|
||||||
|
- name: Prepare isolated Docker config directory
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
cfg="${RUNNER_TEMP}/docker-config"
|
||||||
|
mkdir -p "$cfg"
|
||||||
|
echo "DOCKER_CONFIG=${cfg}" >> "$GITHUB_ENV"
|
||||||
- uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
- uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||||
- name: Log in to GHCR
|
- name: Log in to GHCR
|
||||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||||
@ -280,13 +356,3 @@ jobs:
|
|||||||
subject-name: ghcr.io/${{ steps.ns.outputs.repo }}/${{ matrix.name }}
|
subject-name: ghcr.io/${{ steps.ns.outputs.repo }}/${{ matrix.name }}
|
||||||
subject-digest: ${{ steps.manifest.outputs.digest }}
|
subject-digest: ${{ steps.manifest.outputs.digest }}
|
||||||
push-to-registry: true
|
push-to-registry: true
|
||||||
- name: Remove CI handoff tags
|
|
||||||
continue-on-error: true
|
|
||||||
env:
|
|
||||||
IMAGE: ghcr.io/${{ steps.ns.outputs.repo }}/${{ matrix.name }}
|
|
||||||
IMAGE_NAME: ${{ matrix.name }}
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
for arch in amd64; do
|
|
||||||
docker buildx imagetools rm "${IMAGE}:ci-${GITHUB_RUN_ID}-${IMAGE_NAME}-${arch}"
|
|
||||||
done
|
|
||||||
|
|||||||
@ -91,7 +91,8 @@ function run(overrides: Partial<RunActivityRow> = {}): RunActivityRow {
|
|||||||
describe("isToday", () => {
|
describe("isToday", () => {
|
||||||
it("matches same calendar day", () => {
|
it("matches same calendar day", () => {
|
||||||
expect(isToday("2026-09-22T08:30:00.000Z", now)).toBe(true);
|
expect(isToday("2026-09-22T08:30:00.000Z", now)).toBe(true);
|
||||||
expect(isToday("2026-09-21T23:59:00.000Z", now)).toBe(false);
|
// Midday UTC on the previous day stays the previous calendar day in UTC+14…UTC-12.
|
||||||
|
expect(isToday("2026-09-21T12:00:00.000Z", now)).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -22,6 +22,7 @@ describe("ci workflow merge gate", () => {
|
|||||||
it("aggregates only product jobs in Product CI", () => {
|
it("aggregates only product jobs in Product CI", () => {
|
||||||
const productGate = workflow.jobs["product-gate"];
|
const productGate = workflow.jobs["product-gate"];
|
||||||
expect(productGate?.needs).toEqual([
|
expect(productGate?.needs).toEqual([
|
||||||
|
"changes",
|
||||||
"lint",
|
"lint",
|
||||||
"check",
|
"check",
|
||||||
"build",
|
"build",
|
||||||
@ -29,6 +30,8 @@ describe("ci workflow merge gate", () => {
|
|||||||
"test-integration",
|
"test-integration",
|
||||||
"test-e2e",
|
"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()");
|
expect(productGate?.if).toBe("always()");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -35,7 +35,7 @@ describe("server image publish workflow", () => {
|
|||||||
|
|
||||||
it("publishes one verified manifest per image after the build", () => {
|
it("publishes one verified manifest per image after the build", () => {
|
||||||
const publish = workflow.jobs.publish;
|
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("push-by-digest=true");
|
||||||
expect(workflowText).toContain("docker buildx imagetools create");
|
expect(workflowText).toContain("docker buildx imagetools create");
|
||||||
expect(workflowText).toContain(`if [[ "\${#sources[@]}" -ne 1 ]]`);
|
expect(workflowText).toContain(`if [[ "\${#sources[@]}" -ne 1 ]]`);
|
||||||
@ -62,6 +62,14 @@ describe("server image publish workflow", () => {
|
|||||||
expect(workflowText).toContain("ci-${GITHUB_RUN_ID}-");
|
expect(workflowText).toContain("ci-${GITHUB_RUN_ID}-");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("isolates Docker registry auth per job and does not delete handoff tags via the registry API", () => {
|
||||||
|
expect(workflowText).toContain('echo "DOCKER_CONFIG=${cfg}" >> "$GITHUB_ENV"');
|
||||||
|
expect(workflowText).toContain("Prepare isolated Docker config directory");
|
||||||
|
expect(workflowText).not.toContain("imagetools rm");
|
||||||
|
expect(workflowText).not.toContain("Delete CI handoff tags");
|
||||||
|
expect(workflowText).toContain("safe tag-only delete API");
|
||||||
|
});
|
||||||
|
|
||||||
it("names every image under the lowercase repository namespace", () => {
|
it("names every image under the lowercase repository namespace", () => {
|
||||||
expect(workflowText).not.toContain(`ghcr.io/\${{ github.repository }}`);
|
expect(workflowText).not.toContain(`ghcr.io/\${{ github.repository }}`);
|
||||||
const references =
|
const references =
|
||||||
@ -75,8 +83,19 @@ describe("server image publish workflow", () => {
|
|||||||
const build = workflow.jobs.build;
|
const build = workflow.jobs.build;
|
||||||
const publish = workflow.jobs.publish;
|
const publish = workflow.jobs.publish;
|
||||||
expect(validate.if).toBe("github.event_name == 'pull_request'");
|
expect(validate.if).toBe("github.event_name == 'pull_request'");
|
||||||
expect(build.if).toBe("github.event_name != 'pull_request'");
|
expect(build.if).toBe(
|
||||||
expect(publish.if).toBe("github.event_name != 'pull_request'");
|
"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");
|
expect(workflowText).toContain("push: false");
|
||||||
for (const match of workflowText.matchAll(/uses:\s+([^\s#]+)/g)) {
|
for (const match of workflowText.matchAll(/uses:\s+([^\s#]+)/g)) {
|
||||||
expect(match[1], match[1]).toMatch(/@[0-9a-f]{40}$/);
|
expect(match[1], match[1]).toMatch(/@[0-9a-f]{40}$/);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user