Skip to content

Pin the prewarmed renderer generation across rolling deploys #3385

Pin the prewarmed renderer generation across rolling deploys

Pin the prewarmed renderer generation across rolling deploys #3385

Workflow file for this run

name: ci-required
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled]
merge_group:
workflow_dispatch:
inputs:
pull_request_base_ref:
description: PR base branch for hosted-label cleanup rechecks.
required: false
type: string
pull_request_base_sha:
description: PR base SHA for hosted-label cleanup rechecks.
required: false
type: string
force_required_hosted_ci_recheck:
description: Re-run the required gate after hosted-label cleanup with hosted CI treated as not requested.
required: false
type: boolean
default: false
permissions:
actions: read
checks: read
contents: read
issues: read
concurrency:
group: >-
${{
github.event_name == 'pull_request' &&
(github.event.action == 'labeled' || github.event.action == 'unlabeled') &&
github.event.label.name != 'ready-for-hosted-ci' &&
github.event.label.name != 'force-full-hosted-ci' &&
format('ci-required-ignored-label-{0}', github.run_id) ||
format('ci-required-{0}', github.event.pull_request.number || github.ref)
}}
cancel-in-progress: >-
${{
github.event_name != 'pull_request' ||
(
(github.event.action != 'labeled' && github.event.action != 'unlabeled') ||
github.event.label.name == 'ready-for-hosted-ci' ||
github.event.label.name == 'force-full-hosted-ci'
)
}}
jobs:
required-pr-gate:
name: required-pr-gate
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 50
persist-credentials: false
- name: Check out pinned agent workflows
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
repository: shakacode/agent-workflows
ref: 1958648b70a450aa67c15b833428485d17021045
path: .agent-workflows-source
fetch-depth: 1
persist-credentials: false
- name: Validate pinned agent workflow copies
# yamllint disable rule:line-length
run: |
ruby .agents/bin/agent-workflow-drift-manifest-test.rb --source-root .agent-workflows-source
.agent-workflows-source/bin/check-agent-workflow-drift \
--manifest .agents/agent-workflow-drift.yml \
--source-root .agent-workflows-source \
--consumer-root .
# yamllint enable rule:line-length
- name: Validate workflow YAML
run: |
ruby -e "require 'yaml'; Dir['.github/workflows/*.{yml,yaml}'].sort.each { |f| YAML.safe_load_file(f, permitted_classes: [], aliases: false); puts f }"
- name: Validate CI scripts
run: |
for script in \
bin/ci-local \
bin/ci-rerun-failures \
bin/request-hosted-ci \
script/ci-changes-detector \
script/ci-required-diff-base-test.bash \
script/ci-required-hosted-gate
do
bash -n "$script"
done
ruby -c script/ci-required-merge-group-gate
- name: Run CI gate tests
run: |
bash script/ci-required-hosted-gate-test.bash
bash script/ci-required-diff-base-test.bash
bash script/lint-mirrored-blocks-test.bash
ruby script/ci_required_merge_group_gate_test.rb
node --test .github/workflows/ci-commands.test.cjs
node .github/workflows/hosted-ci-safety.test.cjs
ruby script/pr_merge_ledger_test.rb
ruby .agents/bin/agent_workflow_drift_manifest_test_test.rb
ruby .agents/skills/pr-batch/bin/pr-ci-readiness-test.rb
- name: Check mirrored blocks
run: ruby bin/lint-mirrored-blocks
- name: Fetch workflow dispatch base
if: github.event_name == 'workflow_dispatch'
env:
BASE_REF: ${{ github.event.inputs.pull_request_base_ref || 'main' }}
run: |
git fetch --no-tags --depth=50 origin "${BASE_REF}:refs/remotes/origin/${BASE_REF}"
- name: Select hosted CI mode
id: hosted-ci
uses: ./.github/actions/hosted-ci-selectors
with:
base-ref: ${{ github.event.inputs.pull_request_base_ref || '' }}
- name: Run changed-files detector
id: changes
env:
PULL_REQUEST_BASE_SHA: ${{ github.event.inputs.pull_request_base_sha || '' }}
PULL_REQUEST_HEAD_SHA: ${{ github.event.pull_request.head.sha || '' }}
# Keep this expression on one line. It is the merge-queue base-ref
# contract asserted by react_on_rails/spec/react_on_rails/ruby_version_support_spec.rb,
# which matches the raw file text, and a folded scalar would preserve
# the newlines rather than fold them (the continuation lines are more
# indented than the first).
EVENT_BASE_REF: ${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha || github.event.before || 'origin/main' }}
run: |
# For pull_request events actions/checkout resolves refs/pull/<n>/merge,
# so HEAD is the PR head merged into the CURRENT base tip. By contrast
# github.event.pull_request.base.sha is only refreshed when the PR is
# opened or synchronized, so on a branch that has not been pushed for a
# while it lags HEAD's own base parent. Diffing from that stale SHA folds
# every base-branch commit in between into the changed-file set, which is
# how a docs-only PR gets classified as a generator change and is then
# told to spend a hosted CI run it does not need (#4756).
#
# The merge commit's FIRST PARENT is the base commit the merge was
# actually computed against, so it is the only base that yields this PR's
# own diff. It is also always present locally (it is a parent of HEAD), so
# it cannot fall outside the shallow fetch window. bundle-size.yml derives
# its size baseline from the same first parent for the same reason.
base_ref="${EVENT_BASE_REF}"
base_source="event payload"
if [ -n "${PULL_REQUEST_BASE_SHA}" ]; then
base_ref="${PULL_REQUEST_BASE_SHA}"
base_source="workflow_dispatch input"
elif [ "${GITHUB_EVENT_NAME}" = "pull_request" ]; then
parent_line="$(git rev-list --parents -n 1 HEAD 2>/dev/null || true)"
read -ra parents <<< "$parent_line"
# parents[0] = HEAD, [1] = first parent (base side), [2] = second parent (PR head)
if [ "${#parents[@]}" -eq 3 ]; then
if [ -n "${PULL_REQUEST_HEAD_SHA}" ] && [ "${parents[2]}" != "${PULL_REQUEST_HEAD_SHA}" ]; then
# Fail closed: this is the one state where no base can save us.
#
# Mind the direction of the error. A stale base only ADDS
# base-branch commits to the diff, over-selecting suites and
# wasting runner minutes, which is survivable. Missing changes is
# not: run_generators can come back false and this required gate
# can pass without the hosted run those changes needed.
#
# When the merge ref is stale, HEAD's TREE does not contain the
# current head's changes at all, so every base is diffed against a
# tree that is already missing them. Choosing a wider base cannot
# recover a file that is not in HEAD. The only honest outcomes are
# to classify the reported head instead (a network fetch plus a
# tree that no longer matches the rest of this job) or to refuse to
# classify. A required gate should refuse.
#
# This is a genuinely different situation from the single-parent
# branch below, which continues with a warning. There, HEAD is the
# real PR head, so a wide base still yields a complete diff. Here it
# does not. The condition is a race against GitHub recomputing
# refs/pull/<n>/merge, so a re-run clears it.
echo "::error::Checked-out merge commit's second parent (${parents[2]}) is not the PR head reported by this event (${PULL_REQUEST_HEAD_SHA}). GitHub's merge ref is stale, so HEAD's tree cannot contain the current head's changes and no diff base can produce a trustworthy classification. Re-run this job so GitHub recomputes refs/pull/<n>/merge, or push to the branch again."
exit 1
fi
base_ref="${parents[1]}"
base_source="PR merge commit first parent"
else
# bundle-size.yml hard-fails on an unexpected merge structure. This gate
# runs on every PR, so it warns instead: a new hard-fail path here would
# block more PRs than the misclassification it guards against. The
# changed-file list printed below makes a widened diff obvious anyway.
echo "::warning::HEAD is not the expected two-parent PR merge commit, so the changed-file classification is computed from ${base_ref} and may include base-branch drift. If this PR is routed to suites it does not touch, rebase it on its base branch."
fi
fi
echo "Diff base: ${base_ref} (source: ${base_source})"
script/ci-changes-detector "$base_ref"
# Show the exact file set behind the classification above so a widened
# diff is visible at a glance. The detector has already fetched and
# deepened as needed, so these calls add no network work.
if diff_base="$(git merge-base "$base_ref" HEAD 2>/dev/null)"; then
# Capture the whole stream before truncating. Piping git diff straight
# into `head -50` lets head close the read end once it has its 50
# lines; git then takes SIGPIPE and exits 141, and because Actions runs
# run: steps under `bash --noprofile --norc -eo pipefail`, pipefail
# promotes that 141 to the pipeline's status and set -e fails this
# required gate. It would fire on large diffs, meaning the widened-diff
# case this output exists to expose. sed is used rather than head
# because sed reads its input to the end, so nothing can close the
# pipe early.
# The || guard is for the diagnostic, not for control flow. `set -e`
# DOES abort on a failed command substitution in a plain assignment
# (the masking people remember applies to declaration assignments such
# as `local x=$(false)`, not to this one), so the step would fail here
# either way. Without the guard it fails with a bare non-zero exit and
# git's own stderr; with it, the log names which refs the diff was
# between. Keep it for that message, not because the failure would
# otherwise pass unnoticed.
changed_files="$(git diff --name-only "$diff_base" HEAD)" || {
echo "::error::git diff failed for ${diff_base}..HEAD while listing changed files."
exit 1
}
if [ -z "$changed_files" ]; then
echo "Changed files the classification was computed from: none"
else
changed_total="$(printf '%s\n' "$changed_files" | wc -l | tr -d ' ')"
echo "Changed files the classification was computed from (${changed_total} total, showing up to 50):"
printf '%s\n' "$changed_files" | sed -n '1,50p'
fi
fi
- name: Enforce hosted CI for generator changes
env:
RUN_GENERATORS: ${{ steps.changes.outputs.run_generators }}
SHOULD_RUN_HOSTED_CI: >-
${{
github.event_name == 'workflow_dispatch' &&
github.event.inputs.force_required_hosted_ci_recheck == 'true' &&
'false' ||
steps.hosted-ci.outputs.should_run_hosted_ci
}}
FORCE_REQUIRED_HOSTED_CI_RECHECK: >-
${{
github.event_name == 'workflow_dispatch' &&
github.event.inputs.force_required_hosted_ci_recheck == 'true' &&
'true' ||
'false'
}}
run: |
script/ci-required-hosted-gate
- name: Enforce merge-group package JS minimum gate
env:
GITHUB_TOKEN: ${{ github.token }}
REQUIRE_PACKAGE_JS_BUILD_20: ${{ steps.changes.outputs.run_js_tests }}
run: ruby script/ci-required-merge-group-gate
- name: Lightweight repo sanity check
run: |
test -f Gemfile
test -f package.json
test -d .github/workflows