#!/bin/sh set -eu script_dir="$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd)" project_dir="$(CDPATH='' cd -- "$script_dir/.." && pwd)" workflow="$project_dir/.github/workflows/release.yml" release_notes_config="$project_dir/.github/release.yml" pr_labels_workflow="$project_dir/.github/workflows/pr-release-labels.yml" reconcile_workflow="$project_dir/.github/workflows/reconcile-installer-r2.yml" prepare_job="$(sed -n '/^ prepare:/,/^ publish:/p' "$workflow")" publish_job="$(sed -n '/^ publish:/,/^ release-pr:/p' "$workflow")" release_pr_job="$(sed -n '/^ release-pr:/,$p' "$workflow")" require_in() { section="$1" expected="$2" if ! printf '%s\n' "$section" | grep -Fq "$expected"; then echo "Release workflow is missing: $expected" >&2 exit 1 fi } require_in "$(cat "$workflow")" ' group: vastora-installer-r2' require_in "$(cat "$workflow")" ' cancel-in-progress: false' require_in "$prepare_job" ' skip-github-pull-request: true' require_in "$prepare_job" ' pull-requests: write' require_in "$prepare_job" ' packages: read' require_in "$prepare_job" ' if release_sha="$(gh api "/repos/$GITHUB_REPOSITORY/commits/$RELEASE_TAG"' require_in "$prepare_job" " elif printf '%s' \"\$release_json\" | jq -e '.draft == true and (.assets | length) == 0' >/dev/null; then" require_in "$prepare_job" ' release_sha="$GITHUB_SHA"' require_in "$prepare_job" ' release_retry=image' require_in "$prepare_job" ' -f tag_name="$RELEASE_TAG" \' require_in "$prepare_job" ' -f target_commitish="$release_sha" >/dev/null' require_in "$prepare_job" ' center_image_digest: ${{ steps.retry.outputs.center_image_digest }}' require_in "$prepare_job" " release_retry: \${{ steps.retry.outputs.release_retry || 'false' }}" require_in "$publish_job" ' artifact-metadata: write' require_in "$publish_job" ' AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}' require_in "$publish_job" ' R2_BUCKET_NAME: ${{ vars.R2_BUCKET_NAME }}' require_in "$publish_job" ' scripts/create-installer-release-manifest.sh' require_in "$publish_job" ' scripts/github-installer-release-assets.sh' require_in "$publish_job" ' scripts/reconcile-installer-r2.sh' require_in "$publish_job" ' platforms: linux/amd64,linux/arm64' require_in "$publish_job" ' outputs: type=image,name=${{ env.CENTER_IMAGE }},push-by-digest=true,name-canonical=true,push=true' require_in "$publish_job" ' provenance: mode=max' require_in "$publish_job" ' cache-to: type=registry,ref=${{ env.CENTER_IMAGE }}:buildcache,mode=max,compression=zstd,compression-level=3,ignore-error=true' require_in "$publish_job" ' sbom: true' require_in "$publish_job" ' - name: Smoke-test released Center image' require_in "$publish_job" ' actual_version="$(docker run --rm --platform linux/amd64 --network none --read-only' require_in "$publish_job" ' "$CENTER_IMAGE@$IMAGE_DIGEST" version)"' require_in "$publish_job" ' test "$actual_version" = "$RELEASE_VERSION"' require_in "$publish_job" ' - name: Create durable installer release manifest' require_in "$publish_job" ' - name: Upload and verify draft GitHub installer assets' require_in "$publish_job" ' run: sh .release-tools/scripts/github-installer-release-assets.sh upload --tag "$RELEASE_TAG" --directory dist' require_in "$publish_job" ' - name: Recover verified installer assets for release retry' require_in "$publish_job" ' run: sh .release-tools/scripts/publish-installer-r2.sh stage --version "$RELEASE_VERSION" --bucket "$R2_BUCKET_NAME" --endpoint "$R2_ENDPOINT" --source-dir dist' require_in "$publish_job" ' run: sh .release-tools/scripts/reconcile-installer-r2.sh --bucket "$R2_BUCKET_NAME" --endpoint "$R2_ENDPOINT"' require_in "$publish_job" ' - name: Generate and publish GitHub release notes' require_in "$publish_job" ' RELEASE_SHA: ${{ needs.prepare.outputs.release_sha }}' require_in "$publish_job" ' gh api --method POST "/repos/$GITHUB_REPOSITORY/releases/generate-notes"' require_in "$publish_job" " -f configuration_file_path='.github/release.yml'" require_in "$publish_job" ' --title "Petauron Vastora $RELEASE_TAG"' require_in "$publish_job" ' --notes-file "$notes_file"' require_in "$publish_job" ' --draft=false' require_in "$release_pr_job" ' needs: [prepare, publish]' require_in "$release_pr_job" " if: always() && needs.prepare.result == 'success' && (needs.publish.result == 'success' || needs.publish.result == 'skipped')" require_in "$release_pr_job" ' skip-github-release: true' require_in "$release_pr_job" ' run: scripts/validate-release-metadata.sh "$BASE_SHA" .release-pr' require_in "$release_pr_job" ' path: .release-pr' require_in "$release_pr_job" ' run: echo "sha=$(git -C .release-pr rev-parse HEAD)" >> "$GITHUB_OUTPUT"' require_in "$release_pr_job" ' actions: write' require_in "$release_pr_job" ' persist-credentials: false' dispatch_step="$(printf '%s\n' "$release_pr_job" | sed -n '/^ - name: Dispatch required checks for the version PR$/,$p')" require_in "$dispatch_step" " if: steps.release_pr_action.outputs.prs_created == 'true'" require_in "$dispatch_step" ' RELEASE_BRANCH: ${{ steps.release_pr.outputs.head_branch }}' require_in "$dispatch_step" ' RELEASE_HEAD_SHA: ${{ steps.release_pr_head.outputs.sha }}' require_in "$dispatch_step" ' run: sh scripts/ensure-version-pr-checks.sh "$RELEASE_BRANCH" "$RELEASE_HEAD_SHA"' checks_helper="$(cat "$project_dir/scripts/ensure-version-pr-checks.sh")" require_in "$checks_helper" 'for workflow in ci.yml codeql.yml; do' require_in "$checks_helper" 'gh workflow run "$workflow" --repo "$repository" --ref "$branch"' require_in "$checks_helper" '.head_sha == $sha and .head_branch == $branch' require_in "$checks_helper" 'if [ -n "$run_id" ]; then' require_in "$checks_helper" 'Approve workflows to run' for required_workflow in ci.yml codeql.yml; do require_in "$(cat "$project_dir/.github/workflows/$required_workflow")" ' workflow_dispatch:' done if printf '%s\n' "$release_pr_job" "$checks_helper" | grep -Eq 'checks: write|/check-runs|CHECK_CONCLUSION|/(approve|rerun|cancel)(["[:space:]]|$)'; then echo 'Release PRs must dispatch real workflows, not manufacture required check results.' >&2 exit 1 fi metadata_line="$(printf '%s\n' "$release_pr_job" | grep -nF ' - name: Validate release pull request metadata' | head -n 1 | cut -d: -f1)" dispatch_line="$(printf '%s\n' "$release_pr_job" | grep -nF ' - name: Dispatch required checks for the version PR' | head -n 1 | cut -d: -f1)" if [ -z "$metadata_line" ] || [ -z "$dispatch_line" ] || [ "$dispatch_line" -le "$metadata_line" ]; then echo 'Version PR metadata must be validated before dispatching required workflows.' >&2 exit 1 fi require_fresh_release_step() { step_name="$1" step_block="$(printf '%s\n' "$publish_job" | sed -n "/^ - name: $step_name$/,/^ - name:/p")" require_in "$step_block" " if: needs.prepare.outputs.release_retry == 'false'" } for step in \ 'Log in to GitHub Container Registry' \ 'Build and push Center image' \ 'Scan released Center image for x64 vulnerabilities' \ 'Scan released Center image for ARM64 vulnerabilities' \ 'Smoke-test released Center image' \ 'Publish verified Center image tags' \ 'Attest Center image'; do require_fresh_release_step "$step" done for step in \ 'Set up Docker Buildx' \ 'Package release installer' \ 'Verify release assets and public image access' \ 'Create durable installer release manifest' \ 'Upload and verify draft GitHub installer assets'; do step_block="$(printf '%s\n' "$publish_job" | sed -n "/^ - name: $step$/,/^ - name:/p")" require_in "$step_block" " if: needs.prepare.outputs.release_retry != 'assets'" done retry_assets_step="$(printf '%s\n' "$publish_job" | sed -n '/^ - name: Recover verified installer assets for release retry$/,/^ - name:/p')" require_in "$retry_assets_step" " if: needs.prepare.outputs.release_retry == 'assets'" for step in \ 'Prune stale Vastora installer objects' \ 'Verify immutable installer release after pruning' \ 'Verify public installer endpoint after pruning'; do step_block="$(printf '%s\n' "$publish_job" | sed -n "/^ - name: $step$/,/^ - name:/p")" require_in "$step_block" "!contains(needs.prepare.outputs.release_version, '-alpha.')" done for step in \ 'Scan released Center image for x64 vulnerabilities' \ 'Scan released Center image for ARM64 vulnerabilities'; do step_block="$(printf '%s\n' "$publish_job" | sed -n "/^ - name: $step$/,/^ - name:/p")" require_in "$step_block" "if: needs.prepare.outputs.release_retry == 'false'" done if printf '%s\n' "$prepare_job" | grep -Fq 'skip-github-release: true'; then echo 'Release creation must not update the next release pull request.' >&2 exit 1 fi if printf '%s\n' "$publish_job" | grep -Fq ' tags:'; then echo 'Official image tags must be promoted by digest after the applicable Alpha or stable checks.' >&2 exit 1 fi line_of() { printf '%s\n' "$publish_job" | grep -nF "$1" | head -n 1 | cut -d: -f1 } scan_line="$(line_of 'Scan released Center image for ARM64 vulnerabilities')" smoke_line="$(line_of 'Smoke-test released Center image')" tags_line="$(line_of 'Publish verified Center image tags')" manifest_line="$(line_of 'Create durable installer release manifest')" github_assets_line="$(line_of 'Upload and verify draft GitHub installer assets')" stage_line="$(line_of 'Stage immutable installer assets in R2')" activate_line="$(line_of 'Atomically activate installer release in R2')" verify_line="$(line_of 'Verify immutable and public installer endpoints')" require_in "$publish_job" ' wait "$immutable_pid" || verification_failed=1' require_in "$publish_job" ' wait "$public_pid" || verification_failed=1' require_in "$publish_job" ' exit "$verification_failed"' prune_line="$(line_of 'Prune stale Vastora installer objects')" immutable_reverify_line="$(line_of 'Verify immutable installer release after pruning')" public_reverify_line="$(line_of 'Verify public installer endpoint after pruning')" publish_line="$(line_of 'Generate and publish GitHub release notes')" previous=0 for current in "$scan_line" "$smoke_line" "$tags_line" "$manifest_line" "$github_assets_line" "$stage_line" "$activate_line" "$verify_line" "$prune_line" "$immutable_reverify_line" "$public_reverify_line" "$publish_line"; do if [ -z "$current" ] || [ "$current" -le "$previous" ]; then echo 'Release workflow must enforce build -> smoke -> tags -> draft assets -> stage -> activate -> verify -> optional housekeeping -> publish.' >&2 exit 1 fi previous="$current" done release_notes_text="$(cat "$release_notes_config")" require_in "$release_notes_text" ' - title: 安全更新' require_in "$release_notes_text" ' - security' require_in "$release_notes_text" ' - title: 新功能' require_in "$release_notes_text" ' - enhancement' require_in "$release_notes_text" ' - title: 问题修复' require_in "$release_notes_text" ' - bug' require_in "$release_notes_text" ' - title: 维护与其他' require_in "$release_notes_text" ' - "*"' require_in "$release_notes_text" ' - "dependabot[bot]"' require_in "$release_notes_text" ' - "github-actions[bot]"' pr_labels_text="$(cat "$pr_labels_workflow")" require_in "$pr_labels_text" ' pull_request_target:' require_in "$pr_labels_text" ' issues: write' require_in "$pr_labels_text" ' PR_TITLE: ${{ github.event.pull_request.title }}' require_in "$pr_labels_text" " release_label='security'" require_in "$pr_labels_text" " release_label='enhancement'" require_in "$pr_labels_text" " release_label='bug'" if printf '%s\n' "$pr_labels_text" | grep -Fq 'actions/checkout'; then echo 'The pull_request_target labeler must not check out pull request code.' >&2 exit 1 fi reconcile_text="$(cat "$reconcile_workflow")" require_in "$reconcile_text" ' - cron: "17 3 * * *"' require_in "$reconcile_text" ' group: vastora-installer-r2' require_in "$reconcile_text" ' cancel-in-progress: false' require_in "$reconcile_text" ' --version-out "$RUNNER_TEMP/vastora-active-version"' require_in "$reconcile_text" ' - name: Verify active installer endpoints' echo "Release workflow sequencing test passed"