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
263 changes: 263 additions & 0 deletions .github/actions/inject-linked-client-pr/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
name: Inject linked miden-client PR
description: >
If the current PR's body contains a "Client PR: #N" marker, append a
[patch] block to Cargo.toml that points the miden-client dep at the
linked PR's head branch. Runs only on `pull_request` events (silent
no-op on `push`). On the first run per PR, posts a sticky comment
describing what was patched. Fails fast and loudly if the linked PR
is closed without merge.

Marker syntax (anywhere in the body, first match wins):
Client PR: #1234
Client PR: 0xMiden/miden-client#1234 (cross-repo / forks)

This action keeps Cargo.toml clean on disk for committers — they
never need to hand-edit a patch block. Local-dev parity is provided
by scripts/dev-with-client-pr.sh, which writes the same block
underneath a marked-out region you can clear with --clear.

inputs:
github-token:
description: Token for gh CLI calls (defaults to GITHUB_TOKEN).
required: false
default: ${{ github.token }}
comment:
description: >
Whether to post/update a sticky PR comment summarizing the patch.
Default false: this action is typically called from many cargo-using
jobs in the same workflow run, and posting the same comment from
each is wasteful. Pass `comment: true` from exactly one designated
job (the first one that runs cargo, conventionally `build-wasm`).
The sticky-comment header dedups across calls, so over-firing is
correct but spammy.
required: false
default: "false"

outputs:
patched:
description: "true if a patch was injected, false otherwise."
value: ${{ steps.parse.outputs.patched }}
client_pr:
description: The linked PR number, or empty.
value: ${{ steps.parse.outputs.num }}
client_repo:
description: The linked PR's repo (owner/repo), or empty.
value: ${{ steps.parse.outputs.repo }}
client_head_sha:
description: The linked PR's head SHA, or empty.
value: ${{ steps.parse.outputs.head_sha }}

runs:
using: composite
steps:
- id: parse
shell: bash
env:
GH_TOKEN: ${{ inputs.github-token }}
EVENT_NAME: ${{ github.event_name }}
# github-script can't pass body cleanly through outputs (newlines), so
# we read it from a file the API gives us via gh.
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
set -euo pipefail

# Skip on push / schedule / workflow_dispatch.
if [ "$EVENT_NAME" != "pull_request" ] && [ "$EVENT_NAME" != "pull_request_target" ]; then
echo "patched=false" >> "$GITHUB_OUTPUT"
echo "::notice title=inject-linked-client-pr::Skipping on event=$EVENT_NAME (only pull_request triggers patch injection)."
exit 0
fi

if [ -z "${PR_NUMBER:-}" ]; then
echo "patched=false" >> "$GITHUB_OUTPUT"
echo "::notice title=inject-linked-client-pr::No PR number in event payload."
exit 0
fi

# Pull the body via API (avoids context interpolation hazards with
# arbitrary user input in PR descriptions).
body=$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}" --jq '.body // ""')

# Marker: "Client PR:" optionally followed by "<owner>/<repo>" then "#N".
# First match wins; case-insensitive.
marker=$(printf '%s' "$body" \
| grep -ioE '^[[:space:]]*Client PR:[[:space:]]*([0-9a-zA-Z._-]+/[0-9a-zA-Z._-]+)?#[0-9]+' \
| head -1 || true)
if [ -z "$marker" ]; then
echo "patched=false" >> "$GITHUB_OUTPUT"
exit 0
fi

repo=$(printf '%s' "$marker" | grep -oE '[0-9a-zA-Z._-]+/[0-9a-zA-Z._-]+' | head -1 || echo "0xMiden/miden-client")
num=$(printf '%s' "$marker" | grep -oE '[0-9]+$')

# Resolve head + state.
meta=$(gh api "repos/${repo}/pulls/${num}" \
--jq '"\(.head.repo.owner.login)\t\(.head.repo.name)\t\(.head.ref)\t\(.head.sha)\t\(.state)\t\(.merged)"' 2>/dev/null \
|| { echo "::error title=inject-linked-client-pr::Failed to fetch ${repo}#${num}. Does the PR exist? Is GH_TOKEN scoped to read it?"; exit 1; })

IFS=$'\t' read -r head_owner head_repo head_ref head_sha state merged <<< "$meta"

