Plan Rancher Sign-Off repo targets #585
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Plan Rancher Sign-Off | |
| run-name: ${{ github.event.inputs.rancher_version && format('Plan Rancher Sign-Off {0}', github.event.inputs.rancher_version) || 'Plan Rancher Sign-Off repo targets' }} | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| rancher_version: | |
| description: Optional Rancher alpha or RC, for example v2.15.0-rc2. Leave blank to use signoff-targets.json. | |
| required: false | |
| type: string | |
| previous_rancher_version: | |
| description: Previous Rancher release. Leave blank to resolve automatically. | |
| required: false | |
| type: string | |
| webhook_image: | |
| description: Candidate webhook image. Leave blank to use Docker Hub image from target build.yaml. | |
| required: false | |
| type: string | |
| signing_policy: | |
| description: Signing policy to apply. | |
| required: false | |
| default: auto | |
| type: choice | |
| options: | |
| - auto | |
| - required | |
| - report-only | |
| - skip | |
| dispatch_runs: | |
| description: Dispatch uncovered sign-off lanes after planning. | |
| required: true | |
| default: true | |
| type: boolean | |
| max_parallel_lanes: | |
| description: Maximum active sign-off lane runners to allow. | |
| required: false | |
| default: 4 | |
| type: number | |
| rerun_successful_lanes: | |
| description: Dispatch lanes again even if a matching run already succeeded on this branch. | |
| required: true | |
| default: false | |
| type: boolean | |
| rke2_server_count: | |
| description: RKE2 server nodes per HA for dispatched lane runners. | |
| required: true | |
| default: "3" | |
| type: choice | |
| options: | |
| - "1" | |
| - "3" | |
| - "5" | |
| ignore_active_runner_id: | |
| description: Active Run Rancher Sign-Off Lane run id to ignore when this planner was woken by that runner. | |
| required: false | |
| type: string | |
| permissions: | |
| actions: write | |
| contents: read | |
| concurrency: | |
| group: signoff-plan | |
| cancel-in-progress: false | |
| jobs: | |
| plan: | |
| runs-on: ubuntu-latest | |
| environment: rancher-signoff | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| AWS_PREFIX: ${{ vars.AWS_PREFIX }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | |
| with: | |
| persist-credentials: false | |
| - name: Set up Go | |
| uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 | |
| with: | |
| go-version-file: go.mod | |
| - name: Build automation tools | |
| run: | | |
| go build -o "$RUNNER_TEMP/signoff-plan" ./automation/signoff-plan | |
| - name: Generate sign-off plan | |
| env: | |
| INPUT_RANCHER_VERSION: ${{ github.event.inputs.rancher_version || '' }} | |
| INPUT_PREVIOUS_RANCHER_VERSION: ${{ github.event.inputs.previous_rancher_version || '' }} | |
| INPUT_WEBHOOK_IMAGE: ${{ github.event.inputs.webhook_image || '' }} | |
| INPUT_SIGNING_POLICY: ${{ github.event.inputs.signing_policy || 'auto' }} | |
| run: | | |
| set -euo pipefail | |
| args=() | |
| if [ -n "$INPUT_RANCHER_VERSION" ]; then | |
| args+=("-rancher-version" "$INPUT_RANCHER_VERSION") | |
| else | |
| args+=("-targets" "signoff-targets.json") | |
| fi | |
| if [ -n "$INPUT_PREVIOUS_RANCHER_VERSION" ]; then | |
| args+=("-previous-rancher-version" "$INPUT_PREVIOUS_RANCHER_VERSION") | |
| fi | |
| if [ -n "$INPUT_WEBHOOK_IMAGE" ]; then | |
| args+=("-webhook-image" "$INPUT_WEBHOOK_IMAGE") | |
| fi | |
| args+=("-signing-policy" "$INPUT_SIGNING_POLICY") | |
| args+=("-run-id" "${{ github.run_id }}") | |
| args+=("-ignore-ledger") | |
| args+=("-output" "signoff-plan.json") | |
| if [ -n "${AWS_PREFIX:-}" ]; then | |
| args+=("-aws-base-prefix" "$AWS_PREFIX") | |
| fi | |
| "$RUNNER_TEMP/signoff-plan" "${args[@]}" | |
| - name: Build dispatch queue | |
| id: dispatch-queue | |
| env: | |
| DISPATCH_REQUESTED: ${{ github.event.inputs.dispatch_runs == 'true' }} | |
| MAX_PARALLEL_LANES: ${{ github.event.inputs.max_parallel_lanes || '4' }} | |
| RERUN_SUCCESSFUL_LANES: ${{ github.event.inputs.rerun_successful_lanes == 'true' }} | |
| RKE2_SERVER_COUNT: ${{ github.event.inputs.rke2_server_count || '3' }} | |
| IGNORE_ACTIVE_RUNNER_ID: ${{ github.event.inputs.ignore_active_runner_id || '' }} | |
| REF_NAME: ${{ github.ref_name }} | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| set -euo pipefail | |
| mkdir -p automation-output | |
| jq -c ' | |
| def planned: | |
| if has("plans") then .plans[] else . end; | |
| [planned as $plan | |
| | ($plan.lanes // [])[] | |
| | { | |
| rancher_version: $plan.target_version, | |
| lane: .name, | |
| previous_rancher_version: ($plan.previous_version // ""), | |
| webhook_image: ($plan.webhook_image // ""), | |
| signing_policy: ($plan.signing_policy_input // "auto"), | |
| rke2_server_count: env.RKE2_SERVER_COUNT | |
| }] | |
| ' signoff-plan.json > automation-output/signoff-dispatch-queue-raw.json | |
| gh run list \ | |
| --workflow run-rancher-signoff-lane.yml \ | |
| --limit 200 \ | |
| --json databaseId,status,conclusion,displayTitle,event,headBranch,createdAt,url \ | |
| > automation-output/signoff-lane-runs.json | |
| jq ' | |
| [.[] | select((.databaseId | tostring) != env.IGNORE_ACTIVE_RUNNER_ID) | select(.status == "queued" or .status == "in_progress" or .status == "pending" or .status == "requested" or .status == "waiting")] | |
| ' automation-output/signoff-lane-runs.json \ | |
| > automation-output/signoff-active-runners.json | |
| jq ' | |
| [.[] | select(.event == "workflow_dispatch") | select(.headBranch == env.REF_NAME) | select(.status == "completed" and .conclusion == "success")] | |
| ' automation-output/signoff-lane-runs.json \ | |
| > automation-output/signoff-successful-runners.json | |
| jq -n \ | |
| --slurpfile queue automation-output/signoff-dispatch-queue-raw.json \ | |
| --slurpfile active automation-output/signoff-active-runners.json \ | |
| --slurpfile successful automation-output/signoff-successful-runners.json \ | |
| --argjson rerun_successful "$RERUN_SUCCESSFUL_LANES" \ | |
| ' | |
| def title($lane): "Run \($lane.rancher_version) / \($lane.lane)"; | |
| ($active[0] | map(.displayTitle) | unique) as $active_titles | |
| | ($successful[0] | map(.displayTitle) | unique) as $successful_titles | |
| | [$queue[0][] | |
| | title(.) as $display_title | |
| | select(($active_titles | index($display_title)) | not) | |
| | select($rerun_successful or (($successful_titles | index($display_title)) | not))] | |
| ' > automation-output/signoff-dispatch-queue.json | |
| raw_queue_count="$(jq 'length' automation-output/signoff-dispatch-queue-raw.json)" | |
| active_blocked_count="$(jq -n --slurpfile queue automation-output/signoff-dispatch-queue-raw.json --slurpfile active automation-output/signoff-active-runners.json ' | |
| def title($lane): "Run \($lane.rancher_version) / \($lane.lane)"; | |
| ($active[0] | map(.displayTitle) | unique) as $active_titles | |
| | [$queue[0][] | |
| | title(.) as $display_title | |
| | select(($active_titles | index($display_title)) != null)] | length | |
| ')" | |
| successful_skip_count="$(jq -n --argjson rerun_successful "$RERUN_SUCCESSFUL_LANES" --slurpfile queue automation-output/signoff-dispatch-queue-raw.json --slurpfile successful automation-output/signoff-successful-runners.json ' | |
| if $rerun_successful then 0 | |
| else | |
| def title($lane): "Run \($lane.rancher_version) / \($lane.lane)"; | |
| ($successful[0] | map(.displayTitle) | unique) as $successful_titles | |
| | [$queue[0][] | |
| | title(.) as $display_title | |
| | select(($successful_titles | index($display_title)) != null)] | length | |
| end | |
| ')" | |
| queue_count="$(jq 'length' automation-output/signoff-dispatch-queue.json)" | |
| active_runner_count="$(jq 'length' automation-output/signoff-active-runners.json)" | |
| if ! [[ "$MAX_PARALLEL_LANES" =~ ^[0-9]+$ ]] || [ "$MAX_PARALLEL_LANES" -lt 1 ]; then | |
| echo "max_parallel_lanes must be a positive integer, got '$MAX_PARALLEL_LANES'" | |
| exit 1 | |
| fi | |
| max_parallel_lanes="$MAX_PARALLEL_LANES" | |
| if [ "$max_parallel_lanes" -gt 4 ]; then | |
| max_parallel_lanes=4 | |
| fi | |
| available_runner_count=$((max_parallel_lanes - active_runner_count)) | |
| if [ "$available_runner_count" -lt 0 ]; then | |
| available_runner_count=0 | |
| fi | |
| dispatch_enabled="$DISPATCH_REQUESTED" | |
| dispatch_reason="Dispatching up to ${available_runner_count} uncovered sign-off lane(s)." | |
| if [ "$dispatch_enabled" = "true" ] && [ "$available_runner_count" = "0" ]; then | |
| dispatch_enabled="false" | |
| dispatch_reason="Dispatch blocked because active Run Rancher Sign-Off Lane workflows reached max_parallel_lanes=${max_parallel_lanes}." | |
| elif [ "$dispatch_enabled" != "true" ]; then | |
| dispatch_reason="Dispatch was not requested for this event/input." | |
| elif [ "$queue_count" = "0" ]; then | |
| dispatch_reason="No pending sign-off lanes remain after removing active and already successful lanes." | |
| fi | |
| if [ "$dispatch_enabled" = "true" ]; then | |
| jq --argjson limit "$available_runner_count" '.[0:$limit]' automation-output/signoff-dispatch-queue.json > automation-output/signoff-dispatch-selected.json | |
| else | |
| printf '[]\n' > automation-output/signoff-dispatch-selected.json | |
| fi | |
| printf '[]\n' > automation-output/signoff-dispatch-results.json | |
| selected_count="$(jq 'length' automation-output/signoff-dispatch-selected.json)" | |
| if [ "$dispatch_enabled" = "true" ] && [ "$selected_count" = "0" ] && [ "$queue_count" != "0" ]; then | |
| dispatch_reason="Dispatch was enabled, but no lane was selected." | |
| fi | |
| jq -n \ | |
| --argjson dispatch_requested "$DISPATCH_REQUESTED" \ | |
| --argjson dispatch_enabled "$dispatch_enabled" \ | |
| --arg dispatch_reason "$dispatch_reason" \ | |
| --arg ignored_active_runner_id "$IGNORE_ACTIVE_RUNNER_ID" \ | |
| --argjson rerun_successful_lanes "$RERUN_SUCCESSFUL_LANES" \ | |
| --argjson max_parallel_lanes "$max_parallel_lanes" \ | |
| --argjson available_runner_count "$available_runner_count" \ | |
| --argjson active_runner_count "$active_runner_count" \ | |
| --argjson raw_queue_count "$raw_queue_count" \ | |
| --argjson active_blocked_count "$active_blocked_count" \ | |
| --argjson successful_skip_count "$successful_skip_count" \ | |
| --argjson queue_count "$queue_count" \ | |
| --argjson selected_count "$selected_count" \ | |
| --slurpfile active automation-output/signoff-active-runners.json \ | |
| --slurpfile successful automation-output/signoff-successful-runners.json \ | |
| --slurpfile raw_queue automation-output/signoff-dispatch-queue-raw.json \ | |
| --slurpfile queue automation-output/signoff-dispatch-queue.json \ | |
| --slurpfile selected automation-output/signoff-dispatch-selected.json \ | |
| '{ | |
| dispatch_requested: $dispatch_requested, | |
| dispatch_enabled: $dispatch_enabled, | |
| dispatch_reason: $dispatch_reason, | |
| rerun_successful_lanes: $rerun_successful_lanes, | |
| max_parallel_lanes: $max_parallel_lanes, | |
| available_runner_count: $available_runner_count, | |
| active_runner_count: $active_runner_count, | |
| ignored_active_runner_id: $ignored_active_runner_id, | |
| raw_queue_count: $raw_queue_count, | |
| active_blocked_count: $active_blocked_count, | |
| successful_skip_count: $successful_skip_count, | |
| queue_count: $queue_count, | |
| selected_count: $selected_count, | |
| active_runners: $active[0], | |
| successful_runners: $successful[0], | |
| raw_lanes: $raw_queue[0], | |
| queued_lanes: $queue[0], | |
| selected_lanes: $selected[0] | |
| }' > automation-output/signoff-dispatch-summary.json | |
| echo "Dispatch decision: $dispatch_reason" | |
| echo "Raw dispatch queue:" | |
| jq . automation-output/signoff-dispatch-queue-raw.json | |
| echo "Dispatch queue:" | |
| jq . automation-output/signoff-dispatch-queue.json | |
| echo "Selected lanes:" | |
| jq . automation-output/signoff-dispatch-selected.json | |
| echo "Active runners:" | |
| jq . automation-output/signoff-active-runners.json | |
| echo "Successful runners considered for skips:" | |
| jq . automation-output/signoff-successful-runners.json | |
| { | |
| echo "requested=$DISPATCH_REQUESTED" | |
| echo "enabled=$dispatch_enabled" | |
| echo "reason=$dispatch_reason" | |
| echo "rerun_successful_lanes=$RERUN_SUCCESSFUL_LANES" | |
| echo "max_parallel_lanes=$max_parallel_lanes" | |
| echo "available_runner_count=$available_runner_count" | |
| echo "active_runner_count=$active_runner_count" | |
| echo "ignored_active_runner_id=$IGNORE_ACTIVE_RUNNER_ID" | |
| echo "raw_queue_count=$raw_queue_count" | |
| echo "active_blocked_count=$active_blocked_count" | |
| echo "successful_skip_count=$successful_skip_count" | |
| echo "queue_count=$queue_count" | |
| echo "selected_count=$selected_count" | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Dispatch selected sign-off lanes | |
| if: steps.dispatch-queue.outputs.enabled == 'true' && steps.dispatch-queue.outputs.selected_count != '0' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| REF_NAME: ${{ github.ref_name }} | |
| run: | | |
| set -euo pipefail | |
| { | |
| echo "" | |
| echo "## Dispatched sign-off lane" | |
| echo "" | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| jq -c '.[]' automation-output/signoff-dispatch-selected.json | while IFS= read -r item; do | |
| rancher_version="$(jq -r '.rancher_version' <<<"$item")" | |
| lane="$(jq -r '.lane' <<<"$item")" | |
| previous_rancher_version="$(jq -r '.previous_rancher_version' <<<"$item")" | |
| webhook_image="$(jq -r '.webhook_image' <<<"$item")" | |
| signing_policy="$(jq -r '.signing_policy' <<<"$item")" | |
| rke2_server_count="$(jq -r '.rke2_server_count' <<<"$item")" | |
| args=( | |
| workflow run run-rancher-signoff-lane.yml | |
| --ref "$REF_NAME" | |
| -f "rancher_version=$rancher_version" | |
| -f "lane=$lane" | |
| -f "signing_policy=$signing_policy" | |
| -f "rke2_server_count=$rke2_server_count" | |
| -f "keep_infra_on_failure=false" | |
| -f "run_rancher_tests=true" | |
| ) | |
| if [ -n "$previous_rancher_version" ]; then | |
| args+=(-f "previous_rancher_version=$previous_rancher_version") | |
| fi | |
| if [ -n "$webhook_image" ]; then | |
| args+=(-f "webhook_image=$webhook_image") | |
| fi | |
| gh "${args[@]}" | |
| sleep 3 | |
| dispatched_run="$(TARGET_RUN_TITLE="Run $rancher_version / $lane" gh run list \ | |
| --workflow run-rancher-signoff-lane.yml \ | |
| --limit 20 \ | |
| --json databaseId,status,displayTitle,event,createdAt,url \ | |
| --jq '[.[] | select(.event == "workflow_dispatch") | select(.displayTitle == env.TARGET_RUN_TITLE)] | sort_by(.createdAt) | reverse | .[0] // {}')" | |
| tmp="$(mktemp)" | |
| jq \ | |
| --arg rancher_version "$rancher_version" \ | |
| --arg lane "$lane" \ | |
| --argjson dispatched_run "$dispatched_run" \ | |
| '. + [{ | |
| rancher_version: $rancher_version, | |
| lane: $lane, | |
| dispatch_command_completed: true, | |
| observed_run: $dispatched_run | |
| }]' automation-output/signoff-dispatch-results.json > "$tmp" | |
| mv "$tmp" automation-output/signoff-dispatch-results.json | |
| if [ "$(jq 'has("databaseId")' <<<"$dispatched_run")" = "true" ]; then | |
| run_url="$(jq -r '.url' <<<"$dispatched_run")" | |
| echo "- \`$rancher_version\` / \`$lane\`: $run_url" >> "$GITHUB_STEP_SUMMARY" | |
| else | |
| echo "- \`$rancher_version\` / \`$lane\`: dispatch command completed, but no matching run was observed yet." >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| done | |
| - name: Write step summary | |
| if: ${{ always() && hashFiles('signoff-plan.json') != '' && hashFiles('automation-output/signoff-dispatch-summary.json') != '' }} | |
| run: | | |
| { | |
| echo "## Rancher sign-off plan" | |
| echo "" | |
| echo '```json' | |
| cat signoff-plan.json | |
| echo '```' | |
| echo "" | |
| echo "Dispatch requested: \`${{ steps.dispatch-queue.outputs.requested }}\`." | |
| echo "Dispatch reason: \`${{ steps.dispatch-queue.outputs.reason }}\`." | |
| echo "Rerun successful lanes: \`${{ steps.dispatch-queue.outputs.rerun_successful_lanes }}\`." | |
| echo "Max parallel lanes: \`${{ steps.dispatch-queue.outputs.max_parallel_lanes }}\`." | |
| echo "RKE2 server count for dispatched lanes: \`${{ github.event.inputs.rke2_server_count || '3' }}\`." | |
| echo "Available runner slots: \`${{ steps.dispatch-queue.outputs.available_runner_count }}\`." | |
| echo "Active lane runners: \`${{ steps.dispatch-queue.outputs.active_runner_count }}\`." | |
| echo "Ignored active runner id: \`${{ steps.dispatch-queue.outputs.ignored_active_runner_id }}\`." | |
| echo "Planned lanes: \`${{ steps.dispatch-queue.outputs.raw_queue_count }}\`; active skipped: \`${{ steps.dispatch-queue.outputs.active_blocked_count }}\`; successful skipped: \`${{ steps.dispatch-queue.outputs.successful_skip_count }}\`." | |
| echo "Pending dispatch queue: \`${{ steps.dispatch-queue.outputs.queue_count }}\` lane(s); selected: \`${{ steps.dispatch-queue.outputs.selected_count }}\`." | |
| echo "" | |
| echo '```json' | |
| cat automation-output/signoff-dispatch-summary.json | |
| echo '```' | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| - name: Write plan receipt | |
| id: public_artifacts | |
| if: ${{ always() && hashFiles('signoff-plan.json') != '' }} | |
| run: | | |
| set -euo pipefail | |
| mkdir -p public-plan-artifacts | |
| dispatch_summary='{}' | |
| if [ -s automation-output/signoff-dispatch-summary.json ]; then | |
| dispatch_summary="$(jq -c . automation-output/signoff-dispatch-summary.json)" | |
| fi | |
| jq -n \ | |
| --slurpfile plan signoff-plan.json \ | |
| --argjson dispatch "$dispatch_summary" \ | |
| --arg completed_at "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ | |
| '{ | |
| completed_at: $completed_at, | |
| plan: $plan[0], | |
| dispatch: $dispatch | |
| }' > public-plan-artifacts/signoff-plan-receipt.json | |
| - name: Upload plan artifact | |
| if: ${{ always() && steps.public_artifacts.outcome == 'success' }} | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 | |
| with: | |
| name: signoff-plan | |
| path: public-plan-artifacts/signoff-plan-receipt.json |