From 3e1157a2afcc688410a9eed01025b5248c2aba6e Mon Sep 17 00:00:00 2001 From: tbphp Date: Wed, 19 Aug 2026 15:04:44 +0800 Subject: [PATCH] =?UTF-8?q?ci(release):=20=E4=BC=98=E5=8C=96=20V2=20?= =?UTF-8?q?=E6=9E=84=E5=BB=BA=E5=8F=91=E5=B8=83=E5=B9=B6=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 17 ++ .github/workflows/release.yml | 164 +++++++++++++++----- internal/webui/workflow_parallelism_test.go | 83 ++++++++++ internal/webui/workflow_test.go | 55 +++++-- 4 files changed, 272 insertions(+), 47 deletions(-) create mode 100644 internal/webui/workflow_parallelism_test.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9270b213..53f3f5fd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -80,6 +80,22 @@ jobs: - name: Run race-enabled tests run: go test -race -count=1 -timeout=15m . ./internal/... + race-cpa: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Set up Go + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version-file: go.mod + cache: true + cache-dependency-path: third_party/cpaembedded/go.sum + - name: Run embedded CPA bridge race-enabled tests working-directory: third_party/cpaembedded run: go test -race -count=1 -timeout=15m ./... @@ -90,6 +106,7 @@ jobs: timeout-minutes: 15 strategy: fail-fast: false + max-parallel: 2 matrix: include: - driver: mysql diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b818b0f1..859b4381 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -83,7 +83,7 @@ jobs: verify-and-build-web: needs: validate-tag runs-on: ubuntu-24.04 - timeout-minutes: 20 + timeout-minutes: 10 steps: - name: Checkout tag commit uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -93,6 +93,24 @@ jobs: - name: Validate and build web UI uses: ./.github/actions/web-ci + - name: Upload verified web dist + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: verified-web-dist + path: internal/webui/dist + if-no-files-found: error + retention-days: 14 + + static-checks: + needs: validate-tag + runs-on: ubuntu-24.04 + timeout-minutes: 20 + steps: + - name: Checkout tag commit + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Set up Go uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: @@ -127,14 +145,6 @@ jobs: - name: Check repository invariants run: git diff --check - - name: Upload verified web dist - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: verified-web-dist - path: internal/webui/dist - if-no-files-found: error - retention-days: 14 - race-tests: needs: validate-tag runs-on: ubuntu-24.04 @@ -154,6 +164,23 @@ jobs: - name: Run race-enabled tests run: go test -race -count=1 -timeout=15m . ./internal/... + race-cpa: + needs: validate-tag + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - name: Checkout tag commit + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Set up Go + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version-file: go.mod + cache: true + cache-dependency-path: third_party/cpaembedded/go.sum + - name: Run embedded CPA bridge race-enabled tests working-directory: third_party/cpaembedded run: go test -race -count=1 -timeout=15m ./... @@ -165,6 +192,7 @@ jobs: timeout-minutes: 15 strategy: fail-fast: false + max-parallel: 2 matrix: include: - driver: mysql @@ -220,6 +248,7 @@ jobs: timeout-minutes: 15 strategy: fail-fast: true + max-parallel: 5 matrix: include: - goos: linux @@ -274,8 +303,8 @@ jobs: if-no-files-found: error retention-days: 14 - package-checksums: - needs: build-binaries + package-metadata: + needs: validate-tag runs-on: ubuntu-24.04 timeout-minutes: 10 steps: @@ -290,15 +319,9 @@ jobs: go-version-file: go.mod cache: true - - name: Download all release binaries - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - pattern: binary-gpt-load-* - path: release - merge-multiple: true - - name: Generate release SBOM and license assets run: | + mkdir -p release cp LICENSE release/LICENSE cp THIRD_PARTY_NOTICES.md release/THIRD_PARTY_NOTICES.md cp LICENSES/Apache-2.0.txt release/Apache-2.0.txt @@ -307,6 +330,34 @@ jobs: go run github.com/CycloneDX/cyclonedx-gomod/cmd/cyclonedx-gomod@v1.10.0 \ mod -json -noserial -notimestamp -output release/bom.cdx.json . + - name: Upload release metadata + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: release-metadata + path: release + if-no-files-found: error + retention-days: 14 + + package-checksums: + needs: + - build-binaries + - package-metadata + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - name: Download all release binaries + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + pattern: binary-gpt-load-* + path: release + merge-multiple: true + + - name: Download release metadata + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: release-metadata + path: release + - name: Generate SHA256SUMS working-directory: release run: | @@ -339,6 +390,7 @@ jobs: timeout-minutes: 10 strategy: fail-fast: true + max-parallel: 5 matrix: include: - runner: ubuntu-24.04 @@ -385,9 +437,7 @@ jobs: exit $LASTEXITCODE docker-smoke: - needs: - - validate-tag - - verify-and-build-web + needs: validate-tag runs-on: ubuntu-24.04 timeout-minutes: 30 steps: @@ -408,7 +458,9 @@ jobs: needs: - validate-tag - verify-and-build-web + - static-checks - race-tests + - race-cpa - build-binaries - package-checksums - native-artifact-smoke @@ -974,6 +1026,7 @@ jobs: contents: read strategy: fail-fast: true + max-parallel: 5 matrix: include: - runner: ubuntu-24.04 @@ -1022,13 +1075,61 @@ jobs: RELEASE_SMOKE_CHECKSUM_FILE: ${{ github.workspace }}/draft-release/SHA256SUMS run: .github/scripts/release-native-smoke.ps1 + post-publish-image-smoke: + needs: + - validate-tag + - publish-images + runs-on: ubuntu-24.04 + timeout-minutes: 30 + permissions: + contents: read + packages: read + strategy: + fail-fast: false + max-parallel: 2 + matrix: + include: + - registry: ghcr + repository: ghcr.io/tbphp/gpt-load + suffix: ghcr + - registry: dockerhub + repository: tbphp/gpt-load + suffix: dockerhub + steps: + - name: Checkout tag commit + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Log in to GHCR for runtime smoke + if: ${{ matrix.registry == 'ghcr' }} + uses: docker/login-action@371161bbe7024a29a25c5e19bfcbc0804fe9ad2c # v4.5.2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Log in to Docker Hub for runtime smoke + if: ${{ matrix.registry == 'dockerhub' }} + uses: docker/login-action@371161bbe7024a29a25c5e19bfcbc0804fe9ad2c # v4.5.2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_READ_TOKEN }} + + - name: Run full runtime smoke for published exact image + shell: bash + env: + RELEASE_VERSION: ${{ github.ref_name }} + RELEASE_SMOKE_SOURCE_IMAGE: ${{ matrix.repository }}:${{ needs.validate-tag.outputs.image_exact }} + RELEASE_SMOKE_SUFFIX: ${{ github.run_id }}-${{ matrix.suffix }} + run: .github/scripts/release-docker-smoke.sh + post-publish-verify: needs: - validate-tag - publication-preflight - publish-github - publish-images - - post-publish-native-smoke runs-on: ubuntu-24.04 timeout-minutes: 45 permissions: @@ -1188,22 +1289,6 @@ jobs: done test "${ghcr_exact_digest}" = "${dockerhub_exact_digest}" - - name: Run full runtime smoke for both published exact images - shell: bash - env: - RELEASE_VERSION: ${{ github.ref_name }} - run: | - set -euo pipefail - for source_and_suffix in \ - "ghcr.io/tbphp/gpt-load:${{ needs.validate-tag.outputs.image_exact }}|ghcr" \ - "tbphp/gpt-load:${{ needs.validate-tag.outputs.image_exact }}|dockerhub"; do - source_image="${source_and_suffix%%|*}" - smoke_suffix="${source_and_suffix##*|}" - RELEASE_SMOKE_SOURCE_IMAGE="${source_image}" \ - RELEASE_SMOKE_SUFFIX="${GITHUB_RUN_ID}-${smoke_suffix}" \ - .github/scripts/release-docker-smoke.sh - done - reconcile-publication: if: ${{ always() }} needs: @@ -1211,6 +1296,7 @@ jobs: - publish-images - publish-github - post-publish-native-smoke + - post-publish-image-smoke - post-publish-verify runs-on: ubuntu-24.04 timeout-minutes: 10 @@ -1256,6 +1342,7 @@ jobs: IMAGE_WRITER_RESULT: ${{ needs.publish-images.result }} GITHUB_WRITER_RESULT: ${{ needs.publish-github.result }} NATIVE_POST_RESULT: ${{ needs.post-publish-native-smoke.result }} + IMAGE_SMOKE_RESULT: ${{ needs.post-publish-image-smoke.result }} VERIFY_POST_RESULT: ${{ needs.post-publish-verify.result }} shell: bash run: | @@ -1331,6 +1418,7 @@ jobs: printf -- '- image writer: `%s`\n' "${IMAGE_WRITER_RESULT}" printf -- '- GitHub writer: `%s`\n' "${GITHUB_WRITER_RESULT}" printf -- '- draft native smoke: `%s`\n' "${NATIVE_POST_RESULT}" + printf -- '- published image smoke: `%s`\n' "${IMAGE_SMOKE_RESULT}" printf -- '- draft and image verification: `%s`\n' "${VERIFY_POST_RESULT}" printf -- '- GitHub exact inventory: `%s`\n' "${github_inventory}" printf -- '- GHCR exact inventory: `%s`\n' "${ghcr_inventory}" diff --git a/internal/webui/workflow_parallelism_test.go b/internal/webui/workflow_parallelism_test.go new file mode 100644 index 00000000..cf6c136b --- /dev/null +++ b/internal/webui/workflow_parallelism_test.go @@ -0,0 +1,83 @@ +package webui + +import ( + "strings" + "testing" +) + +func TestReleaseWorkflowParallelizesIndependentBuildStages(t *testing.T) { + content := readRepositoryFile(t, ".github/workflows/release.yml") + webJob := workflowJobBlock(t, content, "verify-and-build-web") + staticJob := workflowJobBlock(t, content, "static-checks") + for _, command := range []string{"govulncheck", "go vet ./...", "go build -trimpath"} { + if strings.Contains(webJob, command) { + t.Fatalf("web artifact job still serializes %q:\n%s", command, webJob) + } + if !strings.Contains(staticJob, command) { + t.Fatalf("parallel static job does not contain %q:\n%s", command, staticJob) + } + } + + binaryJob := workflowJobBlock(t, content, "build-binaries") + for _, required := range []string{"verify-and-build-web", "max-parallel: 5"} { + if !strings.Contains(binaryJob, required) { + t.Fatalf("binary build job does not contain %q:\n%s", required, binaryJob) + } + } + + dockerSmoke := workflowJobBlock(t, content, "docker-smoke") + if !strings.Contains(dockerSmoke, "validate-tag") || strings.Contains(dockerSmoke, "verify-and-build-web") { + t.Fatalf("Docker smoke is not released directly after tag validation:\n%s", dockerSmoke) + } + + metadataJob := workflowJobBlock(t, content, "package-metadata") + for _, required := range []string{"validate-tag", "name: release-metadata", "bom.cdx.json"} { + if !strings.Contains(metadataJob, required) { + t.Fatalf("release metadata job does not contain %q:\n%s", required, metadataJob) + } + } + checksumJob := workflowJobBlock(t, content, "package-checksums") + for _, required := range []string{"build-binaries", "package-metadata", "name: release-metadata"} { + if !strings.Contains(checksumJob, required) { + t.Fatalf("checksum job does not contain %q:\n%s", required, checksumJob) + } + } + + preflight := workflowJobBlock(t, content, "publication-preflight") + for _, dependency := range []string{"static-checks", "race-cpa"} { + if !strings.Contains(preflight, dependency) { + t.Fatalf("publication preflight does not need %s:\n%s", dependency, preflight) + } + } +} + +func TestReleaseWorkflowRunsPublishedImageSmokesInParallel(t *testing.T) { + content := readRepositoryFile(t, ".github/workflows/release.yml") + smokeJob := workflowJobBlock(t, content, "post-publish-image-smoke") + for _, required := range []string{ + "max-parallel: 2", + "ghcr.io/tbphp/gpt-load", + "tbphp/gpt-load", + "RELEASE_SMOKE_SOURCE_IMAGE", + ".github/scripts/release-docker-smoke.sh", + } { + if !strings.Contains(smokeJob, required) { + t.Fatalf("published image smoke job does not contain %q:\n%s", required, smokeJob) + } + } + + verifyJob := workflowJobBlock(t, content, "post-publish-verify") + if strings.Contains(verifyJob, "post-publish-image-smoke") || + strings.Contains(verifyJob, "post-publish-native-smoke") { + t.Fatalf("independent post-publication gates are still serialized:\n%s", verifyJob) + } + if strings.Contains(verifyJob, "for source_and_suffix in") { + t.Fatalf("post-publication verification still runs image smokes serially:\n%s", verifyJob) + } + reconcileJob := workflowJobBlock(t, content, "reconcile-publication") + for _, dependency := range []string{"post-publish-native-smoke", "post-publish-image-smoke", "post-publish-verify"} { + if !strings.Contains(reconcileJob, dependency) { + t.Fatalf("publication reconciliation does not need %s:\n%s", dependency, reconcileJob) + } + } +} diff --git a/internal/webui/workflow_test.go b/internal/webui/workflow_test.go index 2d5343ee..8e7a7dae 100644 --- a/internal/webui/workflow_test.go +++ b/internal/webui/workflow_test.go @@ -98,14 +98,27 @@ func TestBranchAndReleaseWorkflowsRunRaceInParallelGates(t *testing.T) { "Run race-enabled tests", "go test -race -count=1 -timeout=15m . ./internal/...", ) + branchCPA := workflowStepBlock( + t, + workflowJobBlock(t, content, "race-cpa"), + "Run embedded CPA bridge race-enabled tests", + ) + for _, required := range []string{ + "working-directory: third_party/cpaembedded", + "run: go test -race -count=1 -timeout=15m ./...", + } { + if !strings.Contains(branchCPA, required) { + t.Fatalf("branch CPA race gate does not contain %q:\n%s", required, branchCPA) + } + } releaseContent := readRepositoryFile(t, ".github/workflows/release.yml") - releaseVerifyJob := workflowJobBlock( + releaseStaticJob := workflowJobBlock( t, releaseContent, - "verify-and-build-web", + "static-checks", ) - if strings.Contains(releaseVerifyJob, "go test -race") { + if strings.Contains(releaseStaticJob, "go test -race") { t.Fatal("release static job still runs race tests serially") } assertWorkflowGateStep( @@ -114,6 +127,19 @@ func TestBranchAndReleaseWorkflowsRunRaceInParallelGates(t *testing.T) { "Run race-enabled tests", "go test -race -count=1 -timeout=15m . ./internal/...", ) + releaseCPA := workflowStepBlock( + t, + workflowJobBlock(t, releaseContent, "race-cpa"), + "Run embedded CPA bridge race-enabled tests", + ) + for _, required := range []string{ + "working-directory: third_party/cpaembedded", + "run: go test -race -count=1 -timeout=15m ./...", + } { + if !strings.Contains(releaseCPA, required) { + t.Fatalf("release CPA race gate does not contain %q:\n%s", required, releaseCPA) + } + } } func TestWindowsCIExecutesManagedStorageACLTests(t *testing.T) { @@ -203,7 +229,7 @@ func TestGoFormattingScriptsFailClosed(t *testing.T) { job string }{ {name: "branch", file: ".github/workflows/ci.yml", job: "test"}, - {name: "release", file: ".github/workflows/release.yml", job: "verify-and-build-web"}, + {name: "release", file: ".github/workflows/release.yml", job: "static-checks"}, } { t.Run(workflow.name, func(t *testing.T) { script := workflowGoFormattingScript( @@ -519,7 +545,7 @@ func TestReleasePublicationStateClassifiesFreshConsistentConflictAndPartial(t *t func TestReleaseWorkflowDoesNotRequireUntrackedAgentInstructionFiles(t *testing.T) { content := readRepositoryFile(t, ".github/workflows/release.yml") - verifyJob := workflowJobBlock(t, content, "verify-and-build-web") + verifyJob := workflowJobBlock(t, content, "static-checks") if !strings.Contains(verifyJob, "git diff --check") { t.Fatal("release verification does not run git diff --check") } @@ -572,7 +598,9 @@ func TestReleaseWorkflowGatesSingleReleaseWriterAndImagePublication(t *testing.T for _, dependency := range []string{ "validate-tag", "verify-and-build-web", + "static-checks", "race-tests", + "race-cpa", "build-binaries", "package-checksums", "native-artifact-smoke", @@ -1518,10 +1546,6 @@ func TestReleaseWorkflowRunsBothPublishedImagesAndPreservesLatest(t *testing.T) postPublish := workflowJobBlock(t, content, "post-publish-verify") for _, required := range []string{ "publication-preflight", - "RELEASE_SMOKE_SOURCE_IMAGE", - `ghcr.io/tbphp/gpt-load:${{ needs.validate-tag.outputs.image_exact }}`, - `tbphp/gpt-load:${{ needs.validate-tag.outputs.image_exact }}`, - ".github/scripts/release-docker-smoke.sh", ".github/scripts/release-image-digest.sh", "ghcr_latest_digest", "dockerhub_latest_digest", @@ -1530,9 +1554,22 @@ func TestReleaseWorkflowRunsBothPublishedImagesAndPreservesLatest(t *testing.T) t.Fatalf("published image runtime verification does not contain %q:\n%s", required, postPublish) } } + runtimeSmoke := workflowJobBlock(t, content, "post-publish-image-smoke") + for _, required := range []string{ + "RELEASE_SMOKE_SOURCE_IMAGE", + "ghcr.io/tbphp/gpt-load", + "tbphp/gpt-load", + "needs.validate-tag.outputs.image_exact", + ".github/scripts/release-docker-smoke.sh", + } { + if !strings.Contains(runtimeSmoke, required) { + t.Fatalf("published image runtime smoke does not contain %q:\n%s", required, runtimeSmoke) + } + } for name, block := range map[string]string{ "publication-preflight": snapshotJob, "post-publish-verify": postPublish, + "published-image-smoke": runtimeSmoke, } { for _, required := range []string{ "Log in to Docker Hub",