Skip to content

Support release-branch targeting in Skia upstream sync (+ Opus 4.7, libc++ build fix)#4239

Merged
mattleibow merged 7 commits into
mainfrom
mattleibow-skia-sync-release-branch
Jun 25, 2026
Merged

Support release-branch targeting in Skia upstream sync (+ Opus 4.7, libc++ build fix)#4239
mattleibow merged 7 commits into
mainfrom
mattleibow-skia-sync-release-branch

Conversation

@mattleibow

@mattleibow mattleibow commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

What

Extends the daily Skia upstream sync automation (auto-skia-sync) so it can target a maintenance line — release/{major}.{milestone}.x — instead of only main, refactors the milestone/branch resolution out of inline workflow YAML into committed, testable scripts, runs the agent on Claude Opus 4.7, and fixes a native-build regression that broke the sync's Linux build.

Example: dispatching the workflow with milestone 148 now opens PRs against release/4.148.x in both mono/SkiaSharp and mono/skia, rather than main/skiasharp.

Why

Release lines need ongoing bug-fix syncs from upstream Skia, but the sync workflow only knew how to target main. The branch-resolution logic also lived as two large duplicated inline bash blocks in the workflow source, which was hard to read, review, and test.

How it works

Branch-line resolution (skia-sync-detect.sh, the single source of truth):

  • Picks the target milestone from the trigger mode (current/next/latest cron, or an explicit milestone dispatch input).
  • Only looks for a release line when the target is numerically older than main's milestone (the newest line is always served by main).
  • A maintenance line uses the same branch name in both repos (release/{major}.{milestone}.x); main ⇄ mono/skia skiasharp.
  • Fails fast if the matching mono/skia release branch is missing — release branches are owned by the separate release process, not this sync.
  • Hard-fails (rather than guessing) if a milestone matches multiple release branches.

Submodule alignment (skia-sync-align-submodule.sh): points externals/skia at the SHA the base branch records before the agent runs, since the agent checks out the workflow branch (usually main).

Refactor

The milestone/branch logic was extracted from two inline YAML bash blocks into committed scripts, collapsing the workflow run: blocks to ~1 and ~5 lines and removing the duplicated derivation:

  • pre_activation (--gate) runs the detector and writes job outputs; it now sparse-checks-out .github/scripts so it can call the script.
  • The agent's align step re-runs the same detector (without --gate) and sources its output — necessary because the agent job can't read pre_activation outputs in gh-aw.

The extraction also folds in two release-resolution review fixes: numeric -lt (was !=) and hard-fail on multi-match (was silent head -1).

A follow-up moved the cron→mode mapping out of the detector and into the workflow: the 7 AM / 12 PM / 5 PM → current / next / latest decision is now a single GitHub Actions expression next to where the crons are declared, and skia-sync-detect.sh takes real --mode/--milestone arguments instead of reading INPUT_MODE/INPUT_MILESTONE/SCHEDULE env vars. The script no longer knows anything about cron, so it is self-contained and unit-testable (--mode latest instead of faking SCHEDULE='0 17 * * *'). The free-form milestone input is still staged via env (not interpolated into run:) to avoid shell injection.

Model: Claude Opus 4.7

The workflow now pins the agent engine to claude-opus-4.7 via the engine.model block (was the gh-aw default Sonnet). Opus is materially more reliable on this multi-repo, submodule-heavy task — across validation it produced cleaner runs with no failed commands, retries, or self-introduced build hacks. Opus 4.6 and 4.7 carry the same AI-credit multiplier, so 4.7 is a free upgrade to the latest snapshot.

Native build fix (libc++)

The sync's pre-agent "Install native build dependencies" step now also installs libc++-dev libc++abi-dev.

Root cause: PR #4062 ("Migrate Linux Docker builds to .NET official cross-compilation images") moved the Linux native build into the mcr.microsoft.com/dotnet-buildtools/prereqs cross images, which ship Clang + libc++. native/linux/build.cake consequently hardcodes -stdlib=libc++ in its extra_cflags/extra_ldflags. The sync workflow builds on a bare ubuntu-latest host (not inside that Docker image), so libc++ was absent and the agent's dotnet cake --target=externals-linux failed to link. The agent could not self-remediate — there is no apt/sudo inside the AWF chroot, and the firewall blocks OS package mirrors — so it got stuck. Installing libc++ on the host (visible to the chroot via the shared filesystem) resolves it.

The agent prompt was also hardened to forbid patching native build files or compiler/linker flags and attempting package installs; if a host dependency is genuinely missing it must STOP and report it under "items needing human attention" rather than work around it.

Files

  • .github/scripts/skia-sync-detect.sh (new) — milestone/branch-line resolution + gate checks; takes --mode/--milestone args
  • .github/scripts/skia-sync-align-submodule.sh (new) — submodule alignment
  • .github/scripts/skia-sync-push-prs.sh — release-aware --base targeting and PR titles
  • .github/workflows/auto-skia-sync.md / .lock.yml — slimmed steps, new outputs, sparse checkout, cron→mode expression, engine.model: claude-opus-4.7, libc++ install, hardened prompt
  • .agents/skills/update-skia/SKILL.md — documents release-target mode

Note: release-branch skill changes are intentionally out of scope here and will land in a separate PR.

Validation

Static / unit:

  • gh aw compile auto-skia-sync → 0 errors (4 pre-existing warnings); lock verified to reflect the source (model in all 4 places, libc++ in the apt line, --mode/--milestone at both call sites, no stale INPUT_*/SCHEDULE).
  • bash -n clean on all scripts.
  • skia-sync-detect.sh exercised against stubbed gh/git: --mode current/next/latest, the default, explicit --milestone override, empty --milestone, and unknown-arg rejection all resolve as before; plus main-line vs release-line resolution, mono/skia-branch-missing fail, ambiguous-match fail, and missing-upstream fail.
  • Reviewed by GPT-5.5 and Opus 4.7; design and set -euo pipefail hazards confirmed sound.

Live end-to-end (dispatched against release/4.148.x, real upstream, real native build + tests):

Scenario Trigger Expected Result
explicit milestone, up to date milestone=150 skip ✅ gate SKIP (already merged), workflow green
latest cron mode mode=latest skip ✅ gate SKIP, workflow green
re-run same line milestone=148 (already synced) idempotent ✅ gate SKIP, existing PR untouched (no dup/close-reopen)
missing upstream milestone=151 (no chrome/m151 yet) hard fail ✅ pre_activation exits 1, nothing downstream runs, no PRs
full sync milestone=148, 2 commits behind PR to release/4.148.x ✅ libc++ installed (34s); native build clean in ~15 min with no build.cake hacks; 5544 tests pass / 0 fail / 172 skip; bug-fix-only PRs (cgmanifest hash + submodule pointer) opened on release/4.148.x in both repos

The full-sync run (28203686018) transcript was audited line-by-line: 0 failed commands, 0 retries, 0 self-introduced build hacks — the agent built, regenerated bindings (no-op), ran the full test suite, and committed only cgmanifest.json + the submodule pointer. Validation PRs were closed and their branches deleted after verification.

Co-authored-by: Matthew Leibowitz mattleibow@live.com
Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com

mattleibow and others added 2 commits June 23, 2026 19:39
Let the Skia sync workflow and update-skia skill target a
release/<major>.<milestone>.x line (in both mono/SkiaSharp and mono/skia)
instead of only main, derived automatically from the resolved milestone.

- auto-skia-sync.md: pre-activation detects the release line from the
  target milestone, exposes is_release/base_branch/skia_base_branch/
  head_branch, fails fast if the mono/skia release branch is missing, and
  adapts the skip-check; host submodule-align step and prompt body use the
  resolved base/head branches.
- skia-sync-push-prs.sh: consume HEAD_BRANCH/BASE_BRANCH/SKIA_BASE_BRANCH/
  IS_RELEASE and target the right base branches with release-aware titles.
- update-skia skill: document release-target mode (Phase 1 base detection,
  parametric Phase 4 + Phase 11, fail-if-missing mono/skia branch).

Creation of the matching mono/skia release branch is owned by the release
process and will be handled in a separate change to the release-branch skill.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Move the milestone/branch-line resolution out of the inline workflow YAML and
into two committed, testable scripts:

- skia-sync-detect.sh: the single source of truth for mode/target/base-branch
  resolution. Emits key=value to $SKIA_SYNC_OUT (default $GITHUB_OUTPUT); --gate
  adds the upstream-exists / already-merged checks. Run by both the pre_activation
  step (--gate, writes job outputs) and the agent align step (sourced, since that
  job can't read pre_activation outputs).
- skia-sync-align-submodule.sh: the submodule alignment, driven by the sourced
  base_branch / skia_base_branch.

The pre_activation job now sparse-checks-out .github/scripts so it can call the
script. This collapses the two large inline bash blocks to ~1 and ~5 lines and
removes the duplicated branch-resolution logic.

Behaviour is preserved for the main line and the single-match release path. The
extraction also folds in two release-resolution fixes from review that were not
yet in the previous commit: the strictly-older check is now a numeric compare
(`-lt` instead of `!=`), and a milestone matching multiple release branches now
hard-fails instead of silently picking the first via `head -1`.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

Copy link
Copy Markdown
Contributor

📦 Try the packages from this PR

Warning

Do not run these scripts without first reviewing the code in this PR.

Step 1 — Download the packages

bash / macOS / Linux:

curl -fsSL https://raw.githubusercontent.com/mono/SkiaSharp/main/scripts/get-skiasharp-pr.sh | bash -s -- 4239

PowerShell / Windows:

iex "& { $(irm https://raw.githubusercontent.com/mono/SkiaSharp/main/scripts/get-skiasharp-pr.ps1) } 4239"

Step 2 — Add the local NuGet source

dotnet nuget add source ~/.skiasharp/hives/pr-4239/packages --name skiasharp-pr-4239
More options
Option Description
--successful-only / -SuccessfulOnly Only use successful builds
--force / -Force Overwrite previously downloaded packages
--list / -List List available artifacts without downloading
--build-id ID / -BuildId ID Download from a specific build

Or download manually from Azure Pipelines — look for the nuget artifact on the build for this PR.

Remove the source when you're done:

dotnet nuget remove source skiasharp-pr-4239

@github-actions

Copy link
Copy Markdown
Contributor

📖 Documentation Preview

The documentation for this PR has been deployed and is available at:

🔗 View Staging Site
🔗 View Staging Docs
🔗 View Staging Gallery (Blazor)
🔗 View Staging Gallery (Uno Platform)
🔗 View Staging SkiaFiddle

This preview will be updated automatically when you push new commits to this PR.


This comment is automatically updated by the documentation staging workflow.

mattleibow and others added 3 commits June 25, 2026 22:03
Add an engine: block pinning model claude-opus-4.6 (overriding the default
claude-sonnet-4.6). Upstream merge/conflict resolution and native build-fix
reasoning are hard, so the stronger model is worth the higher AI-credit cost
for this workflow.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Opus 4.7 carries the same 5x AI-credit multiplier as 4.6, so move to the
newer model for the upstream merge/conflict-resolution reasoning.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
native/linux/build.cake builds libSkiaSharp/libHarfBuzzSharp with
-stdlib=libc++. On the real CI that runtime comes from the .NET
cross-compilation image, but this workflow builds on a bare Ubuntu host
where the agent cannot apt-install anything (no apt/sudo in the AWF
chroot, and the firewall blocks the OS package mirrors).

Install libc++-dev + libc++abi-dev in the host pre-agent step so the
unmodified native build compiles, and tell the agent not to patch native
build files or compiler flags to work around a missing dependency.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mattleibow mattleibow changed the title Support release-branch targeting in Skia upstream sync Support release-branch targeting in Skia upstream sync (+ Opus 4.7, libc++ build fix) Jun 25, 2026
mattleibow and others added 2 commits June 26, 2026 01:11
The cron→mode mapping (7 AM → current, 5 PM → latest, else → next) was the
only workflow-coupled knowledge inside skia-sync-detect.sh — it hardcoded the
literal cron strings and read mode/milestone/schedule from INPUT_MODE,
INPUT_MILESTONE and SCHEDULE env vars.

Move that mapping into the workflow, next to where the crons are declared, as a
single GitHub Actions expression. The detector now takes real --mode/--milestone
arguments and no longer knows anything about cron, so it is self-contained and
unit-testable (pass --mode latest instead of faking SCHEDULE='0 17 * * *').

mode is staged into an env var (not interpolated straight into run:) so the
free-form milestone dispatch input cannot inject shell — the same anti-injection
safety the previous env-based wiring had. Behavior is unchanged: validated that
--mode current/next/latest, the default, an explicit --milestone override, an
empty --milestone, and an unknown arg all resolve exactly as before.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Final review pass over the skia-sync scripts:

- Harden skia-sync-detect.sh arg parsing: a value-less --mode/--milestone now
  reports a clear error and exits 2 instead of tripping set -e on a double
  shift. Matters now that the detector is meant to be run by hand for testing.
- Flatten the multi-match release-branch list with paste instead of the
  $(echo $VAR) word-splitting idiom (SC2116/SC2086).
- Mark the runtime-generated skia-sync-env.sh source with a shellcheck
  source=/dev/null directive (SC1091).

All three scripts are now shellcheck-clean at style level; behavior verified
unchanged for every valid invocation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mattleibow
mattleibow merged commit c41b443 into main Jun 25, 2026
2 of 3 checks passed
@mattleibow
mattleibow deleted the mattleibow-skia-sync-release-branch branch June 25, 2026 23:26
mattleibow added a commit that referenced this pull request Jun 29, 2026
…ne skill, honest run signal (#4258)

Harden Skia upstream-sync: conflict policy, durable gn args, standalone skill, honest run signal (#4258)

Follow-up to the mode=main tip work (#4254) and release-line targeting
(#4239). The first real tip runs exposed three rough edges in the
auto-skia-sync automation, plus that key Skia-update knowledge lived only
in the workflow prompt where the update-skia skill couldn't reach it.

~~ Verify-upstream-or-reapply conflict resolution ~~

A tip merge runs ~291 commits ahead of our submodule base and conflicts on
every file carrying an [M1xx] fork cherry-pick. The old "keep ours / resolve
what you reasonably can" guidance risked silently dropping a not-yet-
upstreamed patch (--theirs) or freezing a stale form of one upstream had
since refined (--ours; e.g. our SDF-LCD patch used getMaxScale() while
upstream relanded it as sk_ieee_float_divide(1.f, getMinScale())).

Replace it with an explicit, auditable policy: classify each fork patch on a
conflicted file as upstreamed (take upstream's form, record the SHA) or not
upstreamed (re-apply ours on top) -- never a blanket --theirs/--ours, never
a silent drop. A mandatory before-merge fork-patch snapshot is cross-checked
so every patch lands in the resolution table as upstreamed or re-applied; a
patch that is neither is a lost patch -> stop and fix. Applied across gotcha
#15, SKILL.md Phase 5, and the workflow tip note.

~~ Allow a required new gn arg through build.cake ~~

The tip build needed skia_use_partition_alloc=false because upstream now
defaults it to is_clang while our DEPS deliberately omits partition_alloc;
Skia ships an official zero-overhead noop raw_ptr for exactly this case, so
=false is the supported embedder path, not a hack. The rule banned all
build.cake edits, forcing a one-off --gnArgs CLI flag and leaving durable
config non-durable.

Split the rule into two tiers: still forbid changing compiler/linker flags
(or scripts/infra/native/**) to silence a host build error, but allow adding
a genuinely required new upstream gn arg to the affected platforms'
native/**/build.cake gn-args lists, flagged in both PR summaries for cross-
platform review. Sequencing is honored: the toggle doesn't exist in m150, so
the relaxed rule lets the next milestone merge add it in-context rather than
breaking the current build. Recommendation for the eventual bump (gotcha #23):
keep partition_alloc disabled via the supported noop, not by vendoring the
Chromium allocator.

~~ Make the skill standalone; reduce the workflow to CI overrides ~~

main/tip mode and the required-new-gn-arg rule were documented only in the
workflow prompt, so the update-skia skill couldn't drive them locally. Move
both into the skill: tip mode becomes a first-class third option in Phase 1
(new {UPSTREAM_REF} variable, merge-base..upstream/main milestone diffs,
generalized Phase 5 merge command), and the gn-arg rule lands in Phase 7
backed by gotcha #23. Trim the workflow's duplicated build.cake/mode prose to
short pointers, keeping only genuinely CI-specific guardrails (can't apt-
install in the sandbox; a missing host dep is a workflow bug, not a flag hack;
Linux-only-build cross-platform review). These are prompt-body edits, so the
lock stays byte-identical.

~~ Stop mislabeling a real sync as a no-op ~~

A sync that merged upstream, built, tested, and opened both PRs was reported
by the gh-aw conclusion job as a no-op, which then failed trying to file a
no-op issue (the workflow has no issues: write). Cause: safe-outputs staged
mode blocks the agent from creating anything directly -- the real PRs are
opened by the host post-step skia-sync-push-prs.sh with the autobump token --
so noop was the agent's only completion signal and every run, real or not,
signalled noop.

Give the agent an honest signal: declare create-pull-request in safe-outputs,
kept staged (preview-only; the safe_outputs job is ubuntu-slim with
permissions:{} and never touches the workspace) so a real sync registers as a
pull-request output, while real PRs still come only from the post-step. Set
noop: report-as-issue: false so noop is reserved for genuine no-work runs and
no longer files an uncreatable issue; drop the unused create_issue offer; and
add a "Completion signal" section to the prompt. This is the one frontmatter
edit in the PR, so auto-skia-sync.lock.yml is regenerated.

Validated end-to-end by run 28269474099 (mode=main, real 2-commit merge): the
agent emitted a staged create_pull_request, the conclusion logged no noop with
no failed issue step, and the post-step still pushed skia-sync/main to both
repos and updated the real PRs (#4263 + #269). Rebased on main to pick up the
#4260 workflow rename (lock recompiled to a 0-diff) and confirm consistency
with the #4267 support-status model.

Co-authored-by: Matthew Leibowitz <mattleibow@live.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mattleibow mattleibow added this to the 4.150.0-rc.1 milestone Jun 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant