Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 73 additions & 33 deletions .agents/skills/update-skia/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,37 @@ E. Ship (Phase 11)
> **Note:** When this phase is pre-computed by the automated workflow, you still need to
> add the `upstream` remote and fetch `chrome/m{TARGET}` — Phase 5 depends on it.

> 🛑 **GATE**: Confirm current milestone, target milestone, and that upstream branch exists.
5. **Determine the base branch (`main` vs a release line).** Most updates target `main`
(newest, in-development line) with `skiasharp` as the mono/skia base. But when the target
milestone is **older** than `main`'s milestone AND a matching release branch exists, the
update targets that release line instead:

```bash
# SkiaSharp uses release/<major>.<milestone>.x (e.g. release/4.148.x for m148)
git ls-remote --heads origin "refs/heads/release/*.{TARGET}.x"
# mono/skia mirrors the SAME branch name
git -C externals/skia ls-remote --heads origin "refs/heads/release/*.{TARGET}.x"
```

| Variable | `main` target | release-line target |
|----------|---------------|---------------------|
| `{BASE_BRANCH}` (SkiaSharp) | `main` | `release/<major>.{TARGET}.x` |
| `{SKIA_BASE_BRANCH}` (mono/skia) | `skiasharp` | `release/<major>.{TARGET}.x` |
| `{HEAD_BRANCH}` (both repos) | `skia-sync/m{TARGET}` | `skia-sync/release-<major>.{TARGET}.x` |

A release-line update is always a **bug-fix-only sync** (`CURRENT == TARGET`): do NOT bump
the milestone/soname/nuget version in Phase 6 — only `cgmanifest.json`'s commit hash changes.

> 🛑 **The release branch must already exist in BOTH repos.** Branch *creation* is owned by
> the release process (`release-branch` skill), not this skill. If the SkiaSharp
> `release/<major>.{TARGET}.x` branch exists but the mono/skia one does NOT, **STOP and fail** —
> do not create it here. Ask a human to cut the mono/skia release branch first.

Use these `{BASE_BRANCH}` / `{SKIA_BASE_BRANCH}` / `{HEAD_BRANCH}` values everywhere below in
place of the hardcoded `main` / `skiasharp` / `skia-sync/m{TARGET}` defaults.

> 🛑 **GATE**: Confirm current milestone, target milestone, the base branch (main vs release line,
> with the mono/skia base branch confirmed to exist), and that the upstream branch exists.

### Phase 2: Breaking Change Analysis

Expand Down Expand Up @@ -197,27 +227,30 @@ milestone numbers and paste your breaking change analysis table. The default exp

> ⚠️ **This phase creates BOTH branches before making ANY changes.** You may be on a
> workflow branch, a feature branch, or a detached HEAD — none of which is the right base.
> You MUST branch from `origin/main` (parent) and `origin/skiasharp` (submodule).
> You MUST branch from `origin/{BASE_BRANCH}` (parent) and `origin/{SKIA_BASE_BRANCH}`
> (submodule). For a `main` update those are `origin/main` and `origin/skiasharp`; for a
> release-line update they are `origin/release/<major>.{TARGET}.x` in both repos
> (see Phase 1 step 5).

**Parent repo (SkiaSharp):**

1. **Fetch the latest `main`:**
1. **Fetch the latest base branch:**
```bash
git fetch origin main
git fetch origin {BASE_BRANCH}
```

2. **Create the feature branch from `origin/main`:**
2. **Create the feature branch from `origin/{BASE_BRANCH}`:**
```bash
git checkout -b skia-sync/m{TARGET} origin/main
git checkout -b {HEAD_BRANCH} origin/{BASE_BRANCH}
```
If the branch already exists on the remote, check it out instead:
```bash
git fetch origin skia-sync/m{TARGET} && git checkout skia-sync/m{TARGET}
git fetch origin {HEAD_BRANCH} && git checkout {HEAD_BRANCH}
```