# Closed without merge → loud failure. (A merged PR is acceptable —
# it just means the linked branch may be stale; cargo will resolve
# to the head sha which still exists.)
if [ "$state" != "OPEN" ] && [ "$merged" != "true" ]; then
{
echo "## ❌ Linked client PR is closed without merge"
echo
echo "PR description marker: \`Client PR: ${repo}#${num}\`"
echo
echo "[\`${repo}#${num}\`](https://github.com/${repo}/pull/${num}) is in state \`${state}\` (not merged)."
echo
echo "Either re-open the linked PR, point \`Client PR:\` at a different one, or remove the marker (and add the dep retarget by hand)."
} >> "$GITHUB_STEP_SUMMARY"
echo "::error title=Linked PR closed::${repo}#${num} is closed without merge."
exit 1
fi

echo "patched=true" >> "$GITHUB_OUTPUT"
echo "repo=${repo}" >> "$GITHUB_OUTPUT"
echo "num=${num}" >> "$GITHUB_OUTPUT"
echo "head_owner=${head_owner}" >> "$GITHUB_OUTPUT"
echo "head_repo=${head_repo}" >> "$GITHUB_OUTPUT"
echo "head_ref=${head_ref}" >> "$GITHUB_OUTPUT"
echo "head_sha=${head_sha}" >> "$GITHUB_OUTPUT"
echo "state=${state}" >> "$GITHUB_OUTPUT"
echo "merged=${merged}" >> "$GITHUB_OUTPUT"

- if: steps.parse.outputs.patched == 'true'
shell: bash
env:
REPO: ${{ steps.parse.outputs.repo }}
NUM: ${{ steps.parse.outputs.num }}
HEAD_OWNER: ${{ steps.parse.outputs.head_owner }}
HEAD_REPO: ${{ steps.parse.outputs.head_repo }}
HEAD_REF: ${{ steps.parse.outputs.head_ref }}
HEAD_SHA: ${{ steps.parse.outputs.head_sha }}
MERGED: ${{ steps.parse.outputs.merged }}
run: |
set -euo pipefail

# We rewrite the miden-client (and miden-client-sqlite-store, if
# present) dep lines IN PLACE rather than emitting a [patch] block.
#
# Why not [patch]? Cargo's [patch."<url>"] section needs the patch
# source to be DIFFERENT from the original source. Web-sdk's `next`
# pins `miden-client` to `https://github.com/0xMiden/miden-client.git`
# at branch=next; pointing the same URL at a different branch via
# [patch."<url>"] errors with `patches must point to different
# sources`. [patch.crates-io] also doesn't apply on `next` because
# the dep isn't a crates.io source there. In-place rewrite covers
# both branches uniformly.
#
# The block is bracketed by markers so it's clear at a glance that
# this Cargo.toml is in a CI-mutated state — and so a future
# cleanup can locate and revert it.
url="https://github.com/${HEAD_OWNER}/${HEAD_REPO}.git"
python3 <<PY
import re
path = "Cargo.toml"
url = "${url}"
ref = "${HEAD_REF}"
mark_begin = "# >>>>>>> linked-client-pr (auto-injected by .github/actions/inject-linked-client-pr) >>>>>>>"
mark_end = "# <<<<<<< linked-client-pr <<<<<<<"

with open(path) as f:
lines = f.readlines()

dep_re = re.compile(r'^(?P<name>miden-client(?:-sqlite-store)?)\s*=\s*(?P<rhs>.+)$')

captured = []
patched_lines = []
for ln in lines:
m = dep_re.match(ln)
if not m:
continue
captured.append(ln.rstrip('\n'))
df = ', default-features = false' if 'default-features = false' in m.group('rhs') else ''
patched_lines.append(f"{m.group('name'):<25} = {{ branch = \"{ref}\"{df}, git = \"{url}\" }}")

if not captured:
raise SystemExit("No miden-client dep line found in Cargo.toml — nothing to patch.")

out = []
inserted = False
for ln in lines:
if not inserted and dep_re.match(ln):
out.append(mark_begin + "\n")
out.append(f"# Source: PR description marker \"Client PR: ${REPO}#${NUM}\".\n")
out.append(f"# Pin tip: ${HEAD_SHA} on ${HEAD_OWNER}/${HEAD_REPO}@${HEAD_REF}.\n")
out.append(f"# Merged: ${MERGED}. Runner-local override; never committed.\n")
out.append("# Original lines (do not edit):\n")
for c in captured:
out.append("# " + c + "\n")
out.append(mark_end + "\n")
for p in patched_lines:
out.append(p + "\n")
inserted = True
continue
if dep_re.match(ln):
continue
out.append(ln)

with open(path, 'w') as f:
f.writelines(out)
PY

# cargo update -p X requires X to be in the dep tree. main only
# pins miden-client (crates.io until the rewrite above swaps it
# to git+branch); next adds miden-client-sqlite-store. Build the
# -p list dynamically so this works on both branches.
update_args="-p miden-client"
if cargo pkgid -p miden-client-sqlite-store >/dev/null 2>&1; then
update_args="$update_args -p miden-client-sqlite-store"
fi
# shellcheck disable=SC2086
cargo update $update_args --quiet

# Surface the patch state in the job summary regardless of comment-on/off.
{
echo "## 🔗 Linked client PR injected"
echo
echo "| Field | Value |"
echo "|---|---|"
echo "| Marker | \`Client PR: ${REPO}#${NUM}\` |"
echo "| Head | \`${HEAD_OWNER}/${HEAD_REPO}@${HEAD_REF}\` |"
echo "| Pin | \`${HEAD_SHA}\` |"
echo "| Merged upstream | \`${MERGED}\` |"
} >> "$GITHUB_STEP_SUMMARY"

# Comment lifecycle (only fires when comment=true, by convention from a
# single designated job per workflow run):
# - patched=true → upsert the sticky comment via the same `header`,
# so re-runs UPDATE the existing comment in place (no duplicates).
# - patched=false → DELETE any prior comment with the same header.
# This keeps the invariant "at most one linked-client-pr comment
# on any PR": if the marker is added then removed, the stale
# comment doesn't linger.
- if: inputs.comment == 'true' && steps.parse.outputs.patched == 'true'
uses: marocchino/sticky-pull-request-comment@52423e01640425a022ef5fd42c6fb5f633a02728 # v2.9.4
with:
header: linked-client-pr
message: |
🔗 **Linked client PR**: [`${{ steps.parse.outputs.repo }}#${{ steps.parse.outputs.num }}`](https://github.com/${{ steps.parse.outputs.repo }}/pull/${{ steps.parse.outputs.num }})

| Field | Value |
|---|---|
| Patched at | `${{ steps.parse.outputs.head_owner }}/${{ steps.parse.outputs.head_repo }}@${{ steps.parse.outputs.head_ref }}` |
| Pin (head sha) | `${{ steps.parse.outputs.head_sha }}` |
| Upstream state | `${{ steps.parse.outputs.state }}` (merged: `${{ steps.parse.outputs.merged }}`) |

This run is testing against the linked PR's head. The published artifact will use the canonical `miden-client` source — CI on `main`/`next` does **not** auto-patch.

Local-dev parity:
```
scripts/dev-with-client-pr.sh ${{ steps.parse.outputs.num }} # apply the same patch locally
scripts/dev-with-client-pr.sh --clear # remove it before commit
```

- if: inputs.comment == 'true' && steps.parse.outputs.patched != 'true'
uses: marocchino/sticky-pull-request-comment@52423e01640425a022ef5fd42c6fb5f633a02728 # v2.9.4
with:
header: linked-client-pr
delete: true
9 changes: 9 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,15 @@ jobs:
SCCACHE_GHA_ENABLED: "true"
steps:
- uses: actions/checkout@v6
# Auto-patch miden-client dep against any "Client PR: #N" marker in
# the PR description. This is the *one* job per workflow run that
# opts into posting the sticky PR comment about the patch — every
# other cargo-using job re-runs the action with comment=false (the
# default) so we keep the strict 0-or-1 comment invariant.
- name: Inject linked miden-client PR
uses: ./.github/actions/inject-linked-client-pr
with:
comment: "true"
- name: Cleanup large tools for build space
uses: ./.github/actions/cleanup-runner
- name: Install Rust toolchain
Expand Down
Loading
Loading