Skip to content

Commit 034287e

Browse files
authored
ci: Rewrite update-versions script in Go (#81)
1 parent 4c9dfa1 commit 034287e

19 files changed

+728
-472
lines changed

.env.example

-1
This file was deleted.

.github/workflows/build.yml

+7-7
Original file line numberDiff line numberDiff line change
@@ -18,22 +18,22 @@ jobs:
1818
uses: actions/checkout@v4
1919
- name: Install Nix
2020
uses: cachix/install-nix-action@V27
21-
- name: Setup Cachix
22-
uses: cachix/cachix-action@v15
2321
with:
24-
name: devenv
22+
nix_path: nixpkgs=channel:nixos-24.05-small
2523
- name: Install devenv
26-
run: nix-env -if https://install.devenv.sh/latest
24+
run: |
25+
nix profile install --accept-flake-config nixpkgs#devenv
26+
devenv version
2727
- name: Run tests
2828
run: devenv test
2929

3030
build:
3131
strategy:
3232
matrix:
3333
os:
34-
- macos-12 # Intel
35-
- macos-14 # M1
36-
- ubuntu-latest
34+
- macos-13 # x86_64-darwin
35+
- macos-latest # aarch64-darwin
36+
- ubuntu-latest # x86_64-linux
3737
fail-fast: false
3838
runs-on: ${{ matrix.os }}
3939
needs: [check]

.github/workflows/update.yml

+12-8
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,20 @@ jobs:
1919
ref: main
2020
- name: Install Nix
2121
uses: cachix/install-nix-action@V27
22-
- name: Setup Cachix
23-
uses: cachix/cachix-action@v15
2422
with:
25-
name: devenv
23+
nix_path: nixpkgs=channel:nixos-24.05-small
2624
- name: Install devenv
27-
run: nix-env -if https://install.devenv.sh/latest
25+
run: |
26+
nix profile install --accept-flake-config nixpkgs#devenv
27+
devenv version
2828
- name: Update versions
29-
run: devenv shell update-versions
29+
run: |
30+
devenv shell -- go run . update-versions \
31+
--versions ../versions.json \
32+
--vendor-hash ../vendor-hash.nix
3033
env:
31-
GITHUB_TOKEN: ${{ secrets.BOT_TOKEN }}
34+
CLI_GITHUB_TOKEN: ${{ secrets.BOT_TOKEN }}
35+
working-directory: cli
3236
- name: Create pull request
3337
uses: peter-evans/create-pull-request@v6
3438
with:
@@ -38,10 +42,10 @@ jobs:
3842
body: |
3943
Automatically created pull-request to update Terraform versions.
4044
41-
This is the result of configuring a GITHUB_TOKEN in `.env` and running:
45+
This is the result of configuring a CLI_GITHUB_TOKEN in `.env` and running:
4246
4347
```
44-
devenv shell update-versions
48+
cli update-versions
4549
```
4650
delete-branch: true
4751
reviewers: |

.gitignore

-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
.devenv*
22
.direnv
3-
.env
43
.pre-commit-config.yaml
54
result
65
templates/*/flake.lock

cli/.env.example

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
CLI_GITHUB_TOKEN=

cli/.envrc

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
source_url "https://raw.githubusercontent.com/cachix/devenv/d1f7b48e35e6dee421cfd0f51481d17f77586997/direnvrc" "sha256-YBzqskFZxmNb3kYVoKD9ZixoPXJh1C9ZvTLGFRkauZ0="
2+
3+
use devenv

cli/.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
.env
2+
cli

cli/README.md

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# CLI
2+
3+
A set of tools for maintainers.
4+
5+
## Requirements
6+
7+
Install [devenv](https://devenv.sh/getting-started/)
8+
9+
## Usage
10+
11+
Change working directory:
12+
13+
```
14+
cd cli
15+
```
16+
17+
Spawn a [nix-shell]:
18+
19+
```
20+
devenv shell
21+
```
22+
23+
Compile code:
24+
25+
```
26+
go build
27+
```
28+
29+
Update versions file:
30+
31+
```
32+
go run . update-versions \
33+
--versions ../versions.json \
34+
--vendor-hash ../vendor-hash.nix
35+
```
36+
37+
[nix-shell]: https://nixos.wiki/wiki/Development_environment_with_nix-shell

cli/cmd/root.go

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package cmd
2+
3+
import (
4+
"os"
5+
6+
"github.com/spf13/cobra"
7+
)
8+
9+
var rootCmd = &cobra.Command{
10+
Use: "cli",
11+
Short: "A set of tools for maintainers",
12+
Long: "A set of tools for maintainers",
13+
}
14+
15+
func Execute() {
16+
err := rootCmd.Execute()
17+
if err != nil {
18+
os.Exit(1)
19+
}
20+
}
21+
22+
func init() {}

cli/cmd/updateVersions.go

+233
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
package cmd
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"log"
8+
"os"
9+
"os/exec"
10+
"path/filepath"
11+
"strings"
12+
13+
"github.com/Masterminds/semver/v3"
14+
"github.com/google/go-github/v62/github"
15+
"github.com/spf13/cobra"
16+
)
17+
18+
var owner string
19+
var repo string
20+
var vendorHashPath string
21+
var versionsPath string
22+
var minVersionStr string
23+
var maxVersionStr string
24+
25+
type Versions struct {
26+
Releases map[semver.Version]Release `json:"releases"`
27+
Latest map[Alias]semver.Version `json:"latest"`
28+
}
29+
30+
type Release struct {
31+
Hash string `json:"hash"`
32+
VendorHash string `json:"vendorHash"`
33+
}
34+
35+
type Alias struct {
36+
semver.Version
37+
}
38+
39+
func (a Alias) MarshalText() ([]byte, error) {
40+
return []byte(fmt.Sprintf("%d.%d", a.Major(), a.Minor())), nil
41+
}
42+
43+
var updateVersionsCmd = &cobra.Command{
44+
Use: "update-versions",
45+
Short: "Update versions file",
46+
Long: "Look up the most recent Terraform releases and calculate the needed hashes for new versions",
47+
Run: func(cmd *cobra.Command, args []string) {
48+
token := os.Getenv("CLI_GITHUB_TOKEN")
49+
if token == "" {
50+
log.Fatal("Environment variable CLI_GITHUB_TOKEN is missing")
51+
}
52+
53+
versionsPath, err := filepath.Abs(versionsPath)
54+
if err != nil {
55+
log.Fatal("File versions.json not found: ", err)
56+
}
57+
58+
vendorHashPath, err := filepath.Abs(vendorHashPath)
59+
if err != nil {
60+
log.Fatal("File vendor-hash.nix not found: ", err)
61+
}
62+
63+
minVersion, err := semver.NewVersion(minVersionStr)
64+
if err != nil {
65+
log.Fatal("Invalid min-version: ", err)
66+
}
67+
68+
var maxVersion *semver.Version
69+
if maxVersionStr != "" {
70+
maxVersion, err = semver.NewVersion(maxVersionStr)
71+
if err != nil {
72+
log.Fatal("Invalid max-version: ", err)
73+
}
74+
}
75+
76+
err = updateVersions(token, versionsPath, vendorHashPath, minVersion, maxVersion)
77+
if err != nil {
78+
log.Fatal("Unable to update versions: ", err)
79+
}
80+
},
81+
}
82+
83+
func updateVersions(token string, versionsPath string, vendorHashPath string, minVersion *semver.Version, maxVersion *semver.Version) error {
84+
nixPrefetchPath, err := exec.LookPath("nix-prefetch")
85+
if err != nil {
86+
return fmt.Errorf("nix-prefetch not found: %w", err)
87+
}
88+
89+
versions, err := readVersions(versionsPath)
90+
if err != nil {
91+
return err
92+
}
93+
94+
err = withReleases(token, func(release *github.RepositoryRelease) error {
95+
tagName := release.GetTagName()
96+
version, err := semver.NewVersion(strings.TrimLeft(tagName, "v"))
97+
if err != nil {
98+
return err
99+
}
100+
if version.Compare(minVersion) >= 0 && (maxVersion == nil || version.Compare(maxVersion) <= 0) && version.Prerelease() == "" {
101+
if _, ok := versions.Releases[*version]; ok {
102+
log.Printf("Version %s found in file\n", version)
103+
} else {
104+
log.Printf("Computing hashes for %s\n", version)
105+
hash, err := computeHash(nixPrefetchPath, tagName)
106+
if err != nil {
107+
return fmt.Errorf("Unable to compute hash: %w", err)
108+
}
109+
log.Printf("Computed hash: %s\n", hash)
110+
vendorHash, err := computeVendorHash(nixPrefetchPath, vendorHashPath, version, hash)
111+
if err != nil {
112+
return fmt.Errorf("Unable to compute vendor hash: %w", err)
113+
}
114+
log.Printf("Computed vendor hash: %s\n", vendorHash)
115+
versions.Releases[*version] = Release{Hash: hash, VendorHash: vendorHash}
116+
}
117+
}
118+
return nil
119+
})
120+
if err != nil {
121+
return err
122+
}
123+
124+
versions.Latest = make(map[Alias]semver.Version)
125+
for version := range versions.Releases {
126+
alias := Alias{*semver.New(version.Major(), version.Minor(), 0, "", "")}
127+
if latest, ok := versions.Latest[alias]; !ok || version.Compare(&latest) > 0 {
128+
versions.Latest[alias] = version
129+
}
130+
}
131+
132+
content, err := json.MarshalIndent(versions, "", " ")
133+
if err != nil {
134+
log.Fatal("Unable to marshall versions: ", err)
135+
}
136+
137+
err = os.WriteFile(versionsPath, content, 0644)
138+
if err != nil {
139+
log.Fatal("Unable to write file: ", err)
140+
}
141+
142+
return nil
143+
}
144+
145+
func readVersions(versionsPath string) (*Versions, error) {
146+
content, err := os.ReadFile(versionsPath)
147+
if err != nil {
148+
return nil, err
149+
}
150+
var versions *Versions
151+
err = json.Unmarshal(content, &versions)
152+
if err != nil {
153+
return nil, err
154+
}
155+
return versions, nil
156+
}
157+
158+
func withReleases(token string, f func(release *github.RepositoryRelease) error) error {
159+
client := github.NewClient(nil).WithAuthToken(token)
160+
opt := &github.ListOptions{Page: 1}
161+
for {
162+
releases, resp, err := client.Repositories.ListReleases(context.Background(), owner, repo, opt)
163+
if err != nil {
164+
return err
165+
}
166+
for _, release := range releases {
167+
err = f(release)
168+
if err != nil {
169+
return err
170+
}
171+
}
172+
if resp.NextPage == 0 {
173+
break
174+
}
175+
opt.Page = resp.NextPage
176+
}
177+
return nil
178+
}
179+
180+
func computeHash(nixPrefetchPath string, tagName string) (string, error) {
181+
hash, err := runNixPrefetch(
182+
nixPrefetchPath,
183+
"fetchFromGitHub",
184+
"--owner",
185+
owner,
186+
"--repo",
187+
repo,
188+
"--rev",
189+
tagName)
190+
if err != nil {
191+
return "", err
192+
}
193+
return hash, nil
194+
}
195+
196+
func computeVendorHash(nixPrefetchPath string, vendorHashFile string, version *semver.Version, hash string) (string, error) {
197+
vendorHash, err := runNixPrefetch(
198+
nixPrefetchPath,
199+
"--file",
200+
vendorHashFile,
201+
"--argstr",
202+
"version",
203+
version.String(),
204+
"--argstr",
205+
"hash",
206+
hash)
207+
if err != nil {
208+
return "", err
209+
}
210+
return vendorHash, nil
211+
}
212+
213+
func runNixPrefetch(nixPrefetchPath string, extraArgs ...string) (string, error) {
214+
args := append([]string{"--option", "extra-experimental-features", "flakes"}, extraArgs...)
215+
cmd := exec.Command(nixPrefetchPath, args...)
216+
cmd.Stderr = log.Writer()
217+
output, err := cmd.Output()
218+
if err != nil {
219+
return "", err
220+
}
221+
return strings.TrimRight(string(output), "\n"), nil
222+
}
223+
224+
func init() {
225+
rootCmd.AddCommand(updateVersionsCmd)
226+
227+
updateVersionsCmd.Flags().StringVarP(&owner, "owner", "", "hashicorp", "The owner name of the repository on GitHub")
228+
updateVersionsCmd.Flags().StringVarP(&repo, "repo", "", "terraform", "The repository name on GitHub")
229+
updateVersionsCmd.Flags().StringVarP(&vendorHashPath, "vendor-hash", "", "vendor-hash.nix", "Nix file required to compute vendorHash")
230+
updateVersionsCmd.Flags().StringVarP(&versionsPath, "versions", "", "versions.json", "The file to be updated")
231+
updateVersionsCmd.Flags().StringVarP(&minVersionStr, "min-version", "", "1.0.0", "Min release version")
232+
updateVersionsCmd.Flags().StringVarP(&maxVersionStr, "max-version", "", "", "Max release version")
233+
}

0 commit comments

Comments
 (0)