3. **Verify you are on the correct branch and it is based on `origin/main`:**
3. **Verify you are on the correct branch and it is based on `origin/{BASE_BRANCH}`:**
```bash
git log --oneline -1 origin/main
git log --oneline -1 origin/{BASE_BRANCH}
git log --oneline -1 HEAD
```
These should show the same commit (or HEAD should be ahead by only your own commits).
Expand All @@ -229,34 +262,35 @@ milestone numbers and paste your breaking change analysis table. The default exp
cd externals/skia
```

5. **Align to the SHA that `origin/main` expects** (the submodule tracks the `skiasharp`
branch in mono/skia, NOT `main`):
5. **Align to the SHA that `origin/{BASE_BRANCH}` expects** (the submodule tracks
`{SKIA_BASE_BRANCH}` in mono/skia — `skiasharp` for a `main` update, or the matching
`release/<major>.{TARGET}.x` for a release-line update — NOT `main`):
```bash
MAIN_SUB_SHA=$(git -C ../.. ls-tree origin/main -- externals/skia | awk '{print $3}')
git fetch origin skiasharp
git checkout "$MAIN_SUB_SHA"
BASE_SUB_SHA=$(git -C ../.. ls-tree origin/{BASE_BRANCH} -- externals/skia | awk '{print $3}')
git fetch origin {SKIA_BASE_BRANCH}
git checkout "$BASE_SUB_SHA"
```
Verify this SHA is on `origin/skiasharp`:
Verify this SHA is on `origin/{SKIA_BASE_BRANCH}`:
```bash
git branch -r --contains "$MAIN_SUB_SHA" | grep 'origin/skiasharp'
git branch -r --contains "$BASE_SUB_SHA" | grep 'origin/{SKIA_BASE_BRANCH}'
```

6. **Create the submodule feature branch:**
```bash
git checkout -b skia-sync/m{TARGET}
git checkout -b {HEAD_BRANCH}
```

> ⚠️ **Do NOT skip the SHA alignment step (step 5).** If the submodule is at a different
> SHA than `origin/main` expects, the merge will produce phantom diffs — functions that
> already exist on `main` will appear as new or removed.
> SHA than `origin/{BASE_BRANCH}` expects, the merge will produce phantom diffs — functions that
> already exist on the base branch will appear as new or removed.

> 🛑 **GATE**: Both branches created. Verify:
> ```bash
> # In parent repo:
> git rev-parse --abbrev-ref HEAD # → skia-sync/m{TARGET}
> git merge-base HEAD origin/main # → should match origin/main tip
> git rev-parse --abbrev-ref HEAD # → {HEAD_BRANCH}
> git merge-base HEAD origin/{BASE_BRANCH} # → should match origin/{BASE_BRANCH} tip
> # In submodule:
> git -C externals/skia rev-parse --abbrev-ref HEAD # → skia-sync/m{TARGET}
> git -C externals/skia rev-parse --abbrev-ref HEAD # → {HEAD_BRANCH}
> ```

### Phase 5: Upstream Merge (mono/skia)
Expand Down Expand Up @@ -520,22 +554,28 @@ before the update can be considered complete. Do not create PRs with only smoke
> **Same-milestone bug-fix syncs:** When `CURRENT == TARGET`, only `cgmanifest.json`'s
> `commitHash`/`upstream_merge_commit` change. Use PR titles like
> `[skia-sync] Merge upstream chrome/m{TARGET} bug fixes` instead of milestone-bump titles.
> A release-line update is always such a sync.

> **Release-line targets:** Use the `{BASE_BRANCH}` / `{SKIA_BASE_BRANCH}` / `{HEAD_BRANCH}`
> values resolved in Phase 1 step 5. For a `main` update they are `main` / `skiasharp` /
> `skia-sync/m{TARGET}`; for a release-line update they are `release/<major>.{TARGET}.x` (both
> repos) / `skia-sync/release-<major>.{TARGET}.x`.

#### PR 1: mono/skia (submodule)

| Field | Value |
|-------|-------|
| Branch | `skia-sync/m{TARGET}` |
| Target | `skiasharp` |
| Branch | `{HEAD_BRANCH}` |
| Target | `{SKIA_BASE_BRANCH}` |
| Title | `[skia-sync] Merge upstream chrome/m{TARGET}` |

#### PR 2: mono/SkiaSharp (parent)

| Field | Value |
|-------|-------|
| Branch | `skia-sync/m{TARGET}` |
| Target | `main` |
| Title | `[skia-sync] Update skia to milestone {TARGET}` |
| Branch | `{HEAD_BRANCH}` |
| Target | `{BASE_BRANCH}` |
| Title | `[skia-sync] Update skia to milestone {TARGET}` (or `Merge upstream chrome/m{TARGET} bug fixes` when `CURRENT == TARGET`) |

**Submodule must point to the mono/skia PR branch.**

Expand All @@ -548,9 +588,9 @@ Both PRs must reference each other.

Before proceeding to merge, verify ALL of these:

- [ ] Branch names follow `skia-sync/m{TARGET}` convention in BOTH repos
- [ ] mono/skia PR targets `skiasharp` branch
- [ ] mono/SkiaSharp PR targets `main` branch
- [ ] Branch names follow `{HEAD_BRANCH}` convention in BOTH repos
- [ ] mono/skia PR targets `{SKIA_BASE_BRANCH}` branch
- [ ] mono/SkiaSharp PR targets `{BASE_BRANCH}` branch
- [ ] **SkiaSharp's `externals/skia` submodule points to the mono/skia PR branch** (`git submodule status`)
- [ ] `cgmanifest.json` updated with new commit hash, version, and chrome_milestone
- [ ] `scripts/VERSIONS.txt` updated (ALL version lines, not just milestone)
Expand All @@ -562,7 +602,7 @@ Before proceeding to merge, verify ALL of these:

#### Merge Sequence (CRITICAL)

1. Merge mono/skia PR first → creates new squashed SHA on `skiasharp`
1. Merge mono/skia PR first → creates new squashed SHA on `{SKIA_BASE_BRANCH}`
2. Fetch new SHA in SkiaSharp's submodule
3. Update submodule pointer, push to SkiaSharp PR branch
4. **Only then** merge SkiaSharp PR
Expand All @@ -572,12 +612,12 @@ Before proceeding to merge, verify ALL of these:
Before proceeding past each step, verify:

- [ ] mono/skia PR merged
- [ ] Fetched `skiasharp` branch to get new squashed SHA
- [ ] Fetched `{SKIA_BASE_BRANCH}` branch to get new squashed SHA
- [ ] Updated SkiaSharp submodule to new SHA (`cd externals/skia && git fetch origin && git checkout {new-sha}`)
- [ ] Pushed submodule update to SkiaSharp PR branch
- [ ] CI passes on updated SkiaSharp PR
- [ ] SkiaSharp PR merged
- [ ] **Submodule points to a commit on `skiasharp` branch** (not an orphaned branch commit)
- [ ] **Submodule points to a commit on `{SKIA_BASE_BRANCH}` branch** (not an orphaned branch commit)

> ❌ **NEVER** merge both PRs without updating the submodule in between.
> ❌ **NEVER** assume the submodule reference is correct after squash-merging mono/skia.
Expand Down
30 changes: 30 additions & 0 deletions .github/scripts/skia-sync-align-submodule.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/usr/bin/env bash
#
# skia-sync-align-submodule.sh — Point externals/skia at the SHA the base branch
# records, before the agent runs.
#
# The agent job checks out the workflow branch (usually main), so the submodule may
# sit at a different SHA than the base branch (`main` or a release line) expects. The
# submodule tracks `$skia_base_branch` in mono/skia (skiasharp for a main sync,
# release/<major>.<milestone>.x for a release sync), so the base-branch submodule SHA
# should be a commit on that branch.
#
# Requires (export these by sourcing skia-sync-detect.sh's output first):
# base_branch parent base branch (main or release/<major>.<ms>.x)
# skia_base_branch mono/skia base branch (skiasharp or release/<major>.<ms>.x)

set -euo pipefail

BASE_BRANCH="${base_branch:?base_branch not set — source skia-sync-detect.sh first}"
SKIA_BASE_BRANCH="${skia_base_branch:?skia_base_branch not set — source skia-sync-detect.sh first}"

echo "Aligning submodule to origin/${BASE_BRANCH} (mono/skia ${SKIA_BASE_BRANCH})"
git fetch origin "$BASE_BRANCH" 2>&1 || true
BASE_SUB_SHA=$(git ls-tree "origin/${BASE_BRANCH}" -- externals/skia | awk '{print $3}')
echo "origin/${BASE_BRANCH} submodule SHA: $BASE_SUB_SHA"
git -C externals/skia fetch origin "$SKIA_BASE_BRANCH" 2>&1
git -C externals/skia checkout "$BASE_SUB_SHA" 2>&1
echo "Verifying SHA is on ${SKIA_BASE_BRANCH} branch:"
git -C externals/skia branch -r --contains "$BASE_SUB_SHA" | grep -q "origin/${SKIA_BASE_BRANCH}" \
&& echo " ✅ SHA is on origin/${SKIA_BASE_BRANCH}" \
|| echo " ⚠️ SHA is NOT on origin/${SKIA_BASE_BRANCH} — submodule pointer may be stale"
167 changes: 167 additions & 0 deletions .github/scripts/skia-sync-detect.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
#!/usr/bin/env bash
#
# skia-sync-detect.sh — Resolve which Skia milestone and branch line the upstream
# sync targets. This is the SINGLE source of truth for branch resolution, shared by:
#
# - the pre_activation "Detect milestone" step → run with --gate; writes the job
# outputs consumed by the prompt and the activation gate.
# - the agent job "Align submodule" step → run without --gate and sourced,
# because that job cannot read pre_activation's outputs and must recompute them.
#
# Contract:
# - `key=value` lines (lowercase, matching the workflow's declared outputs) are
# appended to the file named by $SKIA_SYNC_OUT, defaulting to $GITHUB_OUTPUT.
# - All human-readable logs and ::error::/::notice:: workflow commands go to stdout
# (so GitHub renders annotations and the machine output stays clean for sourcing).
#
# Args:
# --mode <current|next|latest> Which milestone line to track. The workflow resolves
# this from the dispatch input or the triggering cron
# (the cron→mode mapping lives in the workflow, next to
# where the crons are declared). Defaults to `next`.
# --milestone <number> Exact milestone override; when set, mode becomes
# `explicit` and the target is this number.
# --gate Also run the gating checks (does upstream exist? is it
# already merged?) and emit `skip=true` / exit
# accordingly. Only pre_activation needs this.
#
# Inputs (env):
# GITHUB_REPOSITORY owner/repo of this SkiaSharp checkout (default GitHub env)
# GITHUB_REF ref whose scripts/VERSIONS.txt gives main's milestone
# GH_TOKEN token for `gh api`

set -euo pipefail

GATE=false
MODE=""
MILESTONE=""
while [ $# -gt 0 ]; do
case "$1" in
--gate) GATE=true; shift ;;
--mode)
[ $# -ge 2 ] || { echo "::error::skia-sync-detect.sh: --mode requires a value"; exit 2; }
MODE="$2"; shift 2 ;;
--milestone)
[ $# -ge 2 ] || { echo "::error::skia-sync-detect.sh: --milestone requires a value"; exit 2; }
MILESTONE="$2"; shift 2 ;;
*) echo "::error::skia-sync-detect.sh: unknown argument '$1'"; exit 2 ;;
esac
done
MODE="${MODE:-next}"

OUT="${SKIA_SYNC_OUT:-${GITHUB_OUTPUT:?SKIA_SYNC_OUT or GITHUB_OUTPUT must be set}}"
emit() { printf '%s=%s\n' "$1" "$2" >>"$OUT"; }

# Milestone number from scripts/VERSIONS.txt at the given ref (remote read, no checkout).
milestone_of() {
gh api "repos/${GITHUB_REPOSITORY}/contents/scripts/VERSIONS.txt?ref=$1" \
--jq '.content' | base64 -d | grep '^libSkiaSharp.*milestone' | awk '{print $NF}'
}

# -- Mode -------------------------------------------------------------
# Resolved by the caller (workflow): the cron→mode mapping lives in the workflow,
# next to where the crons are declared. Here it is simply a value in {current,next,latest}.
MAIN_MS=$(milestone_of "$GITHUB_REF")
NEXT=$((MAIN_MS + 1))
LATEST=$(git ls-remote --heads https://github.com/google/skia.git 'refs/heads/chrome/m*' \
| sed -n 's|.*refs/heads/chrome/m\([0-9]*\)$|\1|p' | sort -n | tail -1)

# -- Target -----------------------------------------------------------
if [ -n "$MILESTONE" ]; then
TARGET="$MILESTONE"; MODE="explicit"
elif [ "$MODE" = latest ]; then
TARGET="$LATEST"
elif [ "$MODE" = current ]; then
TARGET="$MAIN_MS"
else
TARGET="$NEXT"
fi

# -- Target line: `main` (newest) vs a release/<major>.<TARGET>.x maintenance line.
# A maintenance line lives under the SAME branch name in both mono/SkiaSharp and
# mono/skia. We only look for one when TARGET is strictly OLDER than main's milestone;
# the newest line is always served by `main`. The release branch itself is owned by
# the release process (release-branch skill), NOT this sync.
IS_RELEASE=false
BASE_BRANCH=main
SKIA_BASE_BRANCH=skiasharp
HEAD_BRANCH="skia-sync/m${TARGET}"
RELEASE_BRANCH=""
# `2>/dev/null` swallows the "integer expression expected" noise for a non-numeric
# TARGET, which then falls through to the main line.
if [ "$TARGET" -lt "$MAIN_MS" ] 2>/dev/null; then
RELEASE_BRANCH=$(git ls-remote --heads "https://github.com/${GITHUB_REPOSITORY}.git" \
"refs/heads/release/*.${TARGET}.x" \
| sed -n 's|.*refs/heads/\(release/[0-9][0-9.]*\.x\)$|\1|p' | sort -u)
# The glob can match more than one major (e.g. release/4.148.x and a stray
# release/14.148.x). Refuse to guess — fail so a human disambiguates.
if [ "$(printf '%s' "$RELEASE_BRANCH" | grep -c . || true)" -gt 1 ]; then
matches=$(echo "$RELEASE_BRANCH" | paste -sd' ' -)
echo "::error::Multiple release branches match milestone ${TARGET}: ${matches} — cannot disambiguate."
exit 1
fi
fi
if [ -n "$RELEASE_BRANCH" ]; then
IS_RELEASE=true
BASE_BRANCH="$RELEASE_BRANCH"
SKIA_BASE_BRANCH="$RELEASE_BRANCH"
HEAD_BRANCH="skia-sync/${RELEASE_BRANCH//\//-}"
# The matching mono/skia release branch MUST already exist.
if [ -z "$(git ls-remote --heads https://github.com/mono/skia.git "refs/heads/${SKIA_BASE_BRANCH}" | awk '{print $1}')" ]; then
echo "::error::mono/skia branch '${SKIA_BASE_BRANCH}' does not exist. Release branches are owned by the release process (release-branch skill) — create it before running a release sync for m${TARGET}."
exit 1
fi
fi

# `current` = milestone of the BASE branch we sync INTO:
# - main line → main's milestone
# - release → that line's milestone (== TARGET ⇒ a bug-fix-only sync)
if [ "$IS_RELEASE" = true ]; then
CURRENT=$(milestone_of "$BASE_BRANCH")
else
CURRENT="$MAIN_MS"
fi

emit mode "$MODE"
emit next "$NEXT"
emit latest "$LATEST"
emit target "$TARGET"
emit is_release "$IS_RELEASE"
emit base_branch "$BASE_BRANCH"
emit skia_base_branch "$SKIA_BASE_BRANCH"
emit head_branch "$HEAD_BRANCH"
emit current "$CURRENT"

echo "Resolved: m${TARGET} → ${BASE_BRANCH} (mode=${MODE}, base milestone=m${CURRENT}, latest=m${LATEST}, head=${HEAD_BRANCH}, release=${IS_RELEASE})"

# Branch derivation is all the agent job needs; gating is pre_activation-only.
$GATE || exit 0

# -- Gate: only spin up the (expensive) agent when there is new upstream work ----
UPSTREAM_SHA=$(git ls-remote https://github.com/google/skia.git "refs/heads/chrome/m${TARGET}" | awk '{print $1}')
if [ -z "$UPSTREAM_SHA" ]; then
echo "::notice::upstream/chrome/m${TARGET} does not exist yet"
[ "$MODE" = explicit ] && exit 1
emit skip true
exit 0
fi

# Compare upstream HEAD against the existing sync branch if present; otherwise, for a
# release line, against the release base branch. A main milestone bump has no sync
# branch yet and always has work, so it proceeds.
SYNC_SHA=$(git ls-remote https://github.com/mono/skia.git "refs/heads/${HEAD_BRANCH}" | awk '{print $1}')
COMPARE_REF=""
if [ -n "$SYNC_SHA" ]; then
COMPARE_REF="$HEAD_BRANCH"
elif [ "$IS_RELEASE" = true ]; then
COMPARE_REF="$SKIA_BASE_BRANCH"
fi
if [ -n "$COMPARE_REF" ]; then
BEHIND=$(gh api "repos/mono/skia/compare/${UPSTREAM_SHA}...${COMPARE_REF}" --jq '.behind_by' 2>/dev/null || echo unknown)
if [ "$BEHIND" = 0 ]; then
echo "::notice::chrome/m${TARGET} already fully merged into ${COMPARE_REF} (upstream HEAD: ${UPSTREAM_SHA:0:12}) — skipping"
emit skip true
exit 0
fi
echo "${COMPARE_REF} exists but is ${BEHIND} commits behind upstream"
fi
Loading
Loading