PS-10595 [8.4]: Lazy initialization for buffer pool block latches #152
Workflow file for this run
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
| # PS-11078: GitHub Actions build for percona-server 8.4 on Hetzner cax* arm64. | |
| # Replaces the Cirrus arm64 tasks before Cirrus shuts down 2026-06-01. | |
| # | |
| # PS-11179 (2026-05-28): AWS Graviton fallback path added. Hetzner remains the | |
| # primary provider; if the cax41/cax31/cax21 sweep across fsn1/hel1/nbg1 fails | |
| # repeatedly, pick-target switches provider to "aws" and create-runner-aws | |
| # provisions a c7g.4xlarge Graviton3 spot (with on-demand fallback) in | |
| # eu-central-1. Build steps are unchanged; only the runner provisioning path | |
| # branches. | |
| # | |
| # One file, three event paths (BUILD_TYPE + MTR_SUITE picked by `dispatch` job): | |
| # - pull_request to 8.4 -> Debug + MTR main.1st (same-repo PRs; Cirrus per-PR replacement) | |
| # - pull_request_target to 8.4 -> Debug + MTR main.1st (PS-11254: fork PRs by percona org members only) | |
| # - workflow_dispatch -> manual run OR nightly (build_type RelWithDebInfo -> binlog_nogtid) | |
| # | |
| # Nightly RelWithDebInfo + binlog_nogtid is NOT a `schedule:` trigger here: | |
| # GHA cron only fires from the DEFAULT branch, so a cron in this file would be | |
| # dead on every non-default branch (e.g. 9.7). Instead .github/workflows/ | |
| # builds-nightly.yml lives on the default branch and dispatches THIS file via | |
| # workflow_dispatch --ref on each maintained branch, so each nightly runs that | |
| # branch's own self-contained builds.yml (its boost version, cmake/apt deltas). | |
| # | |
| # Notes for follow-ups (per Przemek DM 2026-05-27): | |
| # - This file lives on the 8.4 branch. The 9.7 branch carries the same | |
| # shape; per-branch deltas are the parent branch, boost version, and any | |
| # cmake/apt differences. | |
| # - x86_64 nightly RelWithDebInfo can be added as a sibling job reusing the | |
| # `dispatch` outputs (BUILD_TYPE / MTR_SUITE / CCACHE_MAXSIZE). Intentionally | |
| # not wired in this PR per "we may do it, let's see how arm64 behaves first". | |
| # | |
| # Trust split: pick-target / create-runner-* / delete-runner-* hold infra | |
| # secrets (GHA_RUNNER_HCLOUD_TOKEN, GHA_RUNNER_PAT, AWS_ROLE_ARN via OIDC). | |
| # The build-arm64 job runs on the ephemeral runner (Hetzner OR EC2) and only | |
| # sees GITHUB_TOKEN (read-only), so PR code never reaches infra credentials. | |
| # | |
| # PS-11254: fork PRs run via pull_request_target so the provisioning jobs | |
| # resolve secrets in base-repo context. This is safe ONLY because the | |
| # secret-bearing jobs do NO checkout; build-arm64 is the sole job that checks | |
| # out PR code, and it holds no secrets. The `dispatch` gate limits | |
| # pull_request_target to fork PRs whose author is a percona org member | |
| # (author_association MEMBER/OWNER/COLLABORATOR). INVARIANT: never add a PR-code | |
| # checkout to a secret-bearing job. | |
| name: builds | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| build_type: | |
| description: 'Build type' | |
| type: choice | |
| options: | |
| - Debug | |
| - RelWithDebInfo | |
| default: Debug | |
| debug_keep_vm: | |
| description: 'On create-runner failure: keep Hetzner VM alive so you can SSH in and grab /var/log/cloud-init-output.log + /actions-runner/_diag/* manually. SSH as root using key 107239874 (anderson@percona). Remember to delete the VM when done. (Hetzner path only; EC2 fallback always terminates.)' | |
| type: boolean | |
| default: false | |
| force_provider: | |
| description: 'Override provider selection (debug + cost benchmarking). "auto" = Hetzner with EC2 fallback (default, production behavior). "hetzner" = Hetzner only, fail if exhausted (no AWS spend). "aws" = skip Hetzner, go straight to EC2 (validate AWS path on demand without waiting for a real Hetzner outage).' | |
| type: choice | |
| options: | |
| - auto | |
| - hetzner | |
| - aws | |
| default: auto | |
| pull_request: | |
| branches: [8.4] | |
| paths-ignore: | |
| # Mirrors azure-pipelines.yml `paths.exclude`. | |
| - 'doc/**' | |
| - 'build-ps/**' | |
| - 'man/**' | |
| - 'mysql-test/**' | |
| - 'packaging/**' | |
| - 'policy/**' | |
| - 'scripts/**' | |
| - 'support-files/**' | |
| pull_request_target: | |
| # PS-11254: fork PRs get no secrets on `pull_request` (GitHub withholds | |
| # them from forks), so org-member fork PRs run here in base-repo context, | |
| # gated by `dispatch` below. Same-repo PRs stay on `pull_request`. | |
| branches: [8.4] | |
| paths-ignore: | |
| - 'doc/**' | |
| - 'build-ps/**' | |
| - 'man/**' | |
| - 'mysql-test/**' | |
| - 'packaging/**' | |
| - 'policy/**' | |
| - 'scripts/**' | |
| - 'support-files/**' | |
| concurrency: | |
| # PR runs (pull_request + pull_request_target) are cancellable so superseded | |
| # pushes do not waste Hetzner cycles; workflow_dispatch runs (including the | |
| # nightly dispatched from the default branch) stay protected | |
| # (cancel-in-progress: false) because they bill paid arm64 minutes. | |
| # event_name is in the key so a fork PR's (skipped) pull_request run and its | |
| # real pull_request_target run do not share a group. | |
| group: build-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }} | |
| cancel-in-progress: ${{ github.event_name == 'pull_request' || github.event_name == 'pull_request_target' }} | |
| # Workflow-level permissions are minimal (contents: read) to limit blast | |
| # radius for any future jobs added here. The id-token: write grant required | |
| # for OIDC -> AWS STS federation lives only on the two jobs that actually | |
| # use it (create-runner-aws, delete-runner-aws). build-arm64 runs untrusted | |
| # PR code on the ephemeral runner and intentionally does not get id-token. | |
| permissions: | |
| contents: read | |
| env: | |
| PARENT_BRANCH: '8.4' | |
| BUILD_PARAMS_TYPE: normal | |
| COMPILER: gcc | |
| COMPILER_VER: '' | |
| IMAGE_NAME: ubuntu-26.04 | |
| UBUNTU_CODE_NAME: resolute | |
| BOOST_VERSION: boost_1_84_0 | |
| USE_CCACHE: '1' | |
| CCACHE_COMPRESS: '1' | |
| CCACHE_COMPRESSLEVEL: '9' | |
| CCACHE_CPP2: '1' | |
| IMAGE: ubuntu-26.04 | |
| SSH_KEY_ID: '107239874' | |
| EPHEMERAL_RUNNER_NAME: ps-arm64-${{ github.run_id }}-${{ github.run_attempt }} | |
| # PS-11179: EC2 fallback knobs. eu-central-1 chosen to match the existing | |
| # Jenkins eu-central-1 footprint (low latency to EU contributors, no new | |
| # cross-region S3/ECR traffic for boost / ccache restore). c7g.4xlarge gives | |
| # 16 vCPU + 32 GB Graviton3, which is parity with the cax41 default. | |
| AWS_REGION: eu-central-1 | |
| AWS_INSTANCE_TYPE: c7g.4xlarge | |
| jobs: | |
| # TRUSTED authorization gate (PS-11254). Root of the job graph: dispatch | |
| # needs it and every other job chains off dispatch, so gating here gates the | |
| # whole run. | |
| # - schedule / workflow_dispatch: always | |
| # - pull_request: same-repo PRs only (forks get no secrets on this event) | |
| # - pull_request_target: fork PRs only, authorized when the PR author has | |
| # write+ access to the repo. author_association is deliberately NOT used: | |
| # its value in the event payload is CONTRIBUTOR for PRIVATE percona org | |
| # members, so it would wrongly reject them. The repo-permission API call | |
| # via GHA_RUNNER_PAT is reliable (empirically confirmed: members resolve | |
| # to write, non-collaborators to read on this public repo). This job does | |
| # no checkout and runs in base-repo context, so the trust split holds. | |
| authorize: | |
| runs-on: ubuntu-latest | |
| permissions: {} | |
| outputs: | |
| ok: ${{ steps.gate.outputs.ok }} | |
| steps: | |
| - id: gate | |
| env: | |
| GH_TOKEN: ${{ secrets.GHA_RUNNER_PAT }} | |
| EVENT: ${{ github.event_name }} | |
| HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }} | |
| BASE_REPO: ${{ github.repository }} | |
| AUTHOR: ${{ github.event.pull_request.user.login }} | |
| run: | | |
| set -u | |
| ok=false | |
| case "$EVENT" in | |
| schedule|workflow_dispatch) | |
| ok=true ;; | |
| pull_request) | |
| [ "$HEAD_REPO" = "$BASE_REPO" ] && ok=true ;; | |
| pull_request_target) | |
| if [ "$HEAD_REPO" != "$BASE_REPO" ]; then | |
| perm=$(curl -s -H "Authorization: Bearer $GH_TOKEN" \ | |
| -H "Accept: application/vnd.github+json" \ | |
| "https://api.github.com/repos/$BASE_REPO/collaborators/$AUTHOR/permission" \ | |
| | jq -r '.permission // "none"' 2>/dev/null || echo none) | |
| case "$perm" in admin|maintain|write) ok=true ;; esac | |
| echo "::notice::fork PR by $AUTHOR; repo permission=$perm -> authorized=$ok" | |
| fi ;; | |
| esac | |
| echo "ok=$ok" >> "$GITHUB_OUTPUT" | |
| # TRUSTED. Pick BUILD_TYPE + MTR suite + cache size based on event. | |
| dispatch: | |
| needs: authorize | |
| if: needs.authorize.outputs.ok == 'true' | |
| runs-on: ubuntu-latest | |
| permissions: {} | |
| outputs: | |
| build_type: ${{ steps.pick.outputs.build_type }} | |
| mtr_suite: ${{ steps.pick.outputs.mtr_suite }} | |
| ccache_maxsize: ${{ steps.pick.outputs.ccache_maxsize }} | |
| steps: | |
| - id: pick | |
| run: | | |
| set -eu | |
| case "${{ github.event_name }}" in | |
| pull_request|pull_request_target) | |
| BT=Debug; SUITE=main.1st; CACHE=4G | |
| ;; | |
| workflow_dispatch) | |
| # Manual runs AND the nightly dispatcher (builds-nightly.yml on | |
| # the default branch) land here. build_type=RelWithDebInfo -> | |
| # binlog_nogtid (Cirrus nightly parity, Przemek 2026-05-18); | |
| # build_type=Debug -> main.1st. | |
| BT="${{ inputs.build_type }}" | |
| if [ "$BT" = "Debug" ]; then | |
| SUITE=main.1st; CACHE=4G | |
| else | |
| SUITE=binlog_nogtid; CACHE=8G | |
| fi | |
| ;; | |
| esac | |
| echo "Event ${{ github.event_name }} -> BUILD_TYPE=$BT, MTR=$SUITE, CCACHE_MAXSIZE=$CACHE" >> "$GITHUB_STEP_SUMMARY" | |
| { | |
| echo "build_type=$BT" | |
| echo "mtr_suite=$SUITE" | |
| echo "ccache_maxsize=$CACHE" | |
| } >> "$GITHUB_OUTPUT" | |
| # TRUSTED. Dynamic capacity probe with PS-11179 fallback wiring. | |
| # | |
| # Sweep == one full pass over the 9 (server_type, dc) combinations | |
| # (cax41/cax31/cax21 × fsn1/hel1/nbg1), each combination probed via the | |
| # existing POST-then-DELETE pattern (HTTP 201 == we could have created the | |
| # VM, so we have capacity; delete and report the (type, dc) back to | |
| # create-runner-hetzner). | |
| # | |
| # On a fully-saturated Hetzner arm64 fleet we retry 4 sweeps total | |
| # (1 initial + 3 retries) with backoff 2/5/10 minutes between attempts. | |
| # Total wait before EC2 fallback fires: | |
| # 2 + 5 + 10 = 17 min. | |
| # The original 9-sweep / ~3h7m curve was agreed with Przemyslaw DM | |
| # 2026-05-28; PS-11254 shortened it so the AWS Graviton fallback takes | |
| # over fast on a real capacity outage instead of leaving a PR without | |
| # arm64 feedback for ~3h. Early retries still catch the common short dips. | |
| # | |
| # Implemented as a single bash loop in this single job so the workflow | |
| # graph stays linear (no matrix x retries explosion in the UI). | |
| # | |
| # Outputs: | |
| # provider "hetzner" (normal) or "aws" (all sweeps exhausted) | |
| # location hetzner location (fsn1/hel1/nbg1) if provider=hetzner; "" otherwise | |
| # server_type hetzner server type (cax41/cax31/cax21) if provider=hetzner; "" otherwise | |
| pick-target: | |
| needs: dispatch | |
| runs-on: ubuntu-latest | |
| permissions: {} | |
| # PS-11254: 4 sweeps × backoff totals ~17m of sleeps plus 36 HTTP | |
| # round-trips (~2 min worst-case) plus job overhead. 30 min leaves | |
| # headroom for slow Hetzner API responses without auto-cancelling. | |
| timeout-minutes: 30 | |
| outputs: | |
| provider: ${{ steps.probe.outputs.provider }} | |
| location: ${{ steps.probe.outputs.location }} | |
| server_type: ${{ steps.probe.outputs.server_type }} | |
| steps: | |
| - id: probe | |
| env: | |
| HCLOUD_TOKEN: ${{ secrets.GHA_RUNNER_HCLOUD_TOKEN }} | |
| FORCE_PROVIDER: ${{ github.event_name == 'workflow_dispatch' && inputs.force_provider || 'auto' }} | |
| run: | | |
| set -euo pipefail | |
| # PS-11179: force_provider override (workflow_dispatch only; defaults | |
| # to "auto" for pull_request / pull_request_target, and for the nightly | |
| # dispatched as workflow_dispatch). Lets operators validate the | |
| # EC2 path on demand without waiting for a real Hetzner outage, or | |
| # pin to Hetzner-only when AWS spend must be avoided. | |
| case "$FORCE_PROVIDER" in | |
| aws) | |
| echo "::notice::force_provider=aws -> skipping Hetzner probing, going straight to EC2 fallback" | |
| echo "### Provider override: AWS (forced via workflow_dispatch input)" >> "$GITHUB_STEP_SUMMARY" | |
| { | |
| echo "provider=aws" | |
| echo "location=" | |
| echo "server_type=" | |
| } >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| ;; | |
| hetzner) | |
| echo "::notice::force_provider=hetzner -> EC2 fallback disabled; will fail if Hetzner exhausted" | |
| ;; | |
| auto) | |
| ;; | |
| *) | |
| echo "::error::Unrecognised force_provider value: $FORCE_PROVIDER (expected auto|hetzner|aws)" | |
| exit 1 | |
| ;; | |
| esac | |
| # PS-11179: backoff curve in minutes between sweep attempts. | |
| # Index 0 is the gap AFTER sweep 1 fails (before sweep 2), etc. | |
| # PS-11254: 3 entries == 3 retries after the initial sweep == 4 sweeps | |
| # total (~17m: 2+5+10), so the AWS EC2 fallback takes over fast instead | |
| # of burning ~3h on a Hetzner capacity outage. | |
| BACKOFF_MIN=(2 5 10) | |
| MAX_SWEEPS=4 | |
| for sweep in $(seq 1 "$MAX_SWEEPS"); do | |
| echo "::group::Hetzner capacity sweep $sweep/$MAX_SWEEPS" | |
| for type in cax41 cax31 cax21; do | |
| for dc in fsn1 hel1 nbg1; do | |
| PROBE_NAME="cap-probe-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-s${sweep}-${type}-${dc}" | |
| HTTP=$(curl -sS -o /tmp/resp.json -w "%{http_code}" -X POST \ | |
| -H "Authorization: Bearer $HCLOUD_TOKEN" -H "Content-Type: application/json" \ | |
| https://api.hetzner.cloud/v1/servers \ | |
| -d "{\"name\":\"$PROBE_NAME\",\"server_type\":\"$type\",\"image\":\"$IMAGE\",\"location\":\"$dc\",\"start_after_create\":false}") | |
| if [ "$HTTP" = "201" ]; then | |
| ID=$(jq -r '.server.id // empty' /tmp/resp.json) | |
| [ -n "$ID" ] || { echo "::error::201 without server id"; exit 1; } | |
| curl -fsS -X DELETE -H "Authorization: Bearer $HCLOUD_TOKEN" \ | |
| "https://api.hetzner.cloud/v1/servers/$ID" >/dev/null | |
| if [ "$type" != "cax41" ]; then | |
| echo "::warning::cax41 unavailable (PS-11174); falling back to $type in $dc" | |
| fi | |
| if [ "$sweep" -gt 1 ]; then | |
| echo "::notice::Hetzner capacity recovered on sweep $sweep" | |
| fi | |
| echo "Hetzner target: $type in $dc (sweep $sweep/$MAX_SWEEPS)" >> "$GITHUB_STEP_SUMMARY" | |
| { | |
| echo "provider=hetzner" | |
| echo "location=$dc" | |
| echo "server_type=$type" | |
| } >> "$GITHUB_OUTPUT" | |
| echo "::endgroup::" | |
| exit 0 | |
| fi | |
| ERR=$(jq -r '.error.code // "?"' /tmp/resp.json 2>/dev/null || echo "?") | |
| echo "::warning::sweep $sweep: $type/$dc: HTTP $HTTP ($ERR)" | |
| done | |
| done | |
| echo "::endgroup::" | |
| # Sleep before the next sweep unless this was the last one. | |
| if [ "$sweep" -lt "$MAX_SWEEPS" ]; then | |
| idx=$((sweep - 1)) | |
| wait_min="${BACKOFF_MIN[$idx]}" | |
| echo "::notice::All 9 Hetzner combos exhausted on sweep $sweep; sleeping ${wait_min}m before sweep $((sweep + 1))" | |
| sleep $((wait_min * 60)) | |
| fi | |
| done | |
| # PS-11179: All sweeps exhausted; switch to EC2 path (unless | |
| # force_provider=hetzner explicitly disabled fallback). | |
| if [ "$FORCE_PROVIDER" = "hetzner" ]; then | |
| echo "::error::Hetzner CAX exhausted after $MAX_SWEEPS sweeps AND force_provider=hetzner; failing per request" | |
| exit 1 | |
| fi | |
| echo "::warning::Hetzner CAX capacity exhausted after $MAX_SWEEPS sweeps (~17m wall time); falling back to AWS EC2 c7g.4xlarge in $AWS_REGION" | |
| { | |
| echo "### EC2 fallback fired (PS-11179)" | |
| echo "" | |
| echo "Hetzner cax41/cax31/cax21 across fsn1/hel1/nbg1 had no capacity" | |
| echo "across $MAX_SWEEPS sweeps spanning ~17m. Falling back to AWS" | |
| echo "EC2 c7g.4xlarge (Graviton3) in eu-central-1." | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| { | |
| echo "provider=aws" | |
| echo "location=" | |
| echo "server_type=" | |
| } >> "$GITHUB_OUTPUT" | |
| # TRUSTED. Provision the ephemeral Hetzner runner. Runs only when | |
| # pick-target found Hetzner capacity. | |
| # | |
| # Renamed from `create-runner` to `create-runner-hetzner` in PS-11179 so the | |
| # AWS sibling job can live alongside it. Body otherwise unchanged. | |
| create-runner-hetzner: | |
| needs: [dispatch, pick-target] | |
| if: needs.pick-target.outputs.provider == 'hetzner' | |
| runs-on: ubuntu-latest | |
| permissions: {} | |
| outputs: | |
| runner_label: ${{ env.EPHEMERAL_RUNNER_NAME }} | |
| server_id: ${{ steps.create.outputs.server_id }} | |
| server_type: ${{ needs.pick-target.outputs.server_type }} | |
| steps: | |
| - id: create | |
| uses: olexandr-havryliak/hcloud-github-runner@bb1089d8b718a06493cb37c51dfe596e44baefc2 | |
| with: | |
| mode: create | |
| name: ${{ env.EPHEMERAL_RUNNER_NAME }} | |
| hcloud_token: ${{ secrets.GHA_RUNNER_HCLOUD_TOKEN }} | |
| github_token: ${{ secrets.GHA_RUNNER_PAT }} | |
| image: ${{ env.IMAGE }} | |
| location: ${{ needs.pick-target.outputs.location }} | |
| server_type: ${{ needs.pick-target.outputs.server_type }} | |
| ssh_key: ${{ env.SSH_KEY_ID }} | |
| # Default budget is 60x10s = 10 min; bumped to 20 min absorbs transient | |
| # apt-mirror or GitHub-release slowness during bootstrap. | |
| runner_wait: '120' | |
| # Retry apt with backoff for transient apt-mirror hiccups on first | |
| # cloud-init (Codex diagnosis 2026-05-22). | |
| pre_runner_script: | | |
| set -euxo pipefail | |
| export DEBIAN_FRONTEND=noninteractive | |
| ok=0 | |
| for i in 1 2 3; do | |
| if apt-get update -y && apt-get install -y --no-install-recommends curl ca-certificates jq; then | |
| ok=1 | |
| break | |
| fi | |
| sleep $((i * 15)) | |
| done | |
| [ "$ok" = 1 ] | |
| # On create-runner failure with workflow_dispatch `debug_keep_vm` set, | |
| # surface VM IP + SSH instructions in the run summary BEFORE delete-runner | |
| # reaps the VM. Anderson SSHs in manually with his personal key | |
| # (107239874) to grab cloud-init logs and the runner _diag dir. | |
| - name: Preserve VM for manual diagnosis (on failure, debug-only) | |
| if: failure() && inputs.debug_keep_vm == true | |
| env: | |
| HCLOUD_TOKEN: ${{ secrets.GHA_RUNNER_HCLOUD_TOKEN }} | |
| SERVER_ID: ${{ steps.create.outputs.server_id }} | |
| run: | | |
| set -euo pipefail | |
| [ -n "${SERVER_ID:-}" ] || { echo "::warning::No server_id; nothing to preserve"; exit 0; } | |
| IP=$(curl -fsS -H "Authorization: Bearer $HCLOUD_TOKEN" \ | |
| "https://api.hetzner.cloud/v1/servers/$SERVER_ID" \ | |
| | jq -r '.server.public_net.ipv4.ip') | |
| { | |
| echo "### Debug VM kept alive for manual diagnosis" | |
| echo "" | |
| echo "Server ID: \`$SERVER_ID\`" | |
| echo "Public IPv4: \`$IP\`" | |
| echo "" | |
| echo "**SSH** (key 107239874, \`anderson@percona\`):" | |
| echo "\`\`\`bash" | |
| echo "ssh root@$IP" | |
| echo "\`\`\`" | |
| echo "" | |
| echo "**Files to grab:**" | |
| echo "- \`/var/log/cloud-init.log\`" | |
| echo "- \`/var/log/cloud-init-output.log\`" | |
| echo "- \`/actions-runner/_diag/\` (if it exists)" | |
| echo "- \`/actions-runner/.runner\` + \`.credentials\` (if registration partially completed)" | |
| echo "" | |
| echo "**Cleanup when done:**" | |
| echo "\`\`\`bash" | |
| echo "curl -X DELETE -H \"Authorization: Bearer \$HCLOUD_TOKEN\" \\" | |
| echo " https://api.hetzner.cloud/v1/servers/$SERVER_ID" | |
| echo "\`\`\`" | |
| echo "" | |
| echo "orphan-sweep.yml will reap this VM after 6h regardless." | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| # TRUSTED. Provision the ephemeral EC2 runner (PS-11179 fallback path). | |
| # Runs only when pick-target exhausted Hetzner. | |
| # | |
| # Provider-selection design (locked 2026-05-28): | |
| # - Region: eu-central-1 (hardcoded; matches existing Jenkins footprint, | |
| # low latency to EU contributors, no cross-region cache traffic). | |
| # - Instance type: c7g.4xlarge (16 vCPU / 32 GB Graviton3 == cax41 parity). | |
| # - Spot first across eu-central-1a/1b/1c; on InsufficientInstanceCapacity | |
| # in all 3 AZs, retry on-demand across the same 3 AZs. | |
| # - AMI: Canonical Ubuntu 26.04 arm64 resolved via SSM Parameter Store | |
| # at runtime (no hardcoded AMI ID; auto-refreshes weekly). | |
| # - Self-termination: --instance-initiated-shutdown-behavior terminate | |
| # combined with userData `shutdown -h now` after runner exits; | |
| # delete-runner-aws is the always-runs safety net for forced cancels. | |
| # - Mandatory cluster tags (enforced by IAM tag-on-create condition and | |
| # by the LambdaEC2Cleanup sweeper): iit-billing-tag + PerconaKeep. | |
| # | |
| # Trust split (Codex review fix 2026-05-28, P0-1 + P0-2): | |
| # - NO `actions/checkout` here. PR code must not enter this trusted job, | |
| # because the workflow holds AWS_ROLE_ARN + GHA_RUNNER_PAT. | |
| # userData content is INLINED via heredoc below; this workflow file | |
| # is the single source of truth for the runner bootstrap. Inlining | |
| # avoids the "PR-controlled checkout poisons trusted bootstrap" | |
| # attack class (Codex review 2026-05-28 P0-1). | |
| # - NO instance profile attached. The ephemeral VM holds no AWS creds. | |
| # PR code on the runner cannot reach the AWS API. The only credential | |
| # on the instance is the one-shot ~1h-TTL GitHub registration token, | |
| # embedded in userData for `./config.sh --token`. | |
| create-runner-aws: | |
| needs: [dispatch, pick-target] | |
| if: needs.pick-target.outputs.provider == 'aws' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| # id-token: write for OIDC -> STS exchange in | |
| # aws-actions/configure-aws-credentials. | |
| # contents: read is the minimum for the runner to bootstrap; we do | |
| # not checkout the repo here (see trust-split comment above). | |
| contents: read | |
| id-token: write | |
| outputs: | |
| runner_label: ${{ env.EPHEMERAL_RUNNER_NAME }} | |
| instance_id: ${{ steps.run.outputs.instance_id }} | |
| az: ${{ steps.run.outputs.az }} | |
| market: ${{ steps.run.outputs.market }} | |
| steps: | |
| # OIDC -> STS. Role created in Percona/percona-cd-platform PR 1 | |
| # (terraform/iam/gha-percona-server-ec2-fallback.tf). Trust policy | |
| # constrains audience to sts.amazonaws.com and subject to this repo + | |
| # workflow ref so other repos / branches cannot assume the role. | |
| # Pinned to v4.3.1 (current stable v4) by commit SHA; bumping requires | |
| # re-verifying the action's source and updating this comment. | |
| - name: Configure AWS credentials via OIDC | |
| uses: aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a # v4.3.1 | |
| with: | |
| role-to-assume: ${{ secrets.AWS_ROLE_ARN }} | |
| role-session-name: gha-${{ github.run_id }}-${{ github.run_attempt }} | |
| aws-region: ${{ env.AWS_REGION }} | |
| # GitHub Actions runner registration token. One-shot, 1h TTL. Lives in | |
| # the userData script so the EC2 instance can register itself with | |
| # `./config.sh --token`. Generated here (not on the instance) because | |
| # the EC2 instance does not hold GHA_RUNNER_PAT. | |
| - name: Mint GitHub runner registration token | |
| id: regtoken | |
| env: | |
| GH_TOKEN: ${{ secrets.GHA_RUNNER_PAT }} | |
| run: | | |
| set -euo pipefail | |
| TOKEN=$(gh api -X POST /repos/${{ github.repository }}/actions/runners/registration-token --jq .token) | |
| if [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; then | |
| echo "::error::Failed to mint runner registration token" | |
| exit 1 | |
| fi | |
| # Mark as a workflow secret so accidental log echoes are scrubbed. | |
| echo "::add-mask::$TOKEN" | |
| echo "token=$TOKEN" >> "$GITHUB_OUTPUT" | |
| # Resolve the current Canonical Ubuntu 26.04 arm64 AMI from SSM at | |
| # runtime. Canonical refreshes this parameter weekly with security | |
| # patches; never hardcode the AMI ID in this workflow. | |
| - name: Resolve Ubuntu 26.04 arm64 AMI | |
| id: ami | |
| run: | | |
| set -euo pipefail | |
| AMI=$(aws ssm get-parameter \ | |
| --name /aws/service/canonical/ubuntu/server/26.04/stable/current/arm64/hvm/ebs-gp3/ami-id \ | |
| --region "$AWS_REGION" \ | |
| --query Parameter.Value --output text) | |
| if [ -z "$AMI" ] || [ "$AMI" = "None" ]; then | |
| echo "::error::Failed to resolve Ubuntu 26.04 arm64 AMI from SSM" | |
| exit 1 | |
| fi | |
| echo "Resolved AMI: $AMI" | |
| echo "ami=$AMI" >> "$GITHUB_OUTPUT" | |
| # Build the userData payload inline (P0-1 fix). The heredoc body uses | |
| # only literal shell `${VAR}` references that are expanded by the | |
| # workflow runner's shell at write time, NOT by envsubst on a file | |
| # that PR code could mutate. The resulting plain-bash script is what | |
| # cloud-init runs on the EC2 instance. | |
| # | |
| # Variables exported into this step: | |
| # RUNNER_TOKEN one-shot ~1h-TTL GitHub registration token | |
| # EPHEMERAL_RUNNER_NAME unique runner name (=== RUNNER_LABEL) | |
| # RUNNER_URL https://github.com/<owner>/<repo> | |
| # RUNNER_VERSION pinned actions/runner release (without leading v) | |
| # RUNNER_SHA256 SHA256 of actions-runner-linux-arm64-<ver>.tar.gz | |
| - name: Render userData (inline; no checkout) | |
| env: | |
| RUNNER_TOKEN: ${{ steps.regtoken.outputs.token }} | |
| RUNNER_URL: https://github.com/${{ github.repository }} | |
| # Pinned actions/runner release. Bump together with RUNNER_SHA256. | |
| # GitHub deprecates runner versions (typically 4-6 months after | |
| # release); a deprecated runner registers fine but is immediately | |
| # rejected with "cannot receive messages" and exits. Verify the pin | |
| # against https://github.com/actions/runner/releases periodically. | |
| # Last bump 2026-05-28 (v2.328.0 -> v2.334.0; 2.328.0 was deprecated). | |
| RUNNER_VERSION: '2.334.0' | |
| # SHA256 of actions-runner-linux-arm64-2.334.0.tar.gz from the | |
| # actions/runner GitHub release page (BEGIN/END SHA linux-arm64 markers). | |
| RUNNER_SHA256: 'f44255bd3e80160eb25f71bc83d06ea025f6908748807a584687b3184759f7e4' | |
| run: | | |
| set -euo pipefail | |
| mkdir -p /tmp/ec2 | |
| # Fail fast if any required variable is empty; we'd rather not | |
| # ship a half-rendered userData to AWS. | |
| : "${RUNNER_TOKEN:?registration token missing}" | |
| : "${EPHEMERAL_RUNNER_NAME:?runner name missing}" | |
| : "${RUNNER_URL:?runner url missing}" | |
| : "${RUNNER_VERSION:?runner version pin missing}" | |
| : "${RUNNER_SHA256:?runner sha256 pin missing}" | |
| # Heredoc WITHOUT a quoted delimiter -> the workflow shell expands | |
| # ${VAR} at write time. PR code never touches this content. | |
| # YAML forces a multi-line block to share an indentation prefix; | |
| # we strip exactly 10 leading spaces on every heredoc body line | |
| # via sed before writing so cloud-init sees a script starting at | |
| # column 1 (the `#!` shebang must be column 1 or cloud-init | |
| # refuses to exec it). | |
| sed -E 's/^ //' <<EOF > /tmp/ec2/userdata.sh | |
| #!/bin/bash | |
| # Inlined from .github/workflows/builds.yml (create-runner-aws). | |
| # This heredoc is the single source of truth for the runner | |
| # bootstrap script. Do not duplicate it to a separate file | |
| # (would be a PR-poisonable trusted-bootstrap path; PS-11219). | |
| set -euxo pipefail | |
| exec > >(tee -a /var/log/gha-runner.log) 2>&1 | |
| # Self-shutdown on any exit path. RunInstances was called with | |
| # --instance-initiated-shutdown-behavior terminate, so this | |
| # reaps the instance rather than just powering it off. | |
| trap '/sbin/shutdown -h now' EXIT | |
| echo "Starting userData at \$(date -u +%FT%TZ)" | |
| echo "Resolved RUNNER_VERSION=${RUNNER_VERSION}" | |
| echo "Resolved EPHEMERAL_RUNNER_NAME=${EPHEMERAL_RUNNER_NAME}" | |
| export DEBIAN_FRONTEND=noninteractive | |
| # Install runtime deps with retry-on-flake. | |
| ok=0 | |
| for i in 1 2 3; do | |
| if apt-get update -y \\ | |
| && apt-get install -y --no-install-recommends \\ | |
| curl ca-certificates jq tar libicu78 git sudo; then | |
| ok=1 | |
| break | |
| fi | |
| sleep \$((i * 15)) | |
| done | |
| [ "\$ok" = 1 ] || { echo "apt install failed after 3 attempts"; exit 1; } | |
| # Unprivileged runner user; /home/runner matches the Hetzner | |
| # action's convention and the CCACHE_DIR baked into builds.yml. | |
| if ! id -u runner >/dev/null 2>&1; then | |
| useradd --create-home --shell /bin/bash --home-dir /home/runner runner | |
| fi | |
| # Passwordless sudo for the runner. percona-server build steps | |
| # invoke \`sudo apt-get\` extensively (Install Build Dependencies); | |
| # without NOPASSWD the runner cannot install packages and the build | |
| # fails immediately. The instance has no inbound network (no SSH); | |
| # the runner only runs one ephemeral job; expanding sudo to NOPASSWD | |
| # is bounded by the ephemeral-VM blast radius. | |
| install -m 0440 -o root -g root /dev/stdin /etc/sudoers.d/90-runner <<'SUDOERS' | |
| runner ALL=(ALL) NOPASSWD:ALL | |
| SUDOERS | |
| RUNNER_DIR=/home/runner/actions-runner | |
| mkdir -p "\${RUNNER_DIR}" | |
| chown -R runner:runner /home/runner | |
| # Download the pinned actions/runner ARM64 tarball. | |
| TARBALL="actions-runner-linux-arm64-${RUNNER_VERSION}.tar.gz" | |
| TARBALL_URL="https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/\${TARBALL}" | |
| cd "\${RUNNER_DIR}" | |
| curl --fail --silent --show-error --location --retry 5 --retry-delay 5 \\ | |
| --output "\${TARBALL}" "\${TARBALL_URL}" | |
| # FAIL HARD on checksum mismatch. | |
| echo "${RUNNER_SHA256} \${TARBALL}" | sha256sum --check --status \\ | |
| || { echo "SHA256 mismatch for \${TARBALL}; refusing to continue"; exit 1; } | |
| tar xzf "\${TARBALL}" | |
| rm -f "\${TARBALL}" | |
| chown -R runner:runner "\${RUNNER_DIR}" | |
| # Configure + run as the unprivileged user. | |
| # --ephemeral : deregister after one job | |
| # --disableupdate : never self-update mid-job (we pin the version) | |
| # --unattended : non-interactive | |
| sudo -u runner -H bash -lc " | |
| set -euxo pipefail | |
| cd '\${RUNNER_DIR}' | |
| ./config.sh \\ | |
| --url '${RUNNER_URL}' \\ | |
| --token '${RUNNER_TOKEN}' \\ | |
| --name '${EPHEMERAL_RUNNER_NAME}' \\ | |
| --labels '${EPHEMERAL_RUNNER_NAME}' \\ | |
| --runnergroup default \\ | |
| --work _work \\ | |
| --unattended \\ | |
| --ephemeral \\ | |
| --disableupdate | |
| " | |
| echo "Token registered, runner: ${EPHEMERAL_RUNNER_NAME}" | |
| runner_status=0 | |
| sudo -u runner -H bash -lc "cd '\${RUNNER_DIR}' && ./run.sh" || runner_status=\$? | |
| echo "Runner exited with status \${runner_status}" | |
| echo "Shutting down at \$(date -u +%FT%TZ)" | |
| exit 0 | |
| EOF | |
| chmod 0644 /tmp/ec2/userdata.sh | |
| # Sanity checks: | |
| # (a) shebang at column 1 (cloud-init refuses otherwise), | |
| # (b) all required values made it into the rendered script. | |
| if [ "$(head -1 /tmp/ec2/userdata.sh)" != "#!/bin/bash" ]; then | |
| echo "::error::Rendered userData does not start with #!/bin/bash at column 1" | |
| head -3 /tmp/ec2/userdata.sh | |
| exit 1 | |
| fi | |
| for needle in "${EPHEMERAL_RUNNER_NAME}" "${RUNNER_URL}" "${RUNNER_VERSION}" "${RUNNER_SHA256}"; do | |
| if ! grep -qF "$needle" /tmp/ec2/userdata.sh; then | |
| echo "::error::Rendered userData missing expected value (sanity check failed)" | |
| exit 1 | |
| fi | |
| done | |
| # Spot-first across 3 AZs, then on-demand across the same 3 AZs. | |
| # Outputs instance_id, az, market (=spot|on-demand) so delete-runner-aws | |
| # and the GHA summary can report what was launched. | |
| # | |
| # Tags applied on the RunInstances call (not as a follow-up | |
| # CreateTags) because the cluster IAM policy attaches a | |
| # condition that requires iit-billing-tag + PerconaKeep to be | |
| # present in the RunInstances request itself. | |
| # | |
| # --client-token (P1-7): provides launch idempotency keyed on | |
| # run_id + run_attempt + AZ + market, so a retried step does not | |
| # double-launch the same instance. | |
| # | |
| # --iam-instance-profile is DELIBERATELY ABSENT (P0-2 fix). The | |
| # ephemeral VM intentionally holds no AWS credentials; PR code | |
| # cannot reach the AWS API from the runner. | |
| - id: run | |
| name: RunInstances (spot eu-central-1a/1b/1c, then on-demand) | |
| env: | |
| AMI_ID: ${{ steps.ami.outputs.ami }} | |
| run: | | |
| set -euo pipefail | |
| TAG_SPEC="ResourceType=instance,Tags=[\ | |
| {Key=Name,Value=ps-arm64-${{ github.run_id }}-${{ github.run_attempt }}},\ | |
| {Key=iit-billing-tag,Value=percona-server-gha-fallback},\ | |
| {Key=PerconaKeep,Value=True},\ | |
| {Key=github_run_id,Value=${{ github.run_id }}},\ | |
| {Key=github_run_attempt,Value=${{ github.run_attempt }}},\ | |
| {Key=github_workflow,Value=builds},\ | |
| {Key=github_repository,Value=${{ github.repository }}}]" | |
| AZS=(eu-central-1a eu-central-1b eu-central-1c) | |
| INSTANCE_ID="" | |
| CHOSEN_AZ="" | |
| CHOSEN_MARKET="" | |
| # Helper: try to launch in $1=az with market $2=spot|on-demand. | |
| # Echoes the instance id on success, empty on InsufficientCapacity. | |
| # Any other AWS error exits the job non-zero (fail fast). | |
| try_launch() { | |
| local az="$1" market="$2" | |
| local market_opts=() | |
| if [ "$market" = "spot" ]; then | |
| market_opts=(--instance-market-options 'MarketType=spot') | |
| fi | |
| # Client-token: stable per (run_id, run_attempt, az, market) so a | |
| # retried RunInstances call returns the same instance instead of | |
| # launching a duplicate. AWS requires <= 64 chars; the run-id + | |
| # attempt + az + market fits comfortably. | |
| local client_token="ps-${{ github.run_id }}-${{ github.run_attempt }}-${az}-${market}" | |
| set +e | |
| # Canonical's Ubuntu 24.04 ARM64 AMI defaults the root volume to | |
| # 8 GB which fills up partway through the percona-server Debug | |
| # build (boost + ccache + intermediate objects). Override to | |
| # 80 GB gp3, delete-on-termination so the volume dies with the | |
| # ephemeral instance and the cleanup Lambdas have nothing to do. | |
| OUT=$(aws ec2 run-instances \ | |
| --region "$AWS_REGION" \ | |
| --image-id "$AMI_ID" \ | |
| --instance-type "$AWS_INSTANCE_TYPE" \ | |
| --placement "AvailabilityZone=$az" \ | |
| --instance-initiated-shutdown-behavior terminate \ | |
| --user-data file:///tmp/ec2/userdata.sh \ | |
| --tag-specifications "$TAG_SPEC" \ | |
| --block-device-mappings '[{"DeviceName":"/dev/sda1","Ebs":{"VolumeSize":80,"VolumeType":"gp3","DeleteOnTermination":true}}]' \ | |
| --client-token "$client_token" \ | |
| --metadata-options 'HttpTokens=required,HttpPutResponseHopLimit=2,HttpEndpoint=enabled' \ | |
| "${market_opts[@]}" \ | |
| --query 'Instances[0].InstanceId' --output text 2>/tmp/ec2/run.err) | |
| rc=$? | |
| set -e | |
| if [ $rc -eq 0 ] && [ -n "$OUT" ] && [ "$OUT" != "None" ]; then | |
| echo "$OUT" | |
| return 0 | |
| fi | |
| ERR=$(cat /tmp/ec2/run.err) | |
| # Match the two capacity-shaped errors AWS returns; everything | |
| # else (auth, quota, malformed request) is fatal because retrying | |
| # will not help. | |
| # IMPORTANT: warnings/errors must go to STDERR (>&2), otherwise the | |
| # surrounding `$(try_launch ...)` capture swallows them silently. | |
| if echo "$ERR" | grep -qE 'InsufficientInstanceCapacity|SpotMaxPriceTooLow|MaxSpotInstanceCountExceeded'; then | |
| echo "::warning::$market launch failed in $az: $(echo "$ERR" | head -1)" >&2 | |
| return 1 | |
| fi | |
| echo "::error::Fatal RunInstances error in $az ($market): $ERR" >&2 | |
| return 2 | |
| } | |
| # Pass 1: spot across 3 AZs. | |
| for az in "${AZS[@]}"; do | |
| if ID=$(try_launch "$az" spot); then | |
| INSTANCE_ID="$ID" | |
| CHOSEN_AZ="$az" | |
| CHOSEN_MARKET="spot" | |
| break | |
| fi | |
| rc=$? | |
| [ $rc -eq 2 ] && exit 1 | |
| done | |
| # Pass 2: on-demand across the same 3 AZs if spot is fully exhausted. | |
| if [ -z "$INSTANCE_ID" ]; then | |
| echo "::notice::All 3 AZs returned no spot capacity for $AWS_INSTANCE_TYPE; retrying on-demand" | |
| for az in "${AZS[@]}"; do | |
| if ID=$(try_launch "$az" on-demand); then | |
| INSTANCE_ID="$ID" | |
| CHOSEN_AZ="$az" | |
| CHOSEN_MARKET="on-demand" | |
| break | |
| fi | |
| rc=$? | |
| [ $rc -eq 2 ] && exit 1 | |
| done | |
| fi | |
| if [ -z "$INSTANCE_ID" ]; then | |
| echo "::error::No c7g.4xlarge capacity in eu-central-1a/1b/1c for either spot or on-demand. Giving up." | |
| exit 1 | |
| fi | |
| echo "Launched $INSTANCE_ID ($CHOSEN_MARKET) in $CHOSEN_AZ" | |
| { | |
| echo "### EC2 fallback runner launched" | |
| echo "" | |
| echo "- Instance: \`$INSTANCE_ID\`" | |
| echo "- Type: \`$AWS_INSTANCE_TYPE\`" | |
| echo "- AZ: \`$CHOSEN_AZ\`" | |
| echo "- Market: \`$CHOSEN_MARKET\`" | |
| echo "- AMI: \`$AMI_ID\`" | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| { | |
| echo "instance_id=$INSTANCE_ID" | |
| echo "az=$CHOSEN_AZ" | |
| echo "market=$CHOSEN_MARKET" | |
| } >> "$GITHUB_OUTPUT" | |
| # Wait for the runner to register itself with GitHub. Poll the | |
| # repository-scoped runners list every 10s for up to 5 minutes | |
| # (30 polls). cloud-init typically completes in 60-120s on c7g.4xlarge; | |
| # the 5-minute budget absorbs the worst-case GitHub release CDN delay | |
| # observed on Hetzner (Codex 2026-05-22). | |
| - name: Wait for runner to come online | |
| env: | |
| GH_TOKEN: ${{ secrets.GHA_RUNNER_PAT }} | |
| INSTANCE_ID: ${{ steps.run.outputs.instance_id }} | |
| run: | | |
| set -euo pipefail | |
| # 10 min budget. Cloud-init on a fresh c7g.4xlarge spot needs | |
| # apt-update + runner download + extract + configure + register; | |
| # 5 min was too tight (PS-11219 PoC iteration 3 timed out at 5m15s). | |
| # First-boot worst case is ~6-7 min on cold AMI/runner-release cache. | |
| deadline=$(( $(date +%s) + 600 )) | |
| attempt=0 | |
| while [ "$(date +%s)" -lt "$deadline" ]; do | |
| attempt=$((attempt + 1)) | |
| STATUS=$(gh api -X GET /repos/${{ github.repository }}/actions/runners --paginate \ | |
| --jq ".runners[] | select(.name == \"$EPHEMERAL_RUNNER_NAME\") | .status" || true) | |
| if [ "$STATUS" = "online" ]; then | |
| echo "Runner $EPHEMERAL_RUNNER_NAME online after ${attempt} polls" | |
| exit 0 | |
| fi | |
| echo "Attempt $attempt: runner status='${STATUS:-not-registered}' (waiting 10s)" | |
| sleep 10 | |
| done | |
| # On timeout, capture EC2 console output BEFORE terminating so we | |
| # have a post-mortem (no SSM available; the no-IAM-on-VM design | |
| # means we can't shell in either). | |
| echo "::error::Runner $EPHEMERAL_RUNNER_NAME did not come online within 10m; capturing console output and terminating $INSTANCE_ID" | |
| aws ec2 get-console-output --region "$AWS_REGION" --instance-id "$INSTANCE_ID" --output text --query Output 2>&1 | tail -200 || true | |
| aws ec2 terminate-instances --region "$AWS_REGION" --instance-ids "$INSTANCE_ID" >/dev/null || true | |
| exit 1 | |
| # UNTRUSTED. PR code (Debug) or 8.4 HEAD (RelWithDebInfo nightly) runs here. | |
| # Build shape ported from azure-pipelines.yml on 8.4 (per Przemek 2026-05-18: | |
| # ".cirrus.yml is just a simplified port from Azure"). arm64-specific | |
| # deviations (system-gcc selection, MTR libeatmydata, -j cap at 16) are | |
| # annotated inline. | |
| # | |
| # PS-11179: needs lists BOTH create-runner-hetzner + create-runner-aws so | |
| # the dependency wiring works regardless of which provider ran. The | |
| # `runs-on` expression coalesces the two outputs with `||`; exactly one of | |
| # them is populated (the other job was skipped via `if:` and has empty | |
| # outputs), so the coalescing resolves to the active runner label. | |
| build-arm64: | |
| needs: [dispatch, pick-target, create-runner-hetzner, create-runner-aws] | |
| # GHA defaults to skipping a job when ANY needed job is skipped. Since | |
| # exactly ONE of create-runner-hetzner / create-runner-aws is skipped | |
| # (the other ran), we explicitly require: dispatch + pick-target green, | |
| # AND at least one of the two create-runner jobs succeeded. | |
| if: | | |
| always() | |
| && needs.dispatch.result == 'success' | |
| && needs.pick-target.result == 'success' | |
| && (needs.create-runner-hetzner.result == 'success' || needs.create-runner-aws.result == 'success') | |
| runs-on: ${{ needs.create-runner-hetzner.outputs.runner_label || needs.create-runner-aws.outputs.runner_label }} | |
| permissions: | |
| contents: read | |
| timeout-minutes: 240 | |
| env: | |
| BUILD_TYPE: ${{ needs.dispatch.outputs.build_type }} | |
| MTR_SUITE: ${{ needs.dispatch.outputs.mtr_suite }} | |
| CCACHE_MAXSIZE: ${{ needs.dispatch.outputs.ccache_maxsize }} | |
| CCACHE_DIR: /home/runner/ccache | |
| BOOST_DIR: /tmp/boost | |
| DEBIAN_FRONTEND: noninteractive | |
| steps: | |
| - name: System info | |
| run: | | |
| set -eux | |
| # PS-11179: report whichever provider actually fired. | |
| echo "Provider: ${{ needs.pick-target.outputs.provider }}" | |
| if [ "${{ needs.pick-target.outputs.provider }}" = "hetzner" ]; then | |
| echo "Server type: ${{ needs.create-runner-hetzner.outputs.server_type }}" | |
| else | |
| echo "Instance: ${{ needs.create-runner-aws.outputs.instance_id }} (${{ needs.create-runner-aws.outputs.market }} in ${{ needs.create-runner-aws.outputs.az }})" | |
| fi | |
| echo "Build type: $BUILD_TYPE" | |
| echo "MTR suite: $MTR_SUITE" | |
| uname -a | |
| nproc | |
| free -h | |
| df -h / | |
| - name: Conditional swap (only when RAM < 16 GB) | |
| run: | | |
| set -eux | |
| MEM_GB=$(awk '/MemTotal/ {print int($2/1024/1024)}' /proc/meminfo) | |
| if [ "$MEM_GB" -lt 16 ]; then | |
| sudo fallocate -l 16G /swapfile | |
| sudo chmod 600 /swapfile | |
| sudo mkswap /swapfile | |
| sudo swapon /swapfile | |
| fi | |
| free -h | |
| # Azure: `*** Install Build Dependencies`. arm64 picks system gcc (no | |
| # version selector + LLVM repo plumbing needed). | |
| - name: Install Build Dependencies | |
| run: | | |
| set -eux | |
| SELECTED_CC="${COMPILER}${COMPILER_VER:+-${COMPILER_VER}}" | |
| SELECTED_CXX="${COMPILER}++${COMPILER_VER:+-${COMPILER_VER}}" | |
| [ "$COMPILER" = "gcc" ] && SELECTED_CXX="g++${COMPILER_VER:+-${COMPILER_VER}}" | |
| echo "SELECTED_CC=$SELECTED_CC" >> "$GITHUB_ENV" | |
| echo "SELECTED_CXX=$SELECTED_CXX" >> "$GITHUB_ENV" | |
| PACKAGES="$SELECTED_CC" | |
| [ "$COMPILER" = "gcc" ] && PACKAGES="$SELECTED_CXX" | |
| sudo apt-get -yq update | |
| sudo apt-get -yq --no-install-suggests --no-install-recommends install \ | |
| $PACKAGES make pkg-config dpkg-dev unzip lz4 git cmake cmake-curses-gui ccache bison \ | |
| libtirpc-dev libudev-dev libaio-dev libmecab-dev libnuma-dev \ | |
| libssl-dev libreadline-dev libedit-dev libpam-dev \ | |
| libcurl4-openssl-dev libldap2-dev libkrb5-dev libsasl2-dev \ | |
| libsasl2-modules-gssapi-mit \ | |
| libxml-simple-perl | |
| # Azure: SYSTEM_LIBRARIES path (BUILD_PARAMS_TYPE != inverted). | |
| sudo apt-get -yq --no-install-suggests --no-install-recommends install \ | |
| libicu-dev libevent-dev liblz4-dev zlib1g-dev \ | |
| protobuf-compiler libprotobuf-dev libprotoc-dev \ | |
| libzstd-dev libfido2-dev | |
| # arm64-only addition: libeatmydata1 needed for MTR LD_PRELOAD | |
| # (Cirrus arm64 task uses it; Azure x86 jobs do not run MTR). | |
| sudo apt-get -yq --no-install-suggests --no-install-recommends install libeatmydata1 | |
| REAL_COMPILER_VER=$($SELECTED_CC --version | head -1 | awk '{print $4}') | |
| echo "REAL_COMPILER_VER=$REAL_COMPILER_VER" >> "$GITHUB_ENV" | |
| # Azure ccache key shape: "ccache | PARENT_BRANCH | imageName-Compiler-VER-BuildType | BUILD_PARAMS_TYPE | <run id>" | |
| # run_id keeps nightly + dispatch + PR runs each save their own slot; | |
| # restore-keys falls back to the most-recent matching prefix. | |
| - name: ccache cache (Azure key shape; build_type-scoped) | |
| uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.2.0 | |
| with: | |
| path: /home/runner/ccache | |
| key: ccache-${{ env.PARENT_BRANCH }}-${{ env.IMAGE_NAME }}-${{ env.COMPILER }}-${{ env.BUILD_TYPE }}-${{ env.BUILD_PARAMS_TYPE }}-${{ github.run_id }} | |
| restore-keys: | | |
| ccache-${{ env.PARENT_BRANCH }}-${{ env.IMAGE_NAME }}-${{ env.COMPILER }}-${{ env.BUILD_TYPE }}-${{ env.BUILD_PARAMS_TYPE }}- | |
| - name: boost cache (Azure key shape) | |
| uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.2.0 | |
| with: | |
| path: /tmp/boost | |
| key: ${{ env.BOOST_VERSION }} | |
| - name: Checkout (fetchDepth 32 mirrors Azure) | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.2.2 | |
| with: | |
| fetch-depth: 32 | |
| # PS-11254: on pull_request_target the default checkout ref is the | |
| # base branch, so explicitly check out the fork PR head. Safe here: | |
| # build-arm64 holds no secrets (see header trust-split note). | |
| ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || '' }} | |
| - name: Update git submodules | |
| run: | | |
| set -eux | |
| git submodule sync | |
| git submodule update --init --force --depth=256 | |
| git submodule | |
| - name: Compiler and cmake info | |
| run: | | |
| set -eux | |
| $SELECTED_CC -v | |
| $SELECTED_CXX -v | |
| ccache --version | |
| mkdir -p "$CCACHE_DIR" "$BOOST_DIR" | |
| ccache -p | grep -E "max_size|cache_dir" | |
| ccache --zero-stats | |
| # Azure `*** cmake ...` step, 8.4 Linux non-inverted branch. | |
| - name: cmake (Azure parity, 8.4 Linux non-inverted) | |
| run: | | |
| set -eux | |
| mkdir bin && cd bin | |
| COMPILE_OPT=( | |
| -DCMAKE_C_FLAGS_DEBUG=-g1 | |
| -DCMAKE_CXX_FLAGS_DEBUG=-g1 | |
| ) | |
| CMAKE_OPT=" | |
| -DCMAKE_BUILD_TYPE=$BUILD_TYPE | |
| -DBUILD_CONFIG=mysql_release | |
| -DWITH_PACKAGE_FLAGS=OFF | |
| -DDOWNLOAD_BOOST=1 | |
| -DWITH_BOOST=$BOOST_DIR | |
| -DCMAKE_C_COMPILER=$SELECTED_CC | |
| -DCMAKE_CXX_COMPILER=$SELECTED_CXX | |
| -DCMAKE_C_COMPILER_LAUNCHER=ccache | |
| -DCMAKE_CXX_COMPILER_LAUNCHER=ccache | |
| -DWITH_ROCKSDB=ON | |
| -DWITH_COREDUMPER=ON | |
| -DWITH_COMPONENT_KEYRING_VAULT=ON | |
| -DWITH_PAM=ON | |
| -DMYSQL_MAINTAINER_MODE=ON | |
| -DWITH_MECAB=system | |
| -DWITH_NUMA=ON | |
| -DWITH_EDITLINE=system | |
| -DWITH_SYSTEM_LIBS=ON | |
| " | |
| cmake .. $CMAKE_OPT "${COMPILE_OPT[@]}" | |
| rm -f "$BOOST_DIR/$BOOST_VERSION.tar.gz" | |
| cmake -L . | |
| # Azure: `*** Compile` uses `make -j2` (MS-hosted has 2 cores). arm64 | |
| # deviation: scale to cax* cores, capped at 16 (Cirrus pattern). Literal | |
| # -j2 would waste cax41's 16 cores; arm64 builds benefit linearly from | |
| # parallelism (THE migration win). | |
| - name: Compile (make -j, capped at 16) | |
| run: | | |
| set -eux | |
| cd bin | |
| NPROC=$(nproc --all) | |
| NTHREADS=$(( NPROC > 16 ? 16 : NPROC )) | |
| echo "Using $NTHREADS/$NPROC threads" | |
| make -j${NTHREADS} | |
| ccache --show-stats | |
| df -h / | |
| # arm64-only step (Azure x86 does not run MTR; Cirrus arm64 does). | |
| # Suite picked by dispatch: | |
| # - main.1st (Debug, per-PR; Cirrus arm64 PR task parity) | |
| # - binlog_nogtid (RelWithDebInfo, nightly; Cirrus RelWithDebInfo parity, Przemek 2026-05-18) | |
| - name: MTR (suite per build_type) | |
| run: | | |
| set -eux | |
| cd bin | |
| NPROC=$(nproc --all) | |
| NTHREADS=$(( NPROC > 16 ? 16 : NPROC )) | |
| LIBEATMYDATA=$(whereis libeatmydata.so | awk '{print $2}') | |
| if [ "$MTR_SUITE" = "main.1st" ]; then | |
| mysql-test/mysql-test-run.pl main.1st \ | |
| --parallel=$NTHREADS \ | |
| --junit-output=/tmp/MTR_results.xml \ | |
| --mysqld-env=LD_PRELOAD=${LIBEATMYDATA} \ | |
| --force --max-test-fail=0 --retry-failure=0 \ | |
| --debug-server | |
| else | |
| mysql-test/mysql-test-run.pl \ | |
| --suite=$MTR_SUITE \ | |
| --parallel=$NTHREADS \ | |
| --junit-output=/tmp/MTR_results.xml \ | |
| --mysqld-env=LD_PRELOAD=${LIBEATMYDATA} \ | |
| --force --max-test-fail=0 --retry-failure=0 | |
| fi | |
| - name: Upload MTR results | |
| if: always() | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.0 | |
| with: | |
| name: mtr-${{ env.BUILD_TYPE }}-${{ env.MTR_SUITE }} | |
| path: /tmp/MTR_results.xml | |
| if-no-files-found: ignore | |
| # Renamed from `delete-runner` to `delete-runner-hetzner` in PS-11179. | |
| # Body otherwise unchanged. Only fires when the Hetzner provisioning path | |
| # actually created a VM. | |
| delete-runner-hetzner: | |
| needs: [dispatch, pick-target, create-runner-hetzner, build-arm64] | |
| # Skip delete when debug_keep_vm is set AND create-runner-hetzner failed, | |
| # so the broken VM stays alive for manual SSH diagnosis. orphan-sweep | |
| # reaps it after 6h regardless. Normal happy path is unaffected. | |
| # Also gated on provider==hetzner so this is a no-op on EC2 fallback runs. | |
| if: | | |
| always() | |
| && needs.pick-target.outputs.provider == 'hetzner' | |
| && needs.create-runner-hetzner.outputs.server_id != '' | |
| && !(inputs.debug_keep_vm == true && needs.create-runner-hetzner.result == 'failure') | |
| runs-on: ubuntu-latest | |
| permissions: {} | |
| steps: | |
| - uses: olexandr-havryliak/hcloud-github-runner@bb1089d8b718a06493cb37c51dfe596e44baefc2 | |
| with: | |
| mode: delete | |
| name: ${{ env.EPHEMERAL_RUNNER_NAME }} | |
| server_id: ${{ needs.create-runner-hetzner.outputs.server_id }} | |
| hcloud_token: ${{ secrets.GHA_RUNNER_HCLOUD_TOKEN }} | |
| github_token: ${{ secrets.GHA_RUNNER_PAT }} | |
| # PS-11179: Always-runs safety net for the EC2 fallback path. | |
| # | |
| # The instance self-terminates after the runner exits (userData does | |
| # `shutdown -h now` and we pass --instance-initiated-shutdown-behavior | |
| # terminate), so on the happy path TerminateInstances is idempotent / | |
| # no-op. This job exists for the failure cases: | |
| # - Workflow cancelled mid-build | |
| # - build-arm64 timed out | |
| # - userData crashed before reaching the shutdown line | |
| # - registration polling failed and bailed out of create-runner-aws | |
| # | |
| # Also de-registers the runner from GitHub so the next run does not | |
| # collide on the same EPHEMERAL_RUNNER_NAME. The runner usually self-removes | |
| # via `./run.sh --ephemeral`, but if it died before --ephemeral kicked in, | |
| # the orphan entry would block re-registration. | |
| # | |
| # Drops the create-runner-aws.outputs.instance_id != '' gate (P1-7 fix): | |
| # if the job was cancelled between RunInstances and `>> $GITHUB_OUTPUT`, | |
| # the output is empty but a live instance still exists. We discover by | |
| # tag (github_run_id == ${{ github.run_id }}) instead, then union with | |
| # the published output as a belt-and-braces measure. | |
| delete-runner-aws: | |
| needs: [dispatch, pick-target, create-runner-aws, build-arm64] | |
| if: | | |
| always() | |
| && needs.pick-target.outputs.provider == 'aws' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| id-token: write | |
| steps: | |
| - name: Configure AWS credentials via OIDC | |
| uses: aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a # v4.3.1 | |
| with: | |
| role-to-assume: ${{ secrets.AWS_ROLE_ARN }} | |
| role-session-name: gha-${{ github.run_id }}-${{ github.run_attempt }}-cleanup | |
| aws-region: ${{ env.AWS_REGION }} | |
| # Tag-based discovery first, then union with the published output. | |
| # Covers: (a) happy path (output populated; tag query agrees), | |
| # (b) cancelled mid-launch (output empty; tag query finds it), | |
| # (c) double-launch from a retry that escaped client-token (both | |
| # share the tag and both get terminated). | |
| - name: Terminate EC2 instance (safety net) | |
| env: | |
| PUBLISHED_INSTANCE_ID: ${{ needs.create-runner-aws.outputs.instance_id }} | |
| run: | | |
| set -euo pipefail | |
| IDS=$(aws ec2 describe-instances \ | |
| --region "$AWS_REGION" \ | |
| --filters \ | |
| "Name=tag:github_run_id,Values=${{ github.run_id }}" \ | |
| "Name=tag:github_run_attempt,Values=${{ github.run_attempt }}" \ | |
| "Name=tag:github_repository,Values=${{ github.repository }}" \ | |
| "Name=tag:iit-billing-tag,Values=percona-server-gha-fallback" \ | |
| "Name=instance-state-name,Values=pending,running,stopping,stopped" \ | |
| --query 'Reservations[].Instances[].InstanceId' --output text 2>/dev/null || true) | |
| # Union with whatever the create job published; dedupe via sort -u. | |
| ALL_IDS=$(printf '%s\n%s\n' "$IDS" "${PUBLISHED_INSTANCE_ID:-}" \ | |
| | tr '\t' '\n' | grep -E '^i-' | sort -u || true) | |
| if [ -z "$ALL_IDS" ]; then | |
| echo "No live EC2 instance found for run ${{ github.run_id }} attempt ${{ github.run_attempt }} (already terminated or never launched)" | |
| exit 0 | |
| fi | |
| echo "Terminating: $ALL_IDS" | |
| # || true: instance may race to terminated between describe and | |
| # terminate; either way we want this job to succeed so the | |
| # workflow status is not contaminated by cleanup-only errors. | |
| # shellcheck disable=SC2086 | |
| aws ec2 terminate-instances \ | |
| --region "$AWS_REGION" \ | |
| --instance-ids $ALL_IDS >/dev/null 2>&1 || true | |
| - name: De-register orphaned runner (if still present) | |
| env: | |
| GH_TOKEN: ${{ secrets.GHA_RUNNER_PAT }} | |
| run: | | |
| set -euo pipefail | |
| ID=$(gh api -X GET /repos/${{ github.repository }}/actions/runners --paginate \ | |
| --jq ".runners[] | select(.name == \"$EPHEMERAL_RUNNER_NAME\") | .id" || true) | |
| if [ -n "$ID" ]; then | |
| gh api -X DELETE "/repos/${{ github.repository }}/actions/runners/$ID" || true | |
| echo "Removed orphan runner $EPHEMERAL_RUNNER_NAME (id=$ID)" | |
| else | |
| echo "Runner $EPHEMERAL_RUNNER_NAME already de-registered (ephemeral self-removal)" | |
| fi |