From dc573b859897e340048ab5c3a571acda37dd2fa9 Mon Sep 17 00:00:00 2001 From: Ashraf Fouda Date: Tue, 28 Jul 2026 13:51:28 +0300 Subject: [PATCH] fix(update-worker): make version gating testable and fix CI Extract the link-update logic into Worker.applyVersion(network, chainVersion) and lift ChainVersion to package scope, so the safe_to_upgrade gating can be unit-tested deterministically instead of against live chains. Replace the live-chain TestWorker cases (whose pass/fail now depends on the live safe_to_upgrade flag) with deterministic applyVersion tests plus an unreachable-endpoint check. Modernize .github/workflows/zos-update-worker-main.yml: the repo's .golangci.yml is now v2 but the workflow used golangci-lint-action@v3 (v1), failing with "v2 config with v1". Bump to golangci-lint-action@v6 pinned to golangci-lint v2.1.6, setup-go@v5 with go 1.23 (the module requires go 1.21), checkout@v4, and staticcheck 2025.1.1. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/zos-update-worker-main.yml | 19 ++-- .../internal/update_worker.go | 29 +++-- .../zos-update-worker/internal/worker_test.go | 105 ++++++++++-------- 3 files changed, 84 insertions(+), 69 deletions(-) diff --git a/.github/workflows/zos-update-worker-main.yml b/.github/workflows/zos-update-worker-main.yml index 516ecefa8..2cc24b036 100644 --- a/.github/workflows/zos-update-worker-main.yml +++ b/.github/workflows/zos-update-worker-main.yml @@ -17,24 +17,25 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out repository code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install GO - uses: actions/setup-go@v3 - with: - go-version: 1.19 + uses: actions/setup-go@v5 + with: + go-version: "1.23" - name: golangci-lint - uses: golangci/golangci-lint-action@v3 + uses: golangci/golangci-lint-action@v7 with: + version: v2.1.6 args: --timeout 3m --verbose - working-directory: tools/zos-update-worker + working-directory: tools/zos-update-worker - name: staticcheck - uses: dominikh/staticcheck-action@v1.3.0 + uses: dominikh/staticcheck-action@v1.4.0 with: - version: "2022.1.3" - working-directory: tools/zos-update-worker + version: "2025.1.1" + working-directory: tools/zos-update-worker env: GO111MODULE: on diff --git a/tools/zos-update-worker/internal/update_worker.go b/tools/zos-update-worker/internal/update_worker.go index e07c40211..3b2e420f7 100644 --- a/tools/zos-update-worker/internal/update_worker.go +++ b/tools/zos-update-worker/internal/update_worker.go @@ -92,6 +92,13 @@ func checkNetwork(network Network) error { return nil } +// ChainVersion is the zos version document published on chain. +type ChainVersion struct { + SafeToUpgrade bool `json:"safe_to_upgrade"` + Version string `json:"version"` + VersionLight string `json:"version_light"` +} + // updateZosVersion updates the latest zos flist for a specific network with the updated zos version func (w *Worker) updateZosVersion(network Network, manager client.Manager) error { if err := checkNetwork(network); err != nil { @@ -109,24 +116,22 @@ func (w *Worker) updateZosVersion(network Network, manager client.Manager) error return err } - type ChainVersion struct { - SafeToUpgrade bool `json:"safe_to_upgrade"` - Version string `json:"version"` - VersionLight string `json:"version_light"` - } - var chainVersion ChainVersion - err = json.Unmarshal([]byte(currentZosVersion), &chainVersion) - if err != nil { + if err := json.Unmarshal([]byte(currentZosVersion), &chainVersion); err != nil { log.Debug().Err(err).Msg("failed to unmarshal chain version") // shouldn't fail for env that still not updated version format return nil } - // During a canary rollout (safe_to_upgrade == false) the version is delivered only to - // the configured test farms by the node upgrader. Keep the network `latest` symlink - // pointing at the last GA version so freshly bootstrapped nodes (and non-canary nodes) - // don't pick up the canary version. + return w.applyVersion(network, chainVersion) +} + +// applyVersion points the network `latest` flist links at chainVersion, but only when the +// chain marks it safe_to_upgrade. During a canary rollout (safe_to_upgrade == false) the +// link is held at the last GA version so freshly bootstrapped nodes and non-canary nodes +// don't pick up the canary version; canary farms receive it via the node upgrader, which +// targets the chain version tag directly. +func (w *Worker) applyVersion(network Network, chainVersion ChainVersion) error { if !chainVersion.SafeToUpgrade { log.Debug().Msgf("skipping %v latest link update: version %v is not marked safe to upgrade yet", network, chainVersion.Version) return nil diff --git a/tools/zos-update-worker/internal/worker_test.go b/tools/zos-update-worker/internal/worker_test.go index 9efe65026..f549975e0 100644 --- a/tools/zos-update-worker/internal/worker_test.go +++ b/tools/zos-update-worker/internal/worker_test.go @@ -2,78 +2,87 @@ package internal import ( "os" + "path/filepath" "testing" "time" + + client "github.com/threefoldtech/substrate-client" ) -func TestWorker(t *testing.T) { +func newTestWorker(t *testing.T) *Worker { + t.Helper() testDir := t.TempDir() + src := filepath.Join(testDir, "tf-autobuilder") + dst := filepath.Join(testDir, "tf-zos") - params := Params{ - Interval: 1 * time.Second, - QAUrls: []string{"wss://tfchain.qa.grid.tf/ws"}, - TestUrls: []string{"wss://tfchain.test.grid.tf/ws"}, - MainUrls: []string{"wss://tfchain.grid.tf/ws"}, + if err := os.Mkdir(src, os.ModePerm); err != nil { + t.Fatal(err) } - src := testDir + "/tf-autobuilder" - dst := testDir + "/tf-zos" - - err := os.Mkdir(src, os.ModePerm) - if err != nil { - t.Error(err) - } - - err = os.Mkdir(dst, os.ModePerm) - if err != nil { - t.Error(err) + if err := os.Mkdir(dst, os.ModePerm); err != nil { + t.Fatal(err) } - worker, err := NewWorker(src, dst, params) + worker, err := NewWorker(src, dst, Params{Interval: time.Second}) if err != nil { - t.Error(err) + t.Fatal(err) } + return worker +} - t.Run("test_no_src_qa", func(t *testing.T) { - err := worker.updateZosVersion("qa", worker.substrate["qa"]) - if err == nil { - t.Errorf("update zos should fail") +func TestApplyVersion(t *testing.T) { + // During a canary (safe_to_upgrade == false) the latest link must NOT be advanced, + // even if the source flist exists, so new/non-canary nodes stay on GA. + t.Run("gated when not safe to upgrade", func(t *testing.T) { + worker := newTestWorker(t) + const version = "v3.1.1" + if err := os.Mkdir(filepath.Join(worker.src, ".tag-"+version), os.ModePerm); err != nil { + t.Fatal(err) } - }) - t.Run("test_no_src_test", func(t *testing.T) { - _, err := os.Create(src + "/zos:v3.4.0-qa1.flist") - if err != nil { - t.Error(err) + if err := worker.applyVersion(MainNetwork, ChainVersion{Version: version, SafeToUpgrade: false}); err != nil { + t.Fatalf("expected no error when gated, got %v", err) } - - err = worker.updateZosVersion("testing", worker.substrate["testing"]) - if err == nil { - t.Errorf("update zos should fail for test, %v", err) + if _, err := os.Lstat(filepath.Join(worker.dst, string(MainNetwork))); !os.IsNotExist(err) { + t.Fatalf("expected no latest link to be created while gated") } }) - t.Run("test_no_src_main", func(t *testing.T) { - _, err = os.Create(src + "/zos:v3.1.1-rc2.flist") - if err != nil { - t.Error(err) - } - - err = worker.updateZosVersion("production", worker.substrate["production"]) - if err == nil { - t.Errorf("update zos should fail for main, %v", err) + // safe_to_upgrade but the source flist is missing: the link update must fail. + t.Run("fails when safe but source missing", func(t *testing.T) { + worker := newTestWorker(t) + if err := worker.applyVersion(MainNetwork, ChainVersion{Version: "v3.1.1", SafeToUpgrade: true}); err == nil { + t.Fatalf("expected error when the source flist is missing") } }) - t.Run("test_params_wrong_url", func(t *testing.T) { - params.QAUrls = []string{"wss://tfchain.qa1.grid.tf/ws"} + // safe_to_upgrade and the source exists: the latest link is created pointing at it. + t.Run("links latest when safe and source exists", func(t *testing.T) { + worker := newTestWorker(t) + const version = "v3.1.1" + if err := os.Mkdir(filepath.Join(worker.src, ".tag-"+version), os.ModePerm); err != nil { + t.Fatal(err) + } - worker, err = NewWorker(src, dst, params) + if err := worker.applyVersion(MainNetwork, ChainVersion{Version: version, SafeToUpgrade: true}); err != nil { + t.Fatalf("expected success, got %v", err) + } + target, err := os.Readlink(filepath.Join(worker.dst, string(MainNetwork))) if err != nil { - t.Error(err) + t.Fatalf("expected latest link to be created: %v", err) } - err := worker.updateZosVersion("qa", worker.substrate["qa"]) - if err == nil { - t.Errorf("update zos should fail") + if filepath.Base(target) != ".tag-"+version { + t.Fatalf("expected link to point at .tag-%s, got %s", version, target) } }) } + +// TestUpdateZosVersionUnreachable checks the connect/fetch path reports an error when the +// substrate endpoint is unreachable. +func TestUpdateZosVersionUnreachable(t *testing.T) { + worker := newTestWorker(t) + worker.substrate[QANetwork] = client.NewManager("wss://tfchain.qa1.grid.tf/ws") + + if err := worker.updateZosVersion(QANetwork, worker.substrate[QANetwork]); err == nil { + t.Fatalf("expected updateZosVersion to fail against an unreachable substrate url") + } +}