Skip to content

Commit 007ca43

Browse files
authored
Add version check script to validate tags (#1166)
* Add version check script to validate tags * add missing script * Add module makefile * restore push command * make gogenerate && make license-update * fix lint * Use github api and improve code * remove comment * Add readme
1 parent a24f170 commit 007ca43

7 files changed

Lines changed: 682 additions & 1 deletion

File tree

Makefile

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@ GOTOOLCHAIN ?= go1.25.7+auto
2020
.PHONY: all
2121
all: misspell
2222

23+
# Compare versions.yaml module-set version to latest semver tags from repository
24+
VERSIONSCHECK_GITHUB_REPO ?= elastic/opentelemetry-collector-components
25+
26+
.PHONY: versionscheck
27+
versionscheck:
28+
@cd "$(SRC_ROOT)" && $(GOCMD) run -C internal/versionscheck . -versions $(SRC_ROOT)/versions.yaml -github-repo $(VERSIONSCHECK_GITHUB_REPO) -module-key $(MODSET)
29+
2330
# Append root module to all modules
2431
GOMODULES = $(ALL_MODULES)
2532

@@ -149,7 +156,7 @@ COMMIT?=HEAD
149156
MODSET?=edot-base
150157
REMOTE?=git@github.com:elastic/opentelemetry-collector-components.git
151158
.PHONY: push-tags
152-
push-tags: $(MULTIMOD)
159+
push-tags: $(MULTIMOD) versionscheck
153160
$(MULTIMOD) verify
154161
set -e; for tag in `$(MULTIMOD) tag -m ${MODSET} -c ${COMMIT} --print-tags | grep -v "Using" `; do \
155162
echo "pushing tag $${tag}"; \

internal/versionscheck/Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
include ../../Makefile.Common

internal/versionscheck/README.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# versionscheck
2+
3+
`versionscheck` reads a `module-sets` entry from [`versions.yaml`](../versions.yaml), and compares the version from the module against the highest semver-shaped tag from the repo (e.g. `v1.2.3`, `component/v1.2.3`, or `path/component/v1.2.3`). It **requires** that the YAML version is **strictly greater** than the repo tag. If there are **no** semver tags, it fails unless `-allow-empty-tag` is set.
4+
5+
It is run automatically before [`make push-tags`](../Makefile) (the `push-tags` target depends on `versionscheck`).
6+
7+
## Usage
8+
9+
**From the repository root (recommended):**
10+
11+
```shell
12+
make versionscheck
13+
```
14+
15+
This runs `go run -C internal/versionscheck .` with:
16+
17+
- `-versions``<repo-root>/versions.yaml`
18+
- `-github-repo``$(VERSIONSCHECK_GITHUB_REPO)` (default: `elastic/opentelemetry-collector-components`)
19+
- `-module-key``$(MODSET)` (default: `edot-base`)
20+
21+
Override as needed, for example:
22+
23+
```shell
24+
make versionscheck MODSET=edot-base VERSIONSCHECK_GITHUB_REPO=owner/repo
25+
```
26+
27+
**Manual invocation:**
28+
29+
```shell
30+
go run -C internal/versionscheck . \
31+
-versions /path/to/versions.yaml \
32+
-github-repo owner/repo \
33+
-module-key edot-base
34+
```
35+
36+
Add optional flags (see below) at the end.
37+
38+
## Flags
39+
40+
| Flag | Description |
41+
|------|-------------|
42+
| `-versions` | **Required.** Path to `versions.yaml`. |
43+
| `-github-repo` | **Required.** GitHub repository as `owner/repo` (used with the GitHub API to list tags). |
44+
| `-module-key` | **Required.** Key under `module-sets` in `versions.yaml` (e.g. `edot-base`). |
45+
| `-allow-empty-tag` | If set, do not fail when the repository has no semver-shaped tags; otherwise exit with an error when no such tags exist. |
46+
| `-q` | Quiet: suppress the success message on stdout. |
47+
48+
On failure the process exits non-zero and prints an error (via `log.Fatal`).
49+

internal/versionscheck/go.mod

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
module github.com/elastic/opentelemetry-collector-components/internal/versionscheck
2+
3+
go 1.25.0
4+
5+
require (
6+
github.com/google/go-github/v84 v84.0.0
7+
golang.org/x/mod v0.14.0
8+
gopkg.in/yaml.v3 v3.0.1
9+
)
10+
11+
require github.com/google/go-querystring v1.2.0 // indirect

internal/versionscheck/go.sum

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
2+
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
3+
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
4+
github.com/google/go-github/v84 v84.0.0 h1:I/0Xn5IuChMe8TdmI2bbim5nyhaRFJ7DEdzmD2w+yVA=
5+
github.com/google/go-github/v84 v84.0.0/go.mod h1:WwYL1z1ajRdlaPszjVu/47x1L0PXukJBn73xsiYrRRQ=
6+
github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0=
7+
github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU=
8+
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
9+
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
10+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
11+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
12+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
13+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

internal/versionscheck/main.go

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
// Licensed to Elasticsearch B.V. under one or more contributor
2+
// license agreements. See the NOTICE file distributed with
3+
// this work for additional information regarding copyright
4+
// ownership. Elasticsearch B.V. licenses this file to you under
5+
// the Apache License, Version 2.0 (the "License"); you may
6+
// not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
// Versionscheck is a development tool that compares versions.yaml with Git tags.
19+
package main
20+
21+
import (
22+
"context"
23+
"flag"
24+
"fmt"
25+
"log"
26+
"net/http"
27+
"os"
28+
"strings"
29+
"time"
30+
31+
"github.com/google/go-github/v84/github"
32+
"golang.org/x/mod/semver"
33+
"gopkg.in/yaml.v3"
34+
)
35+
36+
type versionsYAML struct {
37+
ModuleSets map[string]moduleSet `yaml:"module-sets"`
38+
ExcludedModules []string `yaml:"excluded-modules"`
39+
}
40+
41+
type moduleSet struct {
42+
Version string `yaml:"version"`
43+
Modules []string `yaml:"modules"`
44+
}
45+
46+
// tagFetchDeadline bounds how long we wait for paginated ListTags calls to GitHub.
47+
const tagFetchDeadline = 5 * time.Minute
48+
49+
var httpClient = newVersionscheckHTTPClient()
50+
51+
func newVersionscheckHTTPClient() *http.Client {
52+
return &http.Client{
53+
Timeout: tagFetchDeadline,
54+
}
55+
}
56+
57+
func main() {
58+
log.SetFlags(0)
59+
log.SetPrefix("versionscheck: ")
60+
if err := run(); err != nil {
61+
log.Fatal(err)
62+
}
63+
}
64+
65+
func run() error {
66+
versionsPath := flag.String("versions", "", "required: path to versions.yaml")
67+
githubRepo := flag.String("github-repo", "", "required: GitHub repository as owner/repo;")
68+
moduleKey := flag.String("module-key", "", "required: module-sets key in versions.yaml (e.g. edot-base)")
69+
allowEmptyTag := flag.Bool("allow-empty-tag", false, "allow a repository with no semver-shaped tags; otherwise fail when GitHub returns no v* tags")
70+
71+
quiet := flag.Bool("q", false, "suppress success message on stdout")
72+
flag.Parse()
73+
74+
if *versionsPath == "" {
75+
return fmt.Errorf("-versions is required")
76+
}
77+
if *githubRepo == "" {
78+
return fmt.Errorf("-github-repo is required (owner/repo)")
79+
}
80+
if *moduleKey == "" {
81+
return fmt.Errorf("-module-key is required")
82+
}
83+
84+
data, err := os.ReadFile(*versionsPath)
85+
if err != nil {
86+
return fmt.Errorf("read versions file: %w", err)
87+
}
88+
89+
cfg, err := parseVersionsYAML(data)
90+
if err != nil {
91+
return err
92+
}
93+
94+
ms, ok := cfg.ModuleSets[*moduleKey]
95+
if !ok {
96+
return fmt.Errorf("no entry %s in %s", *moduleKey, *versionsPath)
97+
}
98+
if ms.Version == "" {
99+
return fmt.Errorf("empty version for %s", *moduleKey)
100+
}
101+
if !semver.IsValid(ms.Version) {
102+
return fmt.Errorf("invalid semver for %s version: %q", *moduleKey, ms.Version)
103+
}
104+
105+
tags, err := gitTagListFromGitHub(*githubRepo)
106+
if err != nil {
107+
return err
108+
}
109+
latest, tagName := latestComponentVersion(tags)
110+
111+
if err := validateModuleSetVersionAgainstTagList(ms.Version, latest, tagName, *allowEmptyTag, *githubRepo); err != nil {
112+
return err
113+
}
114+
115+
if !*quiet {
116+
msg := formatVersionscheckOK(*moduleKey, ms.Version, latest, tagName, *githubRepo)
117+
if _, err := fmt.Fprint(os.Stdout, msg); err != nil {
118+
log.Printf("warning: could not write success message: %v", err)
119+
}
120+
}
121+
return nil
122+
}
123+
124+
// splitGitHubRepo parses "owner/repo" for use with the GitHub API.
125+
func splitGitHubRepo(githubRepo string) (owner, repo string, err error) {
126+
parts := strings.Split(githubRepo, "/")
127+
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
128+
return "", "", fmt.Errorf(`github-repo must be "owner/repo", got %q`, githubRepo)
129+
}
130+
return parts[0], parts[1], nil
131+
}
132+
133+
func gitTagListFromGitHub(githubRepo string) ([]*github.RepositoryTag, error) {
134+
owner, repo, err := splitGitHubRepo(githubRepo)
135+
if err != nil {
136+
return nil, err
137+
}
138+
139+
ctx, cancel := context.WithTimeout(context.Background(), tagFetchDeadline)
140+
defer cancel()
141+
142+
client := github.NewClient(httpClient)
143+
var all []*github.RepositoryTag
144+
tagsIter := client.Repositories.ListTagsIter(ctx, owner, repo, &github.ListOptions{PerPage: 100})
145+
for tag, err := range tagsIter {
146+
if err != nil {
147+
return nil, fmt.Errorf("list tags for %q on GitHub: %w (check owner/repo, network, and GitHub API availability or rate limits)", githubRepo, err)
148+
}
149+
all = append(all, tag)
150+
}
151+
return all, nil
152+
}
153+
154+
func parseVersionsYAML(data []byte) (*versionsYAML, error) {
155+
var cfg versionsYAML
156+
if err := yaml.Unmarshal(data, &cfg); err != nil {
157+
return nil, fmt.Errorf("parse versions.yaml: %w", err)
158+
}
159+
return &cfg, nil
160+
}
161+
162+
// validateModuleSetVersionAgainstTagList enforces that, if a tag exists (i.e. latest is not empty),
163+
// then the module set version must be greater than the latest tag.
164+
// If latest is empty, an error is returned unless allowEmptyTag flag is set.
165+
func validateModuleSetVersionAgainstTagList(msVersion, latest, tagName string, allowEmptyTag bool, githubRepo string) error {
166+
if latest == "" && !allowEmptyTag {
167+
return fmt.Errorf("no semver tags found for repo %s", githubRepo)
168+
}
169+
if latest != "" && semver.Compare(msVersion, latest) <= 0 {
170+
return fmt.Errorf("module-sets version %s must be greater than version %s from tag %s",
171+
msVersion, latest, tagName)
172+
}
173+
return nil
174+
}
175+
176+
func formatVersionscheckOK(moduleKey, msVersion, latest, tagName, githubRepo string) string {
177+
if latest == "" {
178+
return fmt.Sprintf("versionscheck: ok (%s version %s; no semver tags in %s)\n", moduleKey, msVersion, githubRepo)
179+
}
180+
return fmt.Sprintf("versionscheck: ok (%s version %s is greater than latest %s in tag %s)\n", moduleKey, msVersion, latest, tagName)
181+
}
182+
183+
// latestComponentVersion returns the highest valid version amongst all components and the complete tag name
184+
// The tags must follow the format "v1.2.3" or "component/v1.2.3" or "path/component/v1.2.3"
185+
func latestComponentVersion(tags []*github.RepositoryTag) (string, string) {
186+
highest := ""
187+
tagName := ""
188+
189+
for _, tag := range tags {
190+
if tag == nil || tag.Name == nil {
191+
continue
192+
}
193+
ver := *tag.Name
194+
if i := strings.LastIndex(ver, "/"); i != -1 {
195+
ver = ver[i+1:]
196+
}
197+
if !semver.IsValid(ver) {
198+
continue
199+
}
200+
canonical := semver.Canonical(ver)
201+
if highest == "" || semver.Compare(canonical, highest) > 0 {
202+
highest = canonical
203+
tagName = *tag.Name
204+
}
205+
}
206+
return highest, tagName
207+
}

0 commit comments

Comments
 (0)