From 77cbf1faa3f945dee7ca5729f84ec6d5f0229e21 Mon Sep 17 00:00:00 2001 From: kalo <24719519+KaloyanTanev@users.noreply.github.com> Date: Tue, 14 Jul 2026 12:09:37 +0300 Subject: [PATCH 1/4] Fetch latest versions on minor releases for compatibility matrix --- testutil/genchangelog/main.go | 189 ++++++++++++++++++++++++++++++ testutil/genchangelog/template.md | 28 ++--- 2 files changed, 202 insertions(+), 15 deletions(-) diff --git a/testutil/genchangelog/main.go b/testutil/genchangelog/main.go index 7c793e1129..94e0a46dec 100644 --- a/testutil/genchangelog/main.go +++ b/testutil/genchangelog/main.go @@ -61,6 +61,23 @@ var ( numberRegex = regexp.MustCompile(`[#/](\d{2,})`) categoryRegex = regexp.MustCompile(`category:\s?(\w+)`) ticketRegex = regexp.MustCompile(`ticket:(.*)`) + + // tagVersionRegex parses major.minor.patch from a release tag, e.g. "v1.10.3" or "v1.10.0-rc1". + tagVersionRegex = regexp.MustCompile(`^v(\d+)\.(\d+)\.(\d+)`) + + // clientRepos maps each compatibility-matrix client to its GitHub repo, in matrix column order. + clientRepos = []struct { + Name string + Repo string + }{ + {"Teku", "Consensys/teku"}, + {"Lighthouse", "sigp/lighthouse"}, + {"Lodestar", "ChainSafe/lodestar"}, + {"Nimbus", "status-im/nimbus-eth2"}, + {"Prysm", "OffchainLabs/prysm"}, + {"Grandine", "grandinetech/grandine"}, + {"Vouch", "attestantio/vouch"}, + } ) // pullRequest is parsed from log. @@ -87,6 +104,18 @@ type tplData struct { RangeLink string Categories []tplCategory ExtraPRs []pullRequest + Clients clientVersions +} + +// clientVersions holds the client versions rendered in the compatibility matrix. +type clientVersions struct { + Teku string + Lighthouse string + Lodestar string + Nimbus string + Prysm string + Grandine string + Vouch string } // tplCategory is a category section in the changelog. @@ -151,6 +180,11 @@ func run(ctx context.Context, gitRange string, output string, token string) erro return err } + data.Clients, err = clientVersionsForTag(data.Tag, token) + if err != nil { + return err + } + b, err := execTemplate(data) if err != nil { return err @@ -223,6 +257,161 @@ func makeIssueFunc(token string) func(int) (issue string, status string, err err } } +// clientVersionsForTag returns the compatibility-matrix client versions for the release tag. +// Minor releases (patch == 0) use the latest upstream client versions, while patch releases +// reuse the versions tested in their minor release, so a patch line stays consistent. +func clientVersionsForTag(tag string, token string) (clientVersions, error) { + _, _, patch, err := parseTagVersion(tag) + if err != nil { + return clientVersions{}, err + } + + if patch == 0 { + fmt.Printf("Minor release %s: fetching latest client versions\n", tag) + return latestClientVersions(token) + } + + minorTag, err := minorTagOf(tag) + if err != nil { + return clientVersions{}, err + } + + fmt.Printf("Patch release %s: reusing client versions from minor release %s\n", tag, minorTag) + + return minorClientVersions(minorTag, token) +} + +// latestClientVersions fetches the latest release version of each matrix client from GitHub. +func latestClientVersions(token string) (clientVersions, error) { + versions := make(map[string]string) + + for _, c := range clientRepos { + var resp struct { + TagName string `json:"tag_name"` + } + + u := fmt.Sprintf("https://api.github.com/repos/%s/releases/latest", c.Repo) + if err := githubGetJSON(u, token, &resp); err != nil { + return clientVersions{}, errors.Wrap(err, "fetch latest client release", z.Str("client", c.Name), z.Str("repo", c.Repo)) + } + + if resp.TagName == "" { + return clientVersions{}, errors.New("empty client release tag", z.Str("client", c.Name), z.Str("repo", c.Repo)) + } + + versions[c.Name] = normalizeVersion(resp.TagName) + } + + return clientVersionsFromMap(versions), nil +} + +// minorClientVersions parses the client versions from the compatibility matrix of a previous minor release. +func minorClientVersions(minorTag string, token string) (clientVersions, error) { + var resp struct { + Body string `json:"body"` + } + + u := "https://api.github.com/repos/obolnetwork/charon/releases/tags/" + minorTag + if err := githubGetJSON(u, token, &resp); err != nil { + return clientVersions{}, errors.Wrap(err, "fetch minor release", z.Str("minor", minorTag)) + } + + versions := make(map[string]string) + + for _, c := range clientRepos { + re := regexp.MustCompile(regexp.QuoteMeta(c.Name) + `\s+(v[\d][\w.\-]*)`) + + m := re.FindStringSubmatch(resp.Body) + if len(m) < 2 { + return clientVersions{}, errors.New("client version not found in minor release", z.Str("client", c.Name), z.Str("minor", minorTag)) + } + + versions[c.Name] = m[1] + } + + return clientVersionsFromMap(versions), nil +} + +// clientVersionsFromMap builds a clientVersions from a client-name-keyed map. +func clientVersionsFromMap(m map[string]string) clientVersions { + return clientVersions{ + Teku: m["Teku"], + Lighthouse: m["Lighthouse"], + Lodestar: m["Lodestar"], + Nimbus: m["Nimbus"], + Prysm: m["Prysm"], + Grandine: m["Grandine"], + Vouch: m["Vouch"], + } +} + +// githubGetJSON performs an authenticated GET request and unmarshals the JSON response into v. +func githubGetJSON(url string, token string, v any) error { + req, err := http.NewRequest(http.MethodGet, url, nil) //nolint:noctx // Non-critical tooling code. + if err != nil { + return errors.Wrap(err, "new request") + } + + if token != "" { + req.SetBasicAuth(token, "x-oauth-basic") + } + + resp, err := new(http.Client).Do(req) + if err != nil { + return errors.Wrap(err, "do request") + } + defer resp.Body.Close() + + b, err := io.ReadAll(resp.Body) + if err != nil { + return errors.Wrap(err, "read body") + } + + if resp.StatusCode != http.StatusOK { + return errors.New("unexpected status code", z.Int("status", resp.StatusCode), z.Str("url", url), z.Str("body", string(b))) + } + + if err := json.Unmarshal(b, v); err != nil { + return errors.Wrap(err, "unmarshal response", z.Str("url", url), z.Str("body", string(b))) + } + + return nil +} + +// normalizeVersion ensures the version tag has a leading "v" to match the matrix convention. +func normalizeVersion(tag string) string { + tag = strings.TrimSpace(tag) + if tag != "" && !strings.HasPrefix(tag, "v") { + tag = "v" + tag + } + + return tag +} + +// parseTagVersion parses the major, minor and patch numbers from a release tag. +func parseTagVersion(tag string) (major, minor, patch int, err error) { + m := tagVersionRegex.FindStringSubmatch(tag) + if len(m) < 4 { + return 0, 0, 0, errors.New("invalid release tag", z.Str("tag", tag)) + } + + major, _ = strconv.Atoi(m[1]) + minor, _ = strconv.Atoi(m[2]) + patch, _ = strconv.Atoi(m[3]) + + return major, minor, patch, nil +} + +// minorTagOf returns the minor release tag (patch 0) that the given tag belongs to. +func minorTagOf(tag string) (string, error) { + major, minor, _, err := parseTagVersion(tag) + if err != nil { + return "", err + } + + return fmt.Sprintf("v%d.%d.0", major, minor), nil +} + // execTemplate returns the executed changelog template. func execTemplate(data tplData) ([]byte, error) { templ, err := template.New("").Parse(tpl) diff --git a/testutil/genchangelog/template.md b/testutil/genchangelog/template.md index 1b206bf643..8c6e227a02 100644 --- a/testutil/genchangelog/template.md +++ b/testutil/genchangelog/template.md @@ -2,7 +2,7 @@ ![Obol Logo](https://obol.tech/obolnetwork.png) - + Read the rest of the release notes for more: @@ -15,26 +15,24 @@ Read the rest of the release notes for more: {{end}} ## Compatibility Matrix - -This release of Charon is backwards compatible with Charon v1.0, v1.1, v1.2. +This release of Charon is backwards compatible with Charon >= v1.0., though *only v1.7.* and newer are Fulu-ready. -The below matrix details a combination of beacon node (consensus layer) + validator clients and their corresponding versions the DV Labs team have tested with this Charon release. More validator and consensus client will be added to this list as they are supported in our automated testing framework. +The below matrix details a combination of beacon node (consensus layer) + validator clients and their corresponding versions the DV Labs team have tested with this Charon release. More validator and consensus clients will be added to this list as they are supported in our automated testing framework. **Legend** - ✅: All duties succeed in testing - 🟡: All duties succeed in testing, except non-penalised aggregation duties - 🟠: Duties may fail for this combination -- 🔴: One or more duties fails consistently - - -| Validator 👉 Consensus 👇 | Teku v25.3.0 | Lighthouse v7.0.0 | Lodestar v1.28.1 | Nimbus v25.3.1 | Prysm v5.3.1 | Vouch v1.10.2 | Remarks | -|---------------------------|--------------|-------------------|------------------|----------------|--------------|---------------|---------| -| Teku v25.3.0 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | -| Lighthouse v7.0.0 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | -| Lodestar v1.28.1 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | -| Nimbus v25.3.1 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | -| Prysm v5.3.1 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | -| Grandine v1.1.0 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | +- 🔴: One or more duties fail consistently + +| Validator 👉 Consensus 👇 | Teku {{.Clients.Teku}} | Lighthouse {{.Clients.Lighthouse}} | Lodestar {{.Clients.Lodestar}} | Nimbus {{.Clients.Nimbus}} | Prysm {{.Clients.Prysm}} | Vouch {{.Clients.Vouch}} [❗](## "Vouch VC aggregations and sync contributions are not yet supported by Attestant team.") | +|---------------------------|--------------|-------------------|------------------|----------------|--------------|---------------| +| Teku {{.Clients.Teku}} [❗](## "Teku BN needs the `--validators-graffiti-client-append-format=DISABLED` flag in order to produce blocks properly.") | ✅ | ✅ | ✅ | ✅ | ✅ | 🟡 | +| Lighthouse {{.Clients.Lighthouse}} | ✅ | ✅ | ✅ | ✅ | ✅ | 🟡 | +| Lodestar {{.Clients.Lodestar}} | ✅ | ✅ | ✅ | ✅ | ✅ | 🟡 | +| Nimbus {{.Clients.Nimbus}} | ✅ | ✅ | ✅ | ✅ | ✅ | 🟡 | +| Prysm {{.Clients.Prysm}} | ✅ | ✅ | ✅ | ✅ | ✅ | 🟡 | +| Grandine {{.Clients.Grandine}} | ✅ | ✅ | ✅ | ✅ | ✅ | 🟡 | ## What's Changed {{range .ExtraPRs}} From 4cef7bda3c94ac3795efbd917f4902ba9aa1bf4f Mon Sep 17 00:00:00 2001 From: kalo <24719519+KaloyanTanev@users.noreply.github.com> Date: Tue, 14 Jul 2026 12:21:43 +0300 Subject: [PATCH 2/4] Fix auth style --- testutil/genchangelog/main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/testutil/genchangelog/main.go b/testutil/genchangelog/main.go index 94e0a46dec..cbbc43e900 100644 --- a/testutil/genchangelog/main.go +++ b/testutil/genchangelog/main.go @@ -208,7 +208,7 @@ func makeIssueFunc(token string) func(int) (issue string, status string, err err } if token != "" { - req.SetBasicAuth(token, "x-oauth-basic") + req.Header.Set("Authorization", "Bearer "+token) } resp, err := new(http.Client).Do(req) @@ -353,7 +353,7 @@ func githubGetJSON(url string, token string, v any) error { } if token != "" { - req.SetBasicAuth(token, "x-oauth-basic") + req.Header.Set("Authorization", "Bearer "+token) } resp, err := new(http.Client).Do(req) From b1b0c9b70c09f64783d0772182fcc0f82cb31e72 Mon Sep 17 00:00:00 2001 From: kalo <24719519+KaloyanTanev@users.noreply.github.com> Date: Tue, 14 Jul 2026 11:24:39 +0200 Subject: [PATCH 3/4] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: kalo <24719519+KaloyanTanev@users.noreply.github.com> --- testutil/genchangelog/template.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testutil/genchangelog/template.md b/testutil/genchangelog/template.md index 8c6e227a02..a0331dde01 100644 --- a/testutil/genchangelog/template.md +++ b/testutil/genchangelog/template.md @@ -15,7 +15,7 @@ Read the rest of the release notes for more: {{end}} ## Compatibility Matrix -This release of Charon is backwards compatible with Charon >= v1.0., though *only v1.7.* and newer are Fulu-ready. +This release of Charon is backwards compatible with Charon >= v1.0, though only v1.7 and newer are Fulu-ready. The below matrix details a combination of beacon node (consensus layer) + validator clients and their corresponding versions the DV Labs team have tested with this Charon release. More validator and consensus clients will be added to this list as they are supported in our automated testing framework. From 8b08d8a532750ec7f9d8b04f08738f2884e4f1f9 Mon Sep 17 00:00:00 2001 From: kalo <24719519+KaloyanTanev@users.noreply.github.com> Date: Tue, 14 Jul 2026 11:25:29 +0200 Subject: [PATCH 4/4] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: kalo <24719519+KaloyanTanev@users.noreply.github.com> --- testutil/genchangelog/main.go | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/testutil/genchangelog/main.go b/testutil/genchangelog/main.go index cbbc43e900..8502d46188 100644 --- a/testutil/genchangelog/main.go +++ b/testutil/genchangelog/main.go @@ -65,7 +65,7 @@ var ( // tagVersionRegex parses major.minor.patch from a release tag, e.g. "v1.10.3" or "v1.10.0-rc1". tagVersionRegex = regexp.MustCompile(`^v(\d+)\.(\d+)\.(\d+)`) - // clientRepos maps each compatibility-matrix client to its GitHub repo, in matrix column order. + // clientRepos maps each compatibility-matrix client to its GitHub repo. clientRepos = []struct { Name string Repo string @@ -395,9 +395,20 @@ func parseTagVersion(tag string) (major, minor, patch int, err error) { return 0, 0, 0, errors.New("invalid release tag", z.Str("tag", tag)) } - major, _ = strconv.Atoi(m[1]) - minor, _ = strconv.Atoi(m[2]) - patch, _ = strconv.Atoi(m[3]) + major, err = strconv.Atoi(m[1]) + if err != nil { + return 0, 0, 0, errors.Wrap(err, "parse major", z.Str("tag", tag)) + } + + minor, err = strconv.Atoi(m[2]) + if err != nil { + return 0, 0, 0, errors.Wrap(err, "parse minor", z.Str("tag", tag)) + } + + patch, err = strconv.Atoi(m[3]) + if err != nil { + return 0, 0, 0, errors.Wrap(err, "parse patch", z.Str("tag", tag)) + } return major, minor, patch, nil }