From 4efef6d71fbdb40b2e1b1e55edd4bff714b46cf8 Mon Sep 17 00:00:00 2001 From: Leigh <351529+leighmcculloch@users.noreply.github.com> Date: Tue, 25 Aug 2026 21:43:36 +0000 Subject: [PATCH 01/23] add auto-update workflow for protocol_version_default --- .../auto-update-protocol-version.yml | 83 ++++++++++++++ .github/workflows/auto-update-test.yml | 13 +++ .scripts/auto-update-protocol-version | 102 ++++++++++++++++++ 3 files changed, 198 insertions(+) create mode 100644 .github/workflows/auto-update-protocol-version.yml create mode 100755 .scripts/auto-update-protocol-version diff --git a/.github/workflows/auto-update-protocol-version.yml b/.github/workflows/auto-update-protocol-version.yml new file mode 100644 index 000000000..2710afb14 --- /dev/null +++ b/.github/workflows/auto-update-protocol-version.yml @@ -0,0 +1,83 @@ +name: Auto Update Protocol Version + +on: + schedule: + - cron: '30 20 * * *' # 12:30 PM Pacific + - cron: '30 1 * * *' # 5:30 PM Pacific + workflow_dispatch: + +jobs: + + check: + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + has_updates: ${{ steps.changes.outputs.found }} + steps: + - uses: actions/checkout@v4 + + - name: Update protocol_version_default + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: ./.scripts/auto-update-protocol-version + + - name: Check for changes + id: changes + run: | + if git diff --quiet images.json; then + echo "found=false" >> $GITHUB_OUTPUT + else + echo "found=true" >> $GITHUB_OUTPUT + fi + + - name: Upload artifact + if: steps.changes.outputs.found == 'true' + uses: actions/upload-artifact@v4 + with: + name: images + path: images.json + + create-pr: + runs-on: ubuntu-latest + needs: check + if: needs.check.outputs.has_updates == 'true' + permissions: + contents: write + pull-requests: write + steps: + - uses: actions/checkout@v4 + + - name: Download artifact + uses: actions/download-artifact@v4 + with: + name: images + + - name: Create Pull Request + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + hash=$(sha256sum images.json | cut -c1-16) + branch="auto-update-protocol-version/${hash}" + + # Check if PR already exists for this exact update + if gh pr list --head "$branch" --state open --json number --jq '.[0].number' | grep -q .; then + echo "PR already exists for this update" + exit 0 + fi + + git checkout -b "$branch" + git add images.json + git commit -m "Update protocol_version_default (${hash})" + git push -u origin "$branch" + + gh pr create \ + --title "Update protocol_version_default (${hash})" \ + --body "This PR was automatically generated by the auto-update-protocol-version workflow." \ + --base main \ + --head "$branch" + + gh pr merge --auto --squash "$branch" diff --git a/.github/workflows/auto-update-test.yml b/.github/workflows/auto-update-test.yml index 83b0c2bd4..cc3bd85f8 100644 --- a/.github/workflows/auto-update-test.yml +++ b/.github/workflows/auto-update-test.yml @@ -24,3 +24,16 @@ jobs: - name: Validate images.json run: jq empty images.json + + test-protocol-version: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Run auto-update-protocol-version + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: ./.scripts/auto-update-protocol-version + + - name: Validate images.json + run: jq empty images.json diff --git a/.scripts/auto-update-protocol-version b/.scripts/auto-update-protocol-version new file mode 100755 index 000000000..4e4e7c8be --- /dev/null +++ b/.scripts/auto-update-protocol-version @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 + +import json +import re +import subprocess +import sys + +# Auto-update script for the protocol_version_default field in images.json +# +# Usage: ./.scripts/auto-update-protocol-version [image-tag ...] +# +# For each image (all images by default, or only those named as arguments), +# sets config.protocol_version_default to the max ledger protocol version +# supported by the stellar-core the image builds. +# +# The supported version is read from stellar-core's source at the image's +# 'core' dep ref, so it works for every ref kind: release tags, branches +# (e.g. 'master') and commit SHAs. stellar-core defines it in +# src/main/Config.cpp as: +# +# uint32 const Config::CURRENT_LEDGER_PROTOCOL_VERSION = N +# #ifdef ENABLE_NEXT_PROTOCOL_VERSION_UNSAFE_FOR_PRODUCTION +# + 1 +# #endif +# ; +# +# Images whose core is built with --enable-next-protocol-version-unsafe-for-production +# support N + 1, matching that conditional. + +CONFIG_PATH = 'src/main/Config.cpp' +NEXT_PROTOCOL_FLAG = '--enable-next-protocol-version-unsafe-for-production' + +CURRENT_LEDGER_PROTOCOL_VERSION = re.compile( + r'CURRENT_LEDGER_PROTOCOL_VERSION\s*=\s*(?P\d+)' +) + +def main(): + with open('images.json', 'r') as f: + images = json.load(f) + + tags = sys.argv[1:] + + updated = False + for image in images: + if tags and image['tag'] not in tags: + continue + if update_image(image): + updated = True + + if updated: + with open('images.json', 'w') as f: + json.dump(images, f, indent=2) + f.write('\n') + +def update_image(image): + """Update image's protocol_version_default from stellar-core. Returns True if changed.""" + tag = image['tag'] + + core = next((dep for dep in image['deps'] if dep['name'] == 'core'), None) + if not core: + print(f"{tag}: skipping, no 'core' dep") + return False + + version = supported_protocol_version(core['repo'], core['ref']) + if version is None: + print(f"{tag}: skipping, could not read protocol version from {core['repo']}@{core['ref']}") + return False + + # Core built with the next-protocol flag supports one version beyond the + # source constant, matching the #ifdef in Config.cpp. + flags = core.get('options', {}).get('configure_flags', '') + if NEXT_PROTOCOL_FLAG in flags: + version += 1 + + current = image['config'].get('protocol_version_default') + if current == version: + print(f"{tag}: protocol_version_default matches core ({version})") + return False + + print(f"{tag}: protocol_version_default {current} -> {version}") + image['config']['protocol_version_default'] = version + return True + +def supported_protocol_version(repo, ref): + """Read CURRENT_LEDGER_PROTOCOL_VERSION from repo's Config.cpp at ref, or None.""" + result = subprocess.run( + ['gh', 'api', '-H', 'Accept: application/vnd.github.raw', + f'repos/{repo}/contents/{CONFIG_PATH}?ref={ref}'], + capture_output=True, text=True + ) + if result.returncode != 0: + print(f"Error: failed to read {CONFIG_PATH} from {repo}@{ref}: {result.stderr}", + file=sys.stderr) + return None + + match = CURRENT_LEDGER_PROTOCOL_VERSION.search(result.stdout) + if not match: + return None + return int(match['version']) + +if __name__ == '__main__': + main() From ec45dff92ed2090d47186284dfd5829fb58ca8be Mon Sep 17 00:00:00 2001 From: Leigh <351529+leighmcculloch@users.noreply.github.com> Date: Tue, 25 Aug 2026 22:13:23 +0000 Subject: [PATCH 02/23] derive protocol version from each component --- .scripts/auto-update-protocol-version | 128 +++++++++++++++++--------- 1 file changed, 84 insertions(+), 44 deletions(-) diff --git a/.scripts/auto-update-protocol-version b/.scripts/auto-update-protocol-version index 4e4e7c8be..b19c2845e 100755 --- a/.scripts/auto-update-protocol-version +++ b/.scripts/auto-update-protocol-version @@ -10,30 +10,25 @@ import sys # Usage: ./.scripts/auto-update-protocol-version [image-tag ...] # # For each image (all images by default, or only those named as arguments), -# sets config.protocol_version_default to the max ledger protocol version -# supported by the stellar-core the image builds. +# sets config.protocol_version_default to the highest ledger protocol version +# supported by ALL of its xdr, core, rpc and horizon components, i.e. the +# minimum of each component's own max supported version. # -# The supported version is read from stellar-core's source at the image's -# 'core' dep ref, so it works for every ref kind: release tags, branches -# (e.g. 'master') and commit SHAs. stellar-core defines it in -# src/main/Config.cpp as: +# Each component's max supported version is read from its source at the pinned +# dep ref, so it works for every ref kind: release tags, branches (e.g. 'main', +# 'master') and commit SHAs. # -# uint32 const Config::CURRENT_LEDGER_PROTOCOL_VERSION = N -# #ifdef ENABLE_NEXT_PROTOCOL_VERSION_UNSAFE_FOR_PRODUCTION -# + 1 -# #endif -# ; +# xdr rs-stellar-xdr Cargo.toml [package] version's major +# core stellar-core src/main/Config.cpp CURRENT_LEDGER_PROTOCOL_VERSION +# rpc stellar-rpc .../integrationtest/infrastructure/test.go MaxSupportedProtocolVersion +# horizon stellar-horizon internal/ingest/main.go MaxSupportedProtocolVersion # -# Images whose core is built with --enable-next-protocol-version-unsafe-for-production -# support N + 1, matching that conditional. +# stellar-core built with --enable-next-protocol-version-unsafe-for-production +# supports one version beyond its source constant, matching the #ifdef in +# Config.cpp; that flag is read from the image's core dep options. -CONFIG_PATH = 'src/main/Config.cpp' NEXT_PROTOCOL_FLAG = '--enable-next-protocol-version-unsafe-for-production' -CURRENT_LEDGER_PROTOCOL_VERSION = re.compile( - r'CURRENT_LEDGER_PROTOCOL_VERSION\s*=\s*(?P\d+)' -) - def main(): with open('images.json', 'r') as f: images = json.load(f) @@ -53,50 +48,95 @@ def main(): f.write('\n') def update_image(image): - """Update image's protocol_version_default from stellar-core. Returns True if changed.""" + """Set image's protocol_version_default to min over its components. Returns True if changed.""" tag = image['tag'] - - core = next((dep for dep in image['deps'] if dep['name'] == 'core'), None) - if not core: - print(f"{tag}: skipping, no 'core' dep") - return False - - version = supported_protocol_version(core['repo'], core['ref']) - if version is None: - print(f"{tag}: skipping, could not read protocol version from {core['repo']}@{core['ref']}") - return False - - # Core built with the next-protocol flag supports one version beyond the - # source constant, matching the #ifdef in Config.cpp. - flags = core.get('options', {}).get('configure_flags', '') - if NEXT_PROTOCOL_FLAG in flags: - version += 1 - + deps = {dep['name']: dep for dep in image['deps']} + + supported = {} + for name, extract in PROTOCOL_SOURCES.items(): + dep = deps.get(name) + if not dep: + print(f"{tag}: skipping, no '{name}' dep") + return False + version = extract(dep) + if version is None: + print(f"{tag}: skipping, could not read protocol version from {name} ({dep['repo']}@{dep['ref']})") + return False + supported[name] = version + + version = min(supported.values()) current = image['config'].get('protocol_version_default') if current == version: - print(f"{tag}: protocol_version_default matches core ({version})") + print(f"{tag}: protocol_version_default matches components ({version}) {supported}") return False - print(f"{tag}: protocol_version_default {current} -> {version}") + print(f"{tag}: protocol_version_default {current} -> {version} {supported}") image['config']['protocol_version_default'] = version return True -def supported_protocol_version(repo, ref): - """Read CURRENT_LEDGER_PROTOCOL_VERSION from repo's Config.cpp at ref, or None.""" +def read_source(dep, path): + """Read a file from the dep's repo at its ref via the GitHub API, or None.""" result = subprocess.run( ['gh', 'api', '-H', 'Accept: application/vnd.github.raw', - f'repos/{repo}/contents/{CONFIG_PATH}?ref={ref}'], + f"repos/{dep['repo']}/contents/{path}?ref={dep['ref']}"], capture_output=True, text=True ) if result.returncode != 0: - print(f"Error: failed to read {CONFIG_PATH} from {repo}@{ref}: {result.stderr}", + print(f"Error: failed to read {path} from {dep['repo']}@{dep['ref']}: {result.stderr}", file=sys.stderr) return None + return result.stdout - match = CURRENT_LEDGER_PROTOCOL_VERSION.search(result.stdout) +def xdr_version(dep): + """Major of the [package] version in rs-stellar-xdr's Cargo.toml.""" + text = read_source(dep, 'Cargo.toml') + if text is None: + return None + section = None + for line in text.splitlines(): + stripped = line.strip() + if stripped.startswith('[') and stripped.endswith(']'): + section = stripped[1:-1] + elif section == 'package': + match = re.match(r'version\s*=\s*"(\d+)', stripped) + if match: + return int(match.group(1)) + return None + +def core_version(dep): + """CURRENT_LEDGER_PROTOCOL_VERSION from stellar-core, plus the next-protocol flag.""" + text = read_source(dep, 'src/main/Config.cpp') + if text is None: + return None + match = re.search(r'CURRENT_LEDGER_PROTOCOL_VERSION\s*=\s*(\d+)', text) if not match: return None - return int(match['version']) + version = int(match.group(1)) + if NEXT_PROTOCOL_FLAG in dep.get('options', {}).get('configure_flags', ''): + version += 1 + return version + +def regex_version(path, pattern): + """An extractor that reads `path` and pulls an int out of the first `pattern` match.""" + def extract(dep): + text = read_source(dep, path) + if text is None: + return None + match = re.search(pattern, text) + return int(match.group(1)) if match else None + return extract + +# Component name -> function(dep) -> max supported protocol version (or None). +PROTOCOL_SOURCES = { + 'xdr': xdr_version, + 'core': core_version, + 'rpc': regex_version( + 'cmd/stellar-rpc/internal/integrationtest/infrastructure/test.go', + r'MaxSupportedProtocolVersion\s*=\s*(\d+)'), + 'horizon': regex_version( + 'internal/ingest/main.go', + r'MaxSupportedProtocolVersion\s+uint32\s*=\s*(\d+)'), +} if __name__ == '__main__': main() From 4fac7091dbf998b57da1f8e3bbbe57796735ddcd Mon Sep 17 00:00:00 2001 From: Leigh <351529+leighmcculloch@users.noreply.github.com> Date: Tue, 25 Aug 2026 23:26:51 +0000 Subject: [PATCH 03/23] revert auto-update-test additions --- .github/workflows/auto-update-test.yml | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/.github/workflows/auto-update-test.yml b/.github/workflows/auto-update-test.yml index cc3bd85f8..83b0c2bd4 100644 --- a/.github/workflows/auto-update-test.yml +++ b/.github/workflows/auto-update-test.yml @@ -24,16 +24,3 @@ jobs: - name: Validate images.json run: jq empty images.json - - test-protocol-version: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Run auto-update-protocol-version - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: ./.scripts/auto-update-protocol-version - - - name: Validate images.json - run: jq empty images.json From 864a696d37ee20ae2fb3199d42b9121929740ee8 Mon Sep 17 00:00:00 2001 From: Leigh <351529+leighmcculloch@users.noreply.github.com> Date: Tue, 25 Aug 2026 23:27:12 +0000 Subject: [PATCH 04/23] reorder functions to read top-down --- .scripts/auto-update-protocol-version | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/.scripts/auto-update-protocol-version b/.scripts/auto-update-protocol-version index b19c2845e..75f6af2c7 100755 --- a/.scripts/auto-update-protocol-version +++ b/.scripts/auto-update-protocol-version @@ -74,19 +74,6 @@ def update_image(image): image['config']['protocol_version_default'] = version return True -def read_source(dep, path): - """Read a file from the dep's repo at its ref via the GitHub API, or None.""" - result = subprocess.run( - ['gh', 'api', '-H', 'Accept: application/vnd.github.raw', - f"repos/{dep['repo']}/contents/{path}?ref={dep['ref']}"], - capture_output=True, text=True - ) - if result.returncode != 0: - print(f"Error: failed to read {path} from {dep['repo']}@{dep['ref']}: {result.stderr}", - file=sys.stderr) - return None - return result.stdout - def xdr_version(dep): """Major of the [package] version in rs-stellar-xdr's Cargo.toml.""" text = read_source(dep, 'Cargo.toml') @@ -138,5 +125,18 @@ PROTOCOL_SOURCES = { r'MaxSupportedProtocolVersion\s+uint32\s*=\s*(\d+)'), } +def read_source(dep, path): + """Read a file from the dep's repo at its ref via the GitHub API, or None.""" + result = subprocess.run( + ['gh', 'api', '-H', 'Accept: application/vnd.github.raw', + f"repos/{dep['repo']}/contents/{path}?ref={dep['ref']}"], + capture_output=True, text=True + ) + if result.returncode != 0: + print(f"Error: failed to read {path} from {dep['repo']}@{dep['ref']}: {result.stderr}", + file=sys.stderr) + return None + return result.stdout + if __name__ == '__main__': main() From 21e98ea2dbfa6e559a86b23946196ae8d299ef83 Mon Sep 17 00:00:00 2001 From: Leigh <351529+leighmcculloch@users.noreply.github.com> Date: Tue, 25 Aug 2026 23:31:53 +0000 Subject: [PATCH 05/23] move protocol sources map above extractors --- .scripts/auto-update-protocol-version | 40 +++++++++++++-------------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/.scripts/auto-update-protocol-version b/.scripts/auto-update-protocol-version index 75f6af2c7..b8ba4bcbc 100755 --- a/.scripts/auto-update-protocol-version +++ b/.scripts/auto-update-protocol-version @@ -74,6 +74,18 @@ def update_image(image): image['config']['protocol_version_default'] = version return True +# Component name -> function(dep) -> max supported protocol version (or None). +PROTOCOL_SOURCES = { + 'xdr': lambda dep: xdr_version(dep), + 'core': lambda dep: core_version(dep), + 'rpc': lambda dep: regex_version(dep, + 'cmd/stellar-rpc/internal/integrationtest/infrastructure/test.go', + r'MaxSupportedProtocolVersion\s*=\s*(\d+)'), + 'horizon': lambda dep: regex_version(dep, + 'internal/ingest/main.go', + r'MaxSupportedProtocolVersion\s+uint32\s*=\s*(\d+)'), +} + def xdr_version(dep): """Major of the [package] version in rs-stellar-xdr's Cargo.toml.""" text = read_source(dep, 'Cargo.toml') @@ -103,27 +115,13 @@ def core_version(dep): version += 1 return version -def regex_version(path, pattern): - """An extractor that reads `path` and pulls an int out of the first `pattern` match.""" - def extract(dep): - text = read_source(dep, path) - if text is None: - return None - match = re.search(pattern, text) - return int(match.group(1)) if match else None - return extract - -# Component name -> function(dep) -> max supported protocol version (or None). -PROTOCOL_SOURCES = { - 'xdr': xdr_version, - 'core': core_version, - 'rpc': regex_version( - 'cmd/stellar-rpc/internal/integrationtest/infrastructure/test.go', - r'MaxSupportedProtocolVersion\s*=\s*(\d+)'), - 'horizon': regex_version( - 'internal/ingest/main.go', - r'MaxSupportedProtocolVersion\s+uint32\s*=\s*(\d+)'), -} +def regex_version(dep, path, pattern): + """Read `path` from the dep and pull an int out of the first `pattern` match.""" + text = read_source(dep, path) + if text is None: + return None + match = re.search(pattern, text) + return int(match.group(1)) if match else None def read_source(dep, path): """Read a file from the dep's repo at its ref via the GitHub API, or None.""" From fa6f9b32dfc7e5807e6a12c08c7b88f4186bee48 Mon Sep 17 00:00:00 2001 From: Leigh <351529+leighmcculloch@users.noreply.github.com> Date: Tue, 25 Aug 2026 23:51:12 +0000 Subject: [PATCH 06/23] read rpc protocol from bundled core version --- .scripts/auto-update-protocol-version | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/.scripts/auto-update-protocol-version b/.scripts/auto-update-protocol-version index b8ba4bcbc..185f1b73c 100755 --- a/.scripts/auto-update-protocol-version +++ b/.scripts/auto-update-protocol-version @@ -20,7 +20,7 @@ import sys # # xdr rs-stellar-xdr Cargo.toml [package] version's major # core stellar-core src/main/Config.cpp CURRENT_LEDGER_PROTOCOL_VERSION -# rpc stellar-rpc .../integrationtest/infrastructure/test.go MaxSupportedProtocolVersion +# rpc stellar-rpc .github/workflows/stellar-rpc.yml core_deb_version's major # horizon stellar-horizon internal/ingest/main.go MaxSupportedProtocolVersion # # stellar-core built with --enable-next-protocol-version-unsafe-for-production @@ -78,12 +78,8 @@ def update_image(image): PROTOCOL_SOURCES = { 'xdr': lambda dep: xdr_version(dep), 'core': lambda dep: core_version(dep), - 'rpc': lambda dep: regex_version(dep, - 'cmd/stellar-rpc/internal/integrationtest/infrastructure/test.go', - r'MaxSupportedProtocolVersion\s*=\s*(\d+)'), - 'horizon': lambda dep: regex_version(dep, - 'internal/ingest/main.go', - r'MaxSupportedProtocolVersion\s+uint32\s*=\s*(\d+)'), + 'rpc': lambda dep: rpc_version(dep), + 'horizon': lambda dep: horizon_version(dep), } def xdr_version(dep): @@ -115,6 +111,23 @@ def core_version(dep): version += 1 return version +def rpc_version(dep): + """Major of the stellar-core deb the RPC bundles, from its CI workflow. + + stellar-rpc pins the captive-core build it runs against via core_deb_version + in .github/workflows/stellar-rpc.yml (one entry per tested protocol); the + highest of those cores' major versions is the top protocol the RPC supports.""" + text = read_source(dep, '.github/workflows/stellar-rpc.yml') + if text is None: + return None + majors = [int(m) for m in re.findall(r"core_deb_version:\s*['\"]?(\d+)", text)] + return max(majors, default=None) + +def horizon_version(dep): + """MaxSupportedProtocolVersion from stellar-horizon's ingest package.""" + return regex_version(dep, 'internal/ingest/main.go', + r'MaxSupportedProtocolVersion\s+uint32\s*=\s*(\d+)') + def regex_version(dep, path, pattern): """Read `path` from the dep and pull an int out of the first `pattern` match.""" text = read_source(dep, path) From 436c36d58fe23eaec37f89da9fcfa17ef688c315 Mon Sep 17 00:00:00 2001 From: Leigh <351529+leighmcculloch@users.noreply.github.com> Date: Wed, 26 Aug 2026 00:09:22 +0000 Subject: [PATCH 07/23] inline single-use map and next-protocol flag --- .scripts/auto-update-protocol-version | 28 ++++++++++++--------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/.scripts/auto-update-protocol-version b/.scripts/auto-update-protocol-version index 185f1b73c..f0eeb2426 100755 --- a/.scripts/auto-update-protocol-version +++ b/.scripts/auto-update-protocol-version @@ -22,12 +22,6 @@ import sys # core stellar-core src/main/Config.cpp CURRENT_LEDGER_PROTOCOL_VERSION # rpc stellar-rpc .github/workflows/stellar-rpc.yml core_deb_version's major # horizon stellar-horizon internal/ingest/main.go MaxSupportedProtocolVersion -# -# stellar-core built with --enable-next-protocol-version-unsafe-for-production -# supports one version beyond its source constant, matching the #ifdef in -# Config.cpp; that flag is read from the image's core dep options. - -NEXT_PROTOCOL_FLAG = '--enable-next-protocol-version-unsafe-for-production' def main(): with open('images.json', 'r') as f: @@ -49,11 +43,18 @@ def main(): def update_image(image): """Set image's protocol_version_default to min over its components. Returns True if changed.""" + # Component name -> function(dep) -> max supported protocol version (or None). + sources = { + 'xdr': xdr_version, + 'core': core_version, + 'rpc': rpc_version, + 'horizon': horizon_version, + } tag = image['tag'] deps = {dep['name']: dep for dep in image['deps']} supported = {} - for name, extract in PROTOCOL_SOURCES.items(): + for name, extract in sources.items(): dep = deps.get(name) if not dep: print(f"{tag}: skipping, no '{name}' dep") @@ -74,14 +75,6 @@ def update_image(image): image['config']['protocol_version_default'] = version return True -# Component name -> function(dep) -> max supported protocol version (or None). -PROTOCOL_SOURCES = { - 'xdr': lambda dep: xdr_version(dep), - 'core': lambda dep: core_version(dep), - 'rpc': lambda dep: rpc_version(dep), - 'horizon': lambda dep: horizon_version(dep), -} - def xdr_version(dep): """Major of the [package] version in rs-stellar-xdr's Cargo.toml.""" text = read_source(dep, 'Cargo.toml') @@ -107,7 +100,10 @@ def core_version(dep): if not match: return None version = int(match.group(1)) - if NEXT_PROTOCOL_FLAG in dep.get('options', {}).get('configure_flags', ''): + # stellar-core built with this flag supports one version beyond its source + # constant, matching the #ifdef in Config.cpp; read it from the core dep options. + next_flag = '--enable-next-protocol-version-unsafe-for-production' + if next_flag in dep.get('options', {}).get('configure_flags', ''): version += 1 return version From b38ef5c413b66b5079afa051eaf44a08e30722fd Mon Sep 17 00:00:00 2001 From: Leigh <351529+leighmcculloch@users.noreply.github.com> Date: Wed, 26 Aug 2026 00:14:32 +0000 Subject: [PATCH 08/23] key protocol-update branch on resolved component shas --- .../auto-update-protocol-version.yml | 23 ++++++++++-- .scripts/resolve-image-refs | 36 +++++++++++++++++++ 2 files changed, 56 insertions(+), 3 deletions(-) create mode 100755 .scripts/resolve-image-refs diff --git a/.github/workflows/auto-update-protocol-version.yml b/.github/workflows/auto-update-protocol-version.yml index 2710afb14..1a71c8562 100644 --- a/.github/workflows/auto-update-protocol-version.yml +++ b/.github/workflows/auto-update-protocol-version.yml @@ -60,10 +60,14 @@ jobs: git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - hash=$(sha256sum images.json | cut -c1-16) + # Resolve every dep ref to the commit SHA it currently points to, and + # key the branch on that. A downstream component change yields a new + # hash -> a new PR, instead of deduping onto the existing one. + resolved=$(./.scripts/resolve-image-refs) + hash=$(printf '%s' "$resolved" | sha256sum | cut -c1-16) branch="auto-update-protocol-version/${hash}" - # Check if PR already exists for this exact update + # Skip only if a PR for this exact component set is already open. if gh pr list --head "$branch" --state open --json number --jq '.[0].number' | grep -q .; then echo "PR already exists for this update" exit 0 @@ -74,9 +78,22 @@ jobs: git commit -m "Update protocol_version_default (${hash})" git push -u origin "$branch" + { + echo "This PR was automatically generated by the auto-update-protocol-version workflow." + echo + echo "
" + echo "Resolved images.json" + echo + echo '~~~json' + echo "$resolved" + echo '~~~' + echo + echo "
" + } > pr-body.md + gh pr create \ --title "Update protocol_version_default (${hash})" \ - --body "This PR was automatically generated by the auto-update-protocol-version workflow." \ + --body-file pr-body.md \ --base main \ --head "$branch" diff --git a/.scripts/resolve-image-refs b/.scripts/resolve-image-refs new file mode 100755 index 000000000..129371192 --- /dev/null +++ b/.scripts/resolve-image-refs @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 + +# Prints images.json with every dep 'ref' resolved to the commit SHA it points +# to right now, so a protocol-update PR can record the exact component commits +# that protocol_version_default was derived from. +# +# Usage: ./.scripts/resolve-image-refs + +import json +import subprocess +import sys + +def resolve_ref(repo, ref): + """Commit SHA that repo's ref points to, or the original ref if it can't be read.""" + result = subprocess.run( + ['gh', 'api', f'repos/{repo}/commits/{ref}', '--jq', '.sha'], + capture_output=True, text=True + ) + if result.returncode != 0: + print(f"Error: failed to resolve {repo}@{ref}: {result.stderr}", file=sys.stderr) + return ref + return result.stdout.strip() + +def main(): + with open('images.json', 'r') as f: + images = json.load(f) + + for image in images: + for dep in image['deps']: + dep['ref'] = resolve_ref(dep['repo'], dep['ref']) + + json.dump(images, sys.stdout, indent=2) + sys.stdout.write('\n') + +if __name__ == '__main__': + main() From 4ea208b627cb406c77e4cd9e060daed316e79ec4 Mon Sep 17 00:00:00 2001 From: Leigh <351529+leighmcculloch@users.noreply.github.com> Date: Wed, 26 Aug 2026 12:03:05 +0000 Subject: [PATCH 09/23] run protocol auto-update once a day --- .github/workflows/auto-update-protocol-version.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/auto-update-protocol-version.yml b/.github/workflows/auto-update-protocol-version.yml index 1a71c8562..7617bb64b 100644 --- a/.github/workflows/auto-update-protocol-version.yml +++ b/.github/workflows/auto-update-protocol-version.yml @@ -2,7 +2,6 @@ name: Auto Update Protocol Version on: schedule: - - cron: '30 20 * * *' # 12:30 PM Pacific - cron: '30 1 * * *' # 5:30 PM Pacific workflow_dispatch: From 082af2df1f2b28c859fc0fd3bd90a6a731b18400 Mon Sep 17 00:00:00 2001 From: Leigh <351529+leighmcculloch@users.noreply.github.com> Date: Wed, 26 Aug 2026 12:17:05 +0000 Subject: [PATCH 10/23] resolve refs once and feed compute and pr --- .../auto-update-protocol-version.yml | 27 ++++++++----- .scripts/auto-update-protocol-version | 39 ++++++++++++++++--- 2 files changed, 52 insertions(+), 14 deletions(-) diff --git a/.github/workflows/auto-update-protocol-version.yml b/.github/workflows/auto-update-protocol-version.yml index 7617bb64b..f580fb024 100644 --- a/.github/workflows/auto-update-protocol-version.yml +++ b/.github/workflows/auto-update-protocol-version.yml @@ -16,10 +16,18 @@ jobs: steps: - uses: actions/checkout@v4 + # Resolve every dep ref to a commit SHA once, up front, so the protocol + # values and the SHAs the PR records are computed from the same commits + # (a tracked branch could otherwise move between steps). + - name: Resolve component refs + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: ./.scripts/resolve-image-refs > images.resolved.json + - name: Update protocol_version_default env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: ./.scripts/auto-update-protocol-version + run: ./.scripts/auto-update-protocol-version --refs images.resolved.json - name: Check for changes id: changes @@ -35,7 +43,9 @@ jobs: uses: actions/upload-artifact@v4 with: name: images - path: images.json + path: | + images.json + images.resolved.json create-pr: runs-on: ubuntu-latest @@ -56,13 +66,10 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - - # Resolve every dep ref to the commit SHA it currently points to, and - # key the branch on that. A downstream component change yields a new - # hash -> a new PR, instead of deduping onto the existing one. - resolved=$(./.scripts/resolve-image-refs) + # Key the branch on the resolved snapshot (commit SHAs) computed in the + # check job. A downstream component change yields a new hash -> a new + # PR, instead of deduping onto the existing one. + resolved=$(cat images.resolved.json) hash=$(printf '%s' "$resolved" | sha256sum | cut -c1-16) branch="auto-update-protocol-version/${hash}" @@ -72,6 +79,8 @@ jobs: exit 0 fi + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" git checkout -b "$branch" git add images.json git commit -m "Update protocol_version_default (${hash})" diff --git a/.scripts/auto-update-protocol-version b/.scripts/auto-update-protocol-version index f0eeb2426..f24dbfdde 100755 --- a/.scripts/auto-update-protocol-version +++ b/.scripts/auto-update-protocol-version @@ -7,7 +7,7 @@ import sys # Auto-update script for the protocol_version_default field in images.json # -# Usage: ./.scripts/auto-update-protocol-version [image-tag ...] +# Usage: ./.scripts/auto-update-protocol-version [--refs resolved.json] [image-tag ...] # # For each image (all images by default, or only those named as arguments), # sets config.protocol_version_default to the highest ledger protocol version @@ -18,22 +18,44 @@ import sys # dep ref, so it works for every ref kind: release tags, branches (e.g. 'main', # 'master') and commit SHAs. # +# With --refs, component sources are read at the refs in the given resolved +# images.json (produced by resolve-image-refs) rather than the refs in +# images.json, and that file's protocol_version_default is updated in place too. +# This keeps the protocol values and the exact commits a PR records consistent, +# even if a tracked branch moves between steps. +# # xdr rs-stellar-xdr Cargo.toml [package] version's major # core stellar-core src/main/Config.cpp CURRENT_LEDGER_PROTOCOL_VERSION # rpc stellar-rpc .github/workflows/stellar-rpc.yml core_deb_version's major # horizon stellar-horizon internal/ingest/main.go MaxSupportedProtocolVersion def main(): + args = sys.argv[1:] + refs_path = None + if args and args[0] == '--refs': + refs_path, args = args[1], args[2:] + tags = args + with open('images.json', 'r') as f: images = json.load(f) - tags = sys.argv[1:] + # Compute against the refs in refs_path (commit SHAs resolved up front) when + # given, otherwise against images.json's own refs. Either way the computed + # protocol_version_default is written back into images.json (refs untouched). + if refs_path: + with open(refs_path, 'r') as f: + source = json.load(f) + else: + source = images + by_tag = {image['tag']: image for image in images} updated = False - for image in images: - if tags and image['tag'] not in tags: + for src in source: + if tags and src['tag'] not in tags: continue - if update_image(image): + if update_image(src): + by_tag[src['tag']]['config']['protocol_version_default'] = \ + src['config']['protocol_version_default'] updated = True if updated: @@ -41,6 +63,13 @@ def main(): json.dump(images, f, indent=2) f.write('\n') + # Sync the resolved snapshot so it records the exact state the PR proposes: + # the resolved commit SHAs plus the resulting protocol_version_default. + if refs_path: + with open(refs_path, 'w') as f: + json.dump(source, f, indent=2) + f.write('\n') + def update_image(image): """Set image's protocol_version_default to min over its components. Returns True if changed.""" # Component name -> function(dep) -> max supported protocol version (or None). From ce26e556121406e6a940f749fd69c26c1d1cfb19 Mon Sep 17 00:00:00 2001 From: Leigh <351529+leighmcculloch@users.noreply.github.com> Date: Wed, 26 Aug 2026 12:44:39 +0000 Subject: [PATCH 11/23] require resolved file for protocol compute --- .../auto-update-protocol-version.yml | 2 +- .scripts/auto-update-protocol-version | 48 ++++++++----------- 2 files changed, 22 insertions(+), 28 deletions(-) diff --git a/.github/workflows/auto-update-protocol-version.yml b/.github/workflows/auto-update-protocol-version.yml index f580fb024..bed20ecad 100644 --- a/.github/workflows/auto-update-protocol-version.yml +++ b/.github/workflows/auto-update-protocol-version.yml @@ -27,7 +27,7 @@ jobs: - name: Update protocol_version_default env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: ./.scripts/auto-update-protocol-version --refs images.resolved.json + run: ./.scripts/auto-update-protocol-version images.resolved.json - name: Check for changes id: changes diff --git a/.scripts/auto-update-protocol-version b/.scripts/auto-update-protocol-version index f24dbfdde..ae7480e58 100755 --- a/.scripts/auto-update-protocol-version +++ b/.scripts/auto-update-protocol-version @@ -7,22 +7,21 @@ import sys # Auto-update script for the protocol_version_default field in images.json # -# Usage: ./.scripts/auto-update-protocol-version [--refs resolved.json] [image-tag ...] +# Usage: ./.scripts/auto-update-protocol-version [image-tag ...] # # For each image (all images by default, or only those named as arguments), # sets config.protocol_version_default to the highest ledger protocol version # supported by ALL of its xdr, core, rpc and horizon components, i.e. the # minimum of each component's own max supported version. # -# Each component's max supported version is read from its source at the pinned -# dep ref, so it works for every ref kind: release tags, branches (e.g. 'main', -# 'master') and commit SHAs. +# Component sources are read at the commit SHAs in +# (produced by resolve-image-refs), never at the branch refs in images.json, so +# the protocol values match the exact commits a PR will record. images.json is +# updated in place with the resulting protocol_version_default (refs untouched), +# and 's protocol_version_default is synced to match. # -# With --refs, component sources are read at the refs in the given resolved -# images.json (produced by resolve-image-refs) rather than the refs in -# images.json, and that file's protocol_version_default is updated in place too. -# This keeps the protocol values and the exact commits a PR records consistent, -# even if a tracked branch moves between steps. +# resolve-image-refs handles every ref kind: release tags, branches (e.g. 'main', +# 'master') and commit SHAs all resolve to a commit SHA. # # xdr rs-stellar-xdr Cargo.toml [package] version's major # core stellar-core src/main/Config.cpp CURRENT_LEDGER_PROTOCOL_VERSION @@ -31,26 +30,22 @@ import sys def main(): args = sys.argv[1:] - refs_path = None - if args and args[0] == '--refs': - refs_path, args = args[1], args[2:] - tags = args + if not args: + print("Usage: auto-update-protocol-version [image-tag ...]", + file=sys.stderr) + sys.exit(1) + resolved_path, tags = args[0], args[1:] with open('images.json', 'r') as f: images = json.load(f) - - # Compute against the refs in refs_path (commit SHAs resolved up front) when - # given, otherwise against images.json's own refs. Either way the computed - # protocol_version_default is written back into images.json (refs untouched). - if refs_path: - with open(refs_path, 'r') as f: - source = json.load(f) - else: - source = images + with open(resolved_path, 'r') as f: + resolved = json.load(f) by_tag = {image['tag']: image for image in images} + # Compute against the resolved commit SHAs, then write the resulting + # protocol_version_default into images.json (refs untouched). updated = False - for src in source: + for src in resolved: if tags and src['tag'] not in tags: continue if update_image(src): @@ -65,10 +60,9 @@ def main(): # Sync the resolved snapshot so it records the exact state the PR proposes: # the resolved commit SHAs plus the resulting protocol_version_default. - if refs_path: - with open(refs_path, 'w') as f: - json.dump(source, f, indent=2) - f.write('\n') + with open(resolved_path, 'w') as f: + json.dump(resolved, f, indent=2) + f.write('\n') def update_image(image): """Set image's protocol_version_default to min over its components. Returns True if changed.""" From 5cb03bf659136485cb56704a535c0b6d1286c7cf Mon Sep 17 00:00:00 2001 From: Leigh <351529+leighmcculloch@users.noreply.github.com> Date: Wed, 26 Aug 2026 12:55:33 +0000 Subject: [PATCH 12/23] resolve images via existing inherit and extras scripts --- .../auto-update-protocol-version.yml | 13 ++++--- .scripts/auto-update-protocol-version | 18 +++++----- .scripts/resolve-image-refs | 36 ------------------- 3 files changed, 16 insertions(+), 51 deletions(-) delete mode 100755 .scripts/resolve-image-refs diff --git a/.github/workflows/auto-update-protocol-version.yml b/.github/workflows/auto-update-protocol-version.yml index bed20ecad..c8631d520 100644 --- a/.github/workflows/auto-update-protocol-version.yml +++ b/.github/workflows/auto-update-protocol-version.yml @@ -16,13 +16,16 @@ jobs: steps: - uses: actions/checkout@v4 - # Resolve every dep ref to a commit SHA once, up front, so the protocol - # values and the SHAs the PR records are computed from the same commits - # (a tracked branch could otherwise move between steps). - - name: Resolve component refs + # Resolve inherits and every dep ref to a commit SHA once, up front, so the + # protocol values and the SHAs the PR records are computed from the same + # commits (a tracked branch could otherwise move between steps). + - name: Resolve images env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: ./.scripts/resolve-image-refs > images.resolved.json + run: | + set -o pipefail + ./.scripts/images-resolve-inherits < images.json \ + | ./.scripts/images-with-extras > images.resolved.json - name: Update protocol_version_default env: diff --git a/.scripts/auto-update-protocol-version b/.scripts/auto-update-protocol-version index ae7480e58..1ea6f429b 100755 --- a/.scripts/auto-update-protocol-version +++ b/.scripts/auto-update-protocol-version @@ -14,14 +14,12 @@ import sys # supported by ALL of its xdr, core, rpc and horizon components, i.e. the # minimum of each component's own max supported version. # -# Component sources are read at the commit SHAs in -# (produced by resolve-image-refs), never at the branch refs in images.json, so -# the protocol values match the exact commits a PR will record. images.json is -# updated in place with the resulting protocol_version_default (refs untouched), -# and 's protocol_version_default is synced to match. -# -# resolve-image-refs handles every ref kind: release tags, branches (e.g. 'main', -# 'master') and commit SHAs all resolve to a commit SHA. +# Component sources are read at each dep's resolved 'sha' in +# (produced by piping images.json through images-resolve-inherits and +# images-with-extras), never at the branch refs in images.json, so the protocol +# values match the exact commits a PR will record. images.json is updated in +# place with the resulting protocol_version_default (its refs untouched), and +# 's protocol_version_default is synced to match. # # xdr rs-stellar-xdr Cargo.toml [package] version's major # core stellar-core src/main/Config.cpp CURRENT_LEDGER_PROTOCOL_VERSION @@ -156,10 +154,10 @@ def regex_version(dep, path, pattern): return int(match.group(1)) if match else None def read_source(dep, path): - """Read a file from the dep's repo at its ref via the GitHub API, or None.""" + """Read a file from the dep's repo at its resolved sha via the GitHub API, or None.""" result = subprocess.run( ['gh', 'api', '-H', 'Accept: application/vnd.github.raw', - f"repos/{dep['repo']}/contents/{path}?ref={dep['ref']}"], + f"repos/{dep['repo']}/contents/{path}?ref={dep['sha']}"], capture_output=True, text=True ) if result.returncode != 0: diff --git a/.scripts/resolve-image-refs b/.scripts/resolve-image-refs deleted file mode 100755 index 129371192..000000000 --- a/.scripts/resolve-image-refs +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env python3 - -# Prints images.json with every dep 'ref' resolved to the commit SHA it points -# to right now, so a protocol-update PR can record the exact component commits -# that protocol_version_default was derived from. -# -# Usage: ./.scripts/resolve-image-refs - -import json -import subprocess -import sys - -def resolve_ref(repo, ref): - """Commit SHA that repo's ref points to, or the original ref if it can't be read.""" - result = subprocess.run( - ['gh', 'api', f'repos/{repo}/commits/{ref}', '--jq', '.sha'], - capture_output=True, text=True - ) - if result.returncode != 0: - print(f"Error: failed to resolve {repo}@{ref}: {result.stderr}", file=sys.stderr) - return ref - return result.stdout.strip() - -def main(): - with open('images.json', 'r') as f: - images = json.load(f) - - for image in images: - for dep in image['deps']: - dep['ref'] = resolve_ref(dep['repo'], dep['ref']) - - json.dump(images, sys.stdout, indent=2) - sys.stdout.write('\n') - -if __name__ == '__main__': - main() From 020862c82f805609c39156f3b746569eea84e430 Mon Sep 17 00:00:00 2001 From: Leigh <351529+leighmcculloch@users.noreply.github.com> Date: Wed, 26 Aug 2026 12:59:16 +0000 Subject: [PATCH 13/23] split image resolution into debuggable steps --- .../auto-update-protocol-version.yml | 35 +++++++++++++++---- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/.github/workflows/auto-update-protocol-version.yml b/.github/workflows/auto-update-protocol-version.yml index c8631d520..32d01649b 100644 --- a/.github/workflows/auto-update-protocol-version.yml +++ b/.github/workflows/auto-update-protocol-version.yml @@ -16,16 +16,37 @@ jobs: steps: - uses: actions/checkout@v4 - # Resolve inherits and every dep ref to a commit SHA once, up front, so the - # protocol values and the SHAs the PR records are computed from the same - # commits (a tracked branch could otherwise move between steps). - - name: Resolve images + # Resolve inherits and every dep ref to a commit SHA up front, in separate + # steps each printing its input/diff/output, so the protocol values and the + # SHAs a PR records come from the same commits (a tracked branch could + # otherwise move between steps) and each stage is easy to debug. + - name: Images Input + run: | + echo "Input:" + < images.json jq -C + echo "images=$(< images.json jq -c)" >> $GITHUB_ENV + + - name: Images with Inherits Resolved + run: | + images_before="$images" + images="$(<<< "$images" ./.scripts/images-resolve-inherits)" + echo "Diff:" + diff -u --color=always <(<<< "$images_before" jq) <(<<< "$images" jq) || true + echo "Output:" + <<< "$images" jq -C + echo "images=$images" >> $GITHUB_ENV + + - name: Images with Extras env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - set -o pipefail - ./.scripts/images-resolve-inherits < images.json \ - | ./.scripts/images-with-extras > images.resolved.json + images_before="$images" + images="$(<<< "$images" ./.scripts/images-with-extras)" + echo "Diff:" + diff -u --color=always <(<<< "$images_before" jq) <(<<< "$images" jq) || true + echo "Output:" + <<< "$images" jq -C + echo "$images" > images.resolved.json - name: Update protocol_version_default env: From b2d735cf73c61731188b32cc632bf2019a9b2603 Mon Sep 17 00:00:00 2001 From: Leigh <351529+leighmcculloch@users.noreply.github.com> Date: Wed, 26 Aug 2026 13:24:13 +0000 Subject: [PATCH 14/23] temporarily run protocol workflow on prs to test --- .github/workflows/auto-update-protocol-version.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/auto-update-protocol-version.yml b/.github/workflows/auto-update-protocol-version.yml index 32d01649b..1b9524ce5 100644 --- a/.github/workflows/auto-update-protocol-version.yml +++ b/.github/workflows/auto-update-protocol-version.yml @@ -4,6 +4,7 @@ on: schedule: - cron: '30 1 * * *' # 5:30 PM Pacific workflow_dispatch: + pull_request: # TEMPORARY: exercise the workflow on this PR; remove before merge jobs: @@ -74,7 +75,7 @@ jobs: create-pr: runs-on: ubuntu-latest needs: check - if: needs.check.outputs.has_updates == 'true' + if: needs.check.outputs.has_updates == 'true' && github.event_name != 'pull_request' # TEMPORARY: never open a PR during the test run permissions: contents: write pull-requests: write From 43cd80194bfa8aa024a09c30e40a9a0fff1fbc97 Mon Sep 17 00:00:00 2001 From: Leigh <351529+leighmcculloch@users.noreply.github.com> Date: Wed, 26 Aug 2026 13:26:05 +0000 Subject: [PATCH 15/23] stop running protocol workflow on prs --- .github/workflows/auto-update-protocol-version.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/auto-update-protocol-version.yml b/.github/workflows/auto-update-protocol-version.yml index 1b9524ce5..32d01649b 100644 --- a/.github/workflows/auto-update-protocol-version.yml +++ b/.github/workflows/auto-update-protocol-version.yml @@ -4,7 +4,6 @@ on: schedule: - cron: '30 1 * * *' # 5:30 PM Pacific workflow_dispatch: - pull_request: # TEMPORARY: exercise the workflow on this PR; remove before merge jobs: @@ -75,7 +74,7 @@ jobs: create-pr: runs-on: ubuntu-latest needs: check - if: needs.check.outputs.has_updates == 'true' && github.event_name != 'pull_request' # TEMPORARY: never open a PR during the test run + if: needs.check.outputs.has_updates == 'true' permissions: contents: write pull-requests: write From 1f3cc8ef164a9c851d06b11f032590ddaf324326 Mon Sep 17 00:00:00 2001 From: Leigh <351529+leighmcculloch@users.noreply.github.com> Date: Wed, 26 Aug 2026 13:45:11 +0000 Subject: [PATCH 16/23] derive next images as released protocol + 1 --- .scripts/auto-update-protocol-version | 87 ++++++++++++++++----------- 1 file changed, 53 insertions(+), 34 deletions(-) diff --git a/.scripts/auto-update-protocol-version b/.scripts/auto-update-protocol-version index 1ea6f429b..4a46cba9d 100755 --- a/.scripts/auto-update-protocol-version +++ b/.scripts/auto-update-protocol-version @@ -10,9 +10,15 @@ import sys # Usage: ./.scripts/auto-update-protocol-version [image-tag ...] # # For each image (all images by default, or only those named as arguments), -# sets config.protocol_version_default to the highest ledger protocol version -# supported by ALL of its xdr, core, rpc and horizon components, i.e. the -# minimum of each component's own max supported version. +# sets config.protocol_version_default to the highest ledger protocol version it +# supports: +# +# - Released images: the minimum over the max supported version of each of its +# xdr, core, rpc and horizon components. +# - "Next" images (core built with the unsafe next-protocol flag, e.g. +# 'future' and 'nightly-next'): one beyond the highest released image. Their +# pre-release component pins report lagging release versions that would +# otherwise understate the protocol they exist to exercise. # # Component sources are read at each dep's resolved 'sha' in # (produced by piping images.json through images-resolve-inherits and @@ -26,6 +32,8 @@ import sys # rpc stellar-rpc .github/workflows/stellar-rpc.yml core_deb_version's major # horizon stellar-horizon internal/ingest/main.go MaxSupportedProtocolVersion +NEXT_PROTOCOL_FLAG = '--enable-next-protocol-version-unsafe-for-production' + def main(): args = sys.argv[1:] if not args: @@ -40,16 +48,38 @@ def main(): resolved = json.load(f) by_tag = {image['tag']: image for image in images} - # Compute against the resolved commit SHAs, then write the resulting - # protocol_version_default into images.json (refs untouched). + # Max supported protocol per released (non-next) image, then the highest of + # those: the protocol the released set is on. Next images target one beyond. + supported = {image['tag']: supported_versions(image) + for image in resolved if not is_next(image)} + released = max((min(s.values()) for s in supported.values() if s is not None), + default=None) + updated = False for src in resolved: - if tags and src['tag'] not in tags: + tag = src['tag'] + if tags and tag not in tags: + continue + + if is_next(src): + if released is None: + print(f"{tag}: skipping, no released image to derive the next protocol from") + continue + version, detail = released + 1, f"[next: released {released} + 1]" + else: + if supported[tag] is None: + continue # supported_versions already reported why + version, detail = min(supported[tag].values()), supported[tag] + + target = by_tag[tag] + current = target['config'].get('protocol_version_default') + if current == version: + print(f"{tag}: protocol_version_default matches ({version}) {detail}") continue - if update_image(src): - by_tag[src['tag']]['config']['protocol_version_default'] = \ - src['config']['protocol_version_default'] - updated = True + print(f"{tag}: protocol_version_default {current} -> {version} {detail}") + target['config']['protocol_version_default'] = version + src['config']['protocol_version_default'] = version + updated = True if updated: with open('images.json', 'w') as f: @@ -62,8 +92,14 @@ def main(): json.dump(resolved, f, indent=2) f.write('\n') -def update_image(image): - """Set image's protocol_version_default to min over its components. Returns True if changed.""" +def is_next(image): + """True if the image's core is built with the unsafe next-protocol flag.""" + core = next((dep for dep in image['deps'] if dep['name'] == 'core'), None) + return core is not None and \ + NEXT_PROTOCOL_FLAG in core.get('options', {}).get('configure_flags', '') + +def supported_versions(image): + """{component: max supported protocol} for the image, or None if any can't be read.""" # Component name -> function(dep) -> max supported protocol version (or None). sources = { 'xdr': xdr_version, @@ -79,22 +115,13 @@ def update_image(image): dep = deps.get(name) if not dep: print(f"{tag}: skipping, no '{name}' dep") - return False + return None version = extract(dep) if version is None: print(f"{tag}: skipping, could not read protocol version from {name} ({dep['repo']}@{dep['ref']})") - return False + return None supported[name] = version - - version = min(supported.values()) - current = image['config'].get('protocol_version_default') - if current == version: - print(f"{tag}: protocol_version_default matches components ({version}) {supported}") - return False - - print(f"{tag}: protocol_version_default {current} -> {version} {supported}") - image['config']['protocol_version_default'] = version - return True + return supported def xdr_version(dep): """Major of the [package] version in rs-stellar-xdr's Cargo.toml.""" @@ -113,20 +140,12 @@ def xdr_version(dep): return None def core_version(dep): - """CURRENT_LEDGER_PROTOCOL_VERSION from stellar-core, plus the next-protocol flag.""" + """CURRENT_LEDGER_PROTOCOL_VERSION from stellar-core.""" text = read_source(dep, 'src/main/Config.cpp') if text is None: return None match = re.search(r'CURRENT_LEDGER_PROTOCOL_VERSION\s*=\s*(\d+)', text) - if not match: - return None - version = int(match.group(1)) - # stellar-core built with this flag supports one version beyond its source - # constant, matching the #ifdef in Config.cpp; read it from the core dep options. - next_flag = '--enable-next-protocol-version-unsafe-for-production' - if next_flag in dep.get('options', {}).get('configure_flags', ''): - version += 1 - return version + return int(match.group(1)) if match else None def rpc_version(dep): """Major of the stellar-core deb the RPC bundles, from its CI workflow. From 3baf733f9416dd83322d0a7b92ac7ab5cf48f806 Mon Sep 17 00:00:00 2001 From: Leigh <351529+leighmcculloch@users.noreply.github.com> Date: Wed, 26 Aug 2026 14:03:47 +0000 Subject: [PATCH 17/23] revert to flag-based next-protocol derivation --- .scripts/auto-update-protocol-version | 87 +++++++++++---------------- 1 file changed, 34 insertions(+), 53 deletions(-) diff --git a/.scripts/auto-update-protocol-version b/.scripts/auto-update-protocol-version index 4a46cba9d..1ea6f429b 100755 --- a/.scripts/auto-update-protocol-version +++ b/.scripts/auto-update-protocol-version @@ -10,15 +10,9 @@ import sys # Usage: ./.scripts/auto-update-protocol-version [image-tag ...] # # For each image (all images by default, or only those named as arguments), -# sets config.protocol_version_default to the highest ledger protocol version it -# supports: -# -# - Released images: the minimum over the max supported version of each of its -# xdr, core, rpc and horizon components. -# - "Next" images (core built with the unsafe next-protocol flag, e.g. -# 'future' and 'nightly-next'): one beyond the highest released image. Their -# pre-release component pins report lagging release versions that would -# otherwise understate the protocol they exist to exercise. +# sets config.protocol_version_default to the highest ledger protocol version +# supported by ALL of its xdr, core, rpc and horizon components, i.e. the +# minimum of each component's own max supported version. # # Component sources are read at each dep's resolved 'sha' in # (produced by piping images.json through images-resolve-inherits and @@ -32,8 +26,6 @@ import sys # rpc stellar-rpc .github/workflows/stellar-rpc.yml core_deb_version's major # horizon stellar-horizon internal/ingest/main.go MaxSupportedProtocolVersion -NEXT_PROTOCOL_FLAG = '--enable-next-protocol-version-unsafe-for-production' - def main(): args = sys.argv[1:] if not args: @@ -48,38 +40,16 @@ def main(): resolved = json.load(f) by_tag = {image['tag']: image for image in images} - # Max supported protocol per released (non-next) image, then the highest of - # those: the protocol the released set is on. Next images target one beyond. - supported = {image['tag']: supported_versions(image) - for image in resolved if not is_next(image)} - released = max((min(s.values()) for s in supported.values() if s is not None), - default=None) - + # Compute against the resolved commit SHAs, then write the resulting + # protocol_version_default into images.json (refs untouched). updated = False for src in resolved: - tag = src['tag'] - if tags and tag not in tags: - continue - - if is_next(src): - if released is None: - print(f"{tag}: skipping, no released image to derive the next protocol from") - continue - version, detail = released + 1, f"[next: released {released} + 1]" - else: - if supported[tag] is None: - continue # supported_versions already reported why - version, detail = min(supported[tag].values()), supported[tag] - - target = by_tag[tag] - current = target['config'].get('protocol_version_default') - if current == version: - print(f"{tag}: protocol_version_default matches ({version}) {detail}") + if tags and src['tag'] not in tags: continue - print(f"{tag}: protocol_version_default {current} -> {version} {detail}") - target['config']['protocol_version_default'] = version - src['config']['protocol_version_default'] = version - updated = True + if update_image(src): + by_tag[src['tag']]['config']['protocol_version_default'] = \ + src['config']['protocol_version_default'] + updated = True if updated: with open('images.json', 'w') as f: @@ -92,14 +62,8 @@ def main(): json.dump(resolved, f, indent=2) f.write('\n') -def is_next(image): - """True if the image's core is built with the unsafe next-protocol flag.""" - core = next((dep for dep in image['deps'] if dep['name'] == 'core'), None) - return core is not None and \ - NEXT_PROTOCOL_FLAG in core.get('options', {}).get('configure_flags', '') - -def supported_versions(image): - """{component: max supported protocol} for the image, or None if any can't be read.""" +def update_image(image): + """Set image's protocol_version_default to min over its components. Returns True if changed.""" # Component name -> function(dep) -> max supported protocol version (or None). sources = { 'xdr': xdr_version, @@ -115,13 +79,22 @@ def supported_versions(image): dep = deps.get(name) if not dep: print(f"{tag}: skipping, no '{name}' dep") - return None + return False version = extract(dep) if version is None: print(f"{tag}: skipping, could not read protocol version from {name} ({dep['repo']}@{dep['ref']})") - return None + return False supported[name] = version - return supported + + version = min(supported.values()) + current = image['config'].get('protocol_version_default') + if current == version: + print(f"{tag}: protocol_version_default matches components ({version}) {supported}") + return False + + print(f"{tag}: protocol_version_default {current} -> {version} {supported}") + image['config']['protocol_version_default'] = version + return True def xdr_version(dep): """Major of the [package] version in rs-stellar-xdr's Cargo.toml.""" @@ -140,12 +113,20 @@ def xdr_version(dep): return None def core_version(dep): - """CURRENT_LEDGER_PROTOCOL_VERSION from stellar-core.""" + """CURRENT_LEDGER_PROTOCOL_VERSION from stellar-core, plus the next-protocol flag.""" text = read_source(dep, 'src/main/Config.cpp') if text is None: return None match = re.search(r'CURRENT_LEDGER_PROTOCOL_VERSION\s*=\s*(\d+)', text) - return int(match.group(1)) if match else None + if not match: + return None + version = int(match.group(1)) + # stellar-core built with this flag supports one version beyond its source + # constant, matching the #ifdef in Config.cpp; read it from the core dep options. + next_flag = '--enable-next-protocol-version-unsafe-for-production' + if next_flag in dep.get('options', {}).get('configure_flags', ''): + version += 1 + return version def rpc_version(dep): """Major of the stellar-core deb the RPC bundles, from its CI workflow. From 1f7d74ccc586cffdcff16ea559f15527a3c61f0e Mon Sep 17 00:00:00 2001 From: Leigh <351529+leighmcculloch@users.noreply.github.com> Date: Wed, 26 Aug 2026 20:19:21 +0000 Subject: [PATCH 18/23] fail the run when a component version can't be read --- .scripts/auto-update-protocol-version | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.scripts/auto-update-protocol-version b/.scripts/auto-update-protocol-version index 1ea6f429b..333b5361b 100755 --- a/.scripts/auto-update-protocol-version +++ b/.scripts/auto-update-protocol-version @@ -82,8 +82,11 @@ def update_image(image): return False version = extract(dep) if version is None: - print(f"{tag}: skipping, could not read protocol version from {name} ({dep['repo']}@{dep['ref']})") - return False + # A read/parse failure would leave this image stale while others + # still update; fail the whole run rather than open a partial PR. + print(f"{tag}: failed to read protocol version from {name} ({dep['repo']}@{dep['ref']})", + file=sys.stderr) + sys.exit(1) supported[name] = version version = min(supported.values()) From ebb168a79ccf2feaf660eaae98e3a110f63a6f73 Mon Sep 17 00:00:00 2001 From: Leigh <351529+leighmcculloch@users.noreply.github.com> Date: Thu, 27 Aug 2026 04:29:55 +0000 Subject: [PATCH 19/23] drop redundant header comment --- .scripts/auto-update-protocol-version | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.scripts/auto-update-protocol-version b/.scripts/auto-update-protocol-version index 333b5361b..4f841ecee 100755 --- a/.scripts/auto-update-protocol-version +++ b/.scripts/auto-update-protocol-version @@ -14,13 +14,6 @@ import sys # supported by ALL of its xdr, core, rpc and horizon components, i.e. the # minimum of each component's own max supported version. # -# Component sources are read at each dep's resolved 'sha' in -# (produced by piping images.json through images-resolve-inherits and -# images-with-extras), never at the branch refs in images.json, so the protocol -# values match the exact commits a PR will record. images.json is updated in -# place with the resulting protocol_version_default (its refs untouched), and -# 's protocol_version_default is synced to match. -# # xdr rs-stellar-xdr Cargo.toml [package] version's major # core stellar-core src/main/Config.cpp CURRENT_LEDGER_PROTOCOL_VERSION # rpc stellar-rpc .github/workflows/stellar-rpc.yml core_deb_version's major From 6f90d8ce0fda042ce415dab01433a9808a401467 Mon Sep 17 00:00:00 2001 From: Leigh <351529+leighmcculloch@users.noreply.github.com> Date: Thu, 27 Aug 2026 04:30:54 +0000 Subject: [PATCH 20/23] exclude future image from auto-update --- .github/workflows/auto-update-protocol-version.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/auto-update-protocol-version.yml b/.github/workflows/auto-update-protocol-version.yml index 32d01649b..964006a98 100644 --- a/.github/workflows/auto-update-protocol-version.yml +++ b/.github/workflows/auto-update-protocol-version.yml @@ -48,10 +48,11 @@ jobs: <<< "$images" jq -C echo "$images" > images.resolved.json + # 'future' is left out and updated by hand; the rest track automatically. - name: Update protocol_version_default env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: ./.scripts/auto-update-protocol-version images.resolved.json + run: ./.scripts/auto-update-protocol-version images.resolved.json latest testing nightly nightly-next - name: Check for changes id: changes From 6bab8c7862a4ea505886dbd7516aa5d229ea644e Mon Sep 17 00:00:00 2001 From: Leigh <351529+leighmcculloch@users.noreply.github.com> Date: Thu, 27 Aug 2026 04:37:14 +0000 Subject: [PATCH 21/23] gate protocol auto-update on per-image flag --- .../workflows/auto-update-protocol-version.yml | 4 ++-- .scripts/auto-update-protocol-version | 17 +++++++++++------ images.json | 5 +++++ 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/.github/workflows/auto-update-protocol-version.yml b/.github/workflows/auto-update-protocol-version.yml index 964006a98..f074575dc 100644 --- a/.github/workflows/auto-update-protocol-version.yml +++ b/.github/workflows/auto-update-protocol-version.yml @@ -48,11 +48,11 @@ jobs: <<< "$images" jq -C echo "$images" > images.resolved.json - # 'future' is left out and updated by hand; the rest track automatically. + # Only images with "autoupdate": true in images.json are updated. - name: Update protocol_version_default env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: ./.scripts/auto-update-protocol-version images.resolved.json latest testing nightly nightly-next + run: ./.scripts/auto-update-protocol-version images.resolved.json - name: Check for changes id: changes diff --git a/.scripts/auto-update-protocol-version b/.scripts/auto-update-protocol-version index 4f841ecee..0da09ff1c 100755 --- a/.scripts/auto-update-protocol-version +++ b/.scripts/auto-update-protocol-version @@ -9,10 +9,11 @@ import sys # # Usage: ./.scripts/auto-update-protocol-version [image-tag ...] # -# For each image (all images by default, or only those named as arguments), -# sets config.protocol_version_default to the highest ledger protocol version -# supported by ALL of its xdr, core, rpc and horizon components, i.e. the -# minimum of each component's own max supported version. +# For each image with "autoupdate": true (all such by default, or only those +# named as arguments), sets config.protocol_version_default to the highest ledger +# protocol version supported by ALL of its xdr, core, rpc and horizon components, +# i.e. the minimum of each component's own max supported version. Images with +# "autoupdate": false (e.g. 'future') are left for manual updates. # # xdr rs-stellar-xdr Cargo.toml [package] version's major # core stellar-core src/main/Config.cpp CURRENT_LEDGER_PROTOCOL_VERSION @@ -37,10 +38,14 @@ def main(): # protocol_version_default into images.json (refs untouched). updated = False for src in resolved: - if tags and src['tag'] not in tags: + tag = src['tag'] + if tags and tag not in tags: + continue + if not src.get('autoupdate', True): + print(f"{tag}: skipping, autoupdate disabled") continue if update_image(src): - by_tag[src['tag']]['config']['protocol_version_default'] = \ + by_tag[tag]['config']['protocol_version_default'] = \ src['config']['protocol_version_default'] updated = True diff --git a/images.json b/images.json index 0791bf2ce..dc81d5530 100644 --- a/images.json +++ b/images.json @@ -1,6 +1,7 @@ [ { "tag": "latest", + "autoupdate": true, "events": ["pull_request", "push"], "config": { "protocol_version_default": 28 @@ -64,6 +65,7 @@ }, { "tag": "testing", + "autoupdate": true, "events": ["pull_request", "push"], "config": { "protocol_version_default": 28 @@ -132,6 +134,7 @@ }, { "tag": "future", + "autoupdate": false, "events": ["pull_request", "push"], "config": { "protocol_version_default": 28, @@ -190,6 +193,7 @@ }, { "tag": "nightly", + "autoupdate": true, "events": ["push", "schedule"], "config": { "protocol_version_default": 28 @@ -247,6 +251,7 @@ }, { "tag": "nightly-next", + "autoupdate": true, "events": ["push", "schedule"], "config": { "protocol_version_default": 28, From 569cc266adeae584f02b83d6bb429dd9a5f3fa98 Mon Sep 17 00:00:00 2001 From: Leigh <351529+leighmcculloch@users.noreply.github.com> Date: Thu, 27 Aug 2026 10:27:38 +0000 Subject: [PATCH 22/23] move autoupdate flag under config and rename --- .../workflows/auto-update-protocol-version.yml | 2 +- .scripts/auto-update-protocol-version | 14 +++++++------- images.json | 16 ++++++++-------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.github/workflows/auto-update-protocol-version.yml b/.github/workflows/auto-update-protocol-version.yml index f074575dc..cb05a4eb9 100644 --- a/.github/workflows/auto-update-protocol-version.yml +++ b/.github/workflows/auto-update-protocol-version.yml @@ -48,7 +48,7 @@ jobs: <<< "$images" jq -C echo "$images" > images.resolved.json - # Only images with "autoupdate": true in images.json are updated. + # Only images with config.autoupdate_protocol_version_default are updated. - name: Update protocol_version_default env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.scripts/auto-update-protocol-version b/.scripts/auto-update-protocol-version index 0da09ff1c..dcbefa30a 100755 --- a/.scripts/auto-update-protocol-version +++ b/.scripts/auto-update-protocol-version @@ -9,11 +9,11 @@ import sys # # Usage: ./.scripts/auto-update-protocol-version [image-tag ...] # -# For each image with "autoupdate": true (all such by default, or only those -# named as arguments), sets config.protocol_version_default to the highest ledger -# protocol version supported by ALL of its xdr, core, rpc and horizon components, -# i.e. the minimum of each component's own max supported version. Images with -# "autoupdate": false (e.g. 'future') are left for manual updates. +# For each image with config.autoupdate_protocol_version_default true (all such by +# default, or only those named as arguments), sets config.protocol_version_default +# to the highest ledger protocol version supported by ALL of its xdr, core, rpc and +# horizon components, i.e. the minimum of each component's own max supported +# version. Images with it false (e.g. 'future') are left for manual updates. # # xdr rs-stellar-xdr Cargo.toml [package] version's major # core stellar-core src/main/Config.cpp CURRENT_LEDGER_PROTOCOL_VERSION @@ -41,8 +41,8 @@ def main(): tag = src['tag'] if tags and tag not in tags: continue - if not src.get('autoupdate', True): - print(f"{tag}: skipping, autoupdate disabled") + if not src['config'].get('autoupdate_protocol_version_default', True): + print(f"{tag}: skipping, autoupdate_protocol_version_default is false") continue if update_image(src): by_tag[tag]['config']['protocol_version_default'] = \ diff --git a/images.json b/images.json index dc81d5530..ed59c2be4 100644 --- a/images.json +++ b/images.json @@ -1,10 +1,10 @@ [ { "tag": "latest", - "autoupdate": true, "events": ["pull_request", "push"], "config": { - "protocol_version_default": 28 + "protocol_version_default": 28, + "autoupdate_protocol_version_default": true }, "deps": [ { @@ -65,10 +65,10 @@ }, { "tag": "testing", - "autoupdate": true, "events": ["pull_request", "push"], "config": { - "protocol_version_default": 28 + "protocol_version_default": 28, + "autoupdate_protocol_version_default": true }, "deps": [ { @@ -134,10 +134,10 @@ }, { "tag": "future", - "autoupdate": false, "events": ["pull_request", "push"], "config": { "protocol_version_default": 28, + "autoupdate_protocol_version_default": false, "horizon_skip_protocol_version_check": true }, "deps": [ @@ -193,10 +193,10 @@ }, { "tag": "nightly", - "autoupdate": true, "events": ["push", "schedule"], "config": { - "protocol_version_default": 28 + "protocol_version_default": 28, + "autoupdate_protocol_version_default": true }, "deps": [ { @@ -251,10 +251,10 @@ }, { "tag": "nightly-next", - "autoupdate": true, "events": ["push", "schedule"], "config": { "protocol_version_default": 28, + "autoupdate_protocol_version_default": true, "horizon_skip_protocol_version_check": true }, "deps": [ From e3df0af6da735cfcc569ea17be7737479db8712a Mon Sep 17 00:00:00 2001 From: Leigh <351529+leighmcculloch@users.noreply.github.com> Date: Thu, 27 Aug 2026 13:04:58 +0000 Subject: [PATCH 23/23] derive rpc and horizon protocol from xdr commit --- .scripts/auto-update-protocol-version | 132 +++++++++++++++++--------- 1 file changed, 87 insertions(+), 45 deletions(-) diff --git a/.scripts/auto-update-protocol-version b/.scripts/auto-update-protocol-version index dcbefa30a..021831215 100755 --- a/.scripts/auto-update-protocol-version +++ b/.scripts/auto-update-protocol-version @@ -12,13 +12,19 @@ import sys # For each image with config.autoupdate_protocol_version_default true (all such by # default, or only those named as arguments), sets config.protocol_version_default # to the highest ledger protocol version supported by ALL of its xdr, core, rpc and -# horizon components, i.e. the minimum of each component's own max supported -# version. Images with it false (e.g. 'future') are left for manual updates. +# horizon components. Images with it false (e.g. 'future') are left for manual +# updates. # # xdr rs-stellar-xdr Cargo.toml [package] version's major # core stellar-core src/main/Config.cpp CURRENT_LEDGER_PROTOCOL_VERSION -# rpc stellar-rpc .github/workflows/stellar-rpc.yml core_deb_version's major -# horizon stellar-horizon internal/ingest/main.go MaxSupportedProtocolVersion +# rpc stellar-rpc go.mod's go-stellar-sdk -> its Makefile XDR_COMMIT +# horizon stellar-horizon go.mod's go-stellar-sdk -> its Makefile XDR_COMMIT +# +# rpc and horizon don't state a protocol version of their own, so theirs is derived +# from the XDR they build against: the stellar-xdr commit their go-stellar-sdk +# generates from, compared with the stellar-xdr commit core pins at +# src/protocol-curr/xdr. Matching or newer means they handle core's protocol and so +# carry core's version; older means they can't and the image is left alone. def main(): args = sys.argv[1:] @@ -62,30 +68,31 @@ def main(): def update_image(image): """Set image's protocol_version_default to min over its components. Returns True if changed.""" - # Component name -> function(dep) -> max supported protocol version (or None). - sources = { - 'xdr': xdr_version, - 'core': core_version, - 'rpc': rpc_version, - 'horizon': horizon_version, - } tag = image['tag'] deps = {dep['name']: dep for dep in image['deps']} - - supported = {} - for name, extract in sources.items(): - dep = deps.get(name) - if not dep: + for name in ('xdr', 'core', 'rpc', 'horizon'): + if name not in deps: print(f"{tag}: skipping, no '{name}' dep") return False - version = extract(dep) - if version is None: - # A read/parse failure would leave this image stale while others - # still update; fail the whole run rather than open a partial PR. - print(f"{tag}: failed to read protocol version from {name} ({dep['repo']}@{dep['ref']})", - file=sys.stderr) + + core = require(tag, 'core', deps['core'], core_version) + supported = {'xdr': require(tag, 'xdr', deps['xdr'], xdr_version), 'core': core} + + # rpc and horizon are at core's protocol as long as the XDR they build against + # is not older than the XDR core pins. + core_xdr = require(tag, 'core', deps['core'], core_xdr_commit) + for name in ('rpc', 'horizon'): + dep = deps[name] + commit = require(tag, name, dep, sdk_xdr_commit) + status = xdr_compare(core_xdr, commit) + if status is None: + print(f"{tag}: failed to compare {name}'s xdr with core's", file=sys.stderr) sys.exit(1) - supported[name] = version + if status not in ('identical', 'ahead'): + print(f"{tag}: skipping, {name}'s xdr {commit[:12]} is {status} " + f"vs core's {core_xdr[:12]}") + return False + supported[name] = core version = min(supported.values()) current = image['config'].get('protocol_version_default') @@ -97,6 +104,15 @@ def update_image(image): image['config']['protocol_version_default'] = version return True +def require(tag, name, dep, extract): + """extract(dep), or exit: a read/parse failure must not leave the image stale.""" + value = extract(dep) + if value is None: + print(f"{tag}: failed to read protocol version from {name} ({dep['repo']}@{dep['ref']})", + file=sys.stderr) + sys.exit(1) + return value + def xdr_version(dep): """Major of the [package] version in rs-stellar-xdr's Cargo.toml.""" text = read_source(dep, 'Cargo.toml') @@ -114,7 +130,7 @@ def xdr_version(dep): return None def core_version(dep): - """CURRENT_LEDGER_PROTOCOL_VERSION from stellar-core, plus the next-protocol flag.""" + """CURRENT_LEDGER_PROTOCOL_VERSION from stellar-core.""" text = read_source(dep, 'src/main/Config.cpp') if text is None: return None @@ -129,43 +145,69 @@ def core_version(dep): version += 1 return version -def rpc_version(dep): - """Major of the stellar-core deb the RPC bundles, from its CI workflow. +def core_xdr_commit(dep): + """stellar-xdr commit that stellar-core pins as its protocol XDR.""" + return read_submodule(dep, 'src/protocol-curr/xdr') - stellar-rpc pins the captive-core build it runs against via core_deb_version - in .github/workflows/stellar-rpc.yml (one entry per tested protocol); the - highest of those cores' major versions is the top protocol the RPC supports.""" - text = read_source(dep, '.github/workflows/stellar-rpc.yml') +def sdk_xdr_commit(dep): + """stellar-xdr commit the dep's go-stellar-sdk generates its XDR from.""" + text = read_source(dep, 'go.mod') if text is None: return None - majors = [int(m) for m in re.findall(r"core_deb_version:\s*['\"]?(\d+)", text)] - return max(majors, default=None) + match = re.search(r'github\.com/stellar/go-stellar-sdk\s+(\S+)', text) + if not match: + return None + text = read_file('stellar/go-stellar-sdk', module_ref(match.group(1)), 'Makefile') + if text is None: + return None + match = re.search(r'^XDR_COMMIT\s*=\s*(\w+)', text, re.MULTILINE) + return match.group(1) if match else None -def horizon_version(dep): - """MaxSupportedProtocolVersion from stellar-horizon's ingest package.""" - return regex_version(dep, 'internal/ingest/main.go', - r'MaxSupportedProtocolVersion\s+uint32\s*=\s*(\d+)') +def module_ref(version): + """Git ref for a go module version: a pseudo-version's commit, else the tag.""" + match = re.search(r'-([0-9a-f]{12})$', version) + return match.group(1) if match else version -def regex_version(dep, path, pattern): - """Read `path` from the dep and pull an int out of the first `pattern` match.""" - text = read_source(dep, path) - if text is None: +def xdr_compare(base, head): + """How head compares with base in stellar-xdr: identical, ahead, behind, diverged.""" + result = subprocess.run( + ['gh', 'api', f'repos/stellar/stellar-xdr/compare/{base}...{head}', '--jq', '.status'], + capture_output=True, text=True + ) + if result.returncode != 0: + print(f"Error: failed to compare stellar-xdr {base}...{head}: {result.stderr}", + file=sys.stderr) return None - match = re.search(pattern, text) - return int(match.group(1)) if match else None + return result.stdout.strip() or None def read_source(dep, path): - """Read a file from the dep's repo at its resolved sha via the GitHub API, or None.""" + """Read a file from the dep's repo at its resolved sha, or None.""" + return read_file(dep['repo'], dep['sha'], path) + +def read_file(repo, ref, path): + """Read a file from a repo at a ref via the GitHub API, or None.""" result = subprocess.run( ['gh', 'api', '-H', 'Accept: application/vnd.github.raw', - f"repos/{dep['repo']}/contents/{path}?ref={dep['sha']}"], + f"repos/{repo}/contents/{path}?ref={ref}"], capture_output=True, text=True ) if result.returncode != 0: - print(f"Error: failed to read {path} from {dep['repo']}@{dep['ref']}: {result.stderr}", + print(f"Error: failed to read {path} from {repo}@{ref}: {result.stderr}", file=sys.stderr) return None return result.stdout +def read_submodule(dep, path): + """Commit a submodule is pinned at in the dep's repo, or None.""" + result = subprocess.run( + ['gh', 'api', f"repos/{dep['repo']}/contents/{path}?ref={dep['sha']}", '--jq', '.sha'], + capture_output=True, text=True + ) + if result.returncode != 0: + print(f"Error: failed to read submodule {path} from {dep['repo']}@{dep['ref']}: {result.stderr}", + file=sys.stderr) + return None + return result.stdout.strip() or None + if __name__ == '__main__': main()