From 0232d9f7c90ff1d9bdf2966e361d12f7c610fbfb Mon Sep 17 00:00:00 2001 From: LYJW131 Date: Tue, 14 Jul 2026 10:48:04 +0800 Subject: [PATCH] ci: publish multi-arch manager image Install the architecture-matching wrapper release at runtime. Co-authored-by: Codex Signed-off-by: LYJW131 --- .github/workflows/wrapper-manager-image.yml | 8 +- Dockerfile | 3 +- wrapper.go | 60 ++++++++--- wrapper_download_test.go | 112 ++++++++++++-------- 4 files changed, 114 insertions(+), 69 deletions(-) diff --git a/.github/workflows/wrapper-manager-image.yml b/.github/workflows/wrapper-manager-image.yml index 7a041cd..fd7ed4a 100644 --- a/.github/workflows/wrapper-manager-image.yml +++ b/.github/workflows/wrapper-manager-image.yml @@ -1,4 +1,4 @@ -name: Build amd64 Docker image +name: Build multi-arch Docker image on: workflow_dispatch: @@ -12,7 +12,7 @@ permissions: packages: write concurrency: - group: wrapper-manager-amd64-${{ github.ref }} + group: wrapper-manager-multiarch-${{ github.ref }} cancel-in-progress: true env: @@ -45,11 +45,11 @@ jobs: type=raw,value=latest,enable={{is_default_branch}} type=sha - - name: Build and publish amd64 image + - name: Build and publish multi-arch image uses: docker/build-push-action@v6 with: context: . - platforms: linux/amd64 + platforms: linux/amd64,linux/arm64 push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} diff --git a/Dockerfile b/Dockerfile index d890000..a81594f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,8 +10,7 @@ COPY proto/go.mod proto/go.sum ./proto/ RUN go mod download COPY . . -RUN test "$TARGETARCH" = "amd64" && \ - CGO_ENABLED=0 GOOS="$TARGETOS" GOARCH="$TARGETARCH" \ +RUN CGO_ENABLED=0 GOOS="$TARGETOS" GOARCH="$TARGETARCH" \ go build -trimpath -ldflags="-s -w" -o /out/wrapper-manager . FROM debian:13.2 diff --git a/wrapper.go b/wrapper.go index fda3a48..7337255 100644 --- a/wrapper.go +++ b/wrapper.go @@ -16,12 +16,32 @@ import ( "strings" ) -const ( - wrapperReleaseTag = "wrapper.x86_64.latest" - wrapperAssetName = "Wrapper.x86_64.latest.zip" -) +type wrapperRelease struct { + tag string + assetName string + archivePath string +} -var wrapperReleaseAPIURL = "https://api.github.com/repos/AMDL-Web/wrapper/releases/tags/" + wrapperReleaseTag +var wrapperReleaseAPIBaseURL = "https://api.github.com/repos/AMDL-Web/wrapper/releases/tags/" + +func wrapperReleaseForArch(arch string) (wrapperRelease, error) { + switch arch { + case "amd64": + return wrapperRelease{ + tag: "wrapper.x86_64.latest", + assetName: "Wrapper.x86_64.latest.zip", + archivePath: "data/wrapper-x86_64.zip", + }, nil + case "arm64": + return wrapperRelease{ + tag: "wrapper.arm64.latest", + assetName: "Wrapper.arm64.latest.zip", + archivePath: "data/wrapper-arm64.zip", + }, nil + default: + return wrapperRelease{}, fmt.Errorf("wrapper auto-install only supports amd64 and arm64, current architecture is %s", arch) + } +} func parseStorefrontID(id string) string { sfID, err := strconv.Atoi(strings.Split(id, "-")[0]) @@ -55,17 +75,17 @@ func PrepareWrapper(mirror bool) error { } func prepareWrapper(mirror bool, arch string) error { - if arch != "amd64" { - return fmt.Errorf("wrapper auto-install only supports x86_64, current architecture is %s", arch) + release, err := wrapperReleaseForArch(arch) + if err != nil { + return err } - wrapperZipPath := "data/wrapper-x86_64.zip" if _, err := os.Stat("data/wrapper/wrapper"); os.IsNotExist(err) { - if _, err := os.Stat(wrapperZipPath); os.IsNotExist(err) { - if err := DownloadWrapperRelease(mirror); err != nil { + if _, err := os.Stat(release.archivePath); os.IsNotExist(err) { + if err := downloadWrapperRelease(mirror, release); err != nil { return err } } - err := unzip.New(wrapperZipPath, "data/wrapper").Extract() + err := unzip.New(release.archivePath, "data/wrapper").Extract() if err != nil { return fmt.Errorf("extract wrapper release: %w", err) } @@ -260,7 +280,15 @@ func RemoveWrapperData(id string) { } func DownloadWrapperRelease(mirror bool) error { - resp, err := GetHttpClient().Get(wrapperReleaseAPIURL) + release, err := wrapperReleaseForArch(runtime.GOARCH) + if err != nil { + return err + } + return downloadWrapperRelease(mirror, release) +} + +func downloadWrapperRelease(mirror bool, expected wrapperRelease) error { + resp, err := GetHttpClient().Get(wrapperReleaseAPIBaseURL + expected.tag) if err != nil { return fmt.Errorf("request wrapper release: %w", err) } @@ -281,13 +309,13 @@ func DownloadWrapperRelease(mirror bool) error { var downloadURL string for _, asset := range release.Assets { - if asset.Name == wrapperAssetName { + if asset.Name == expected.assetName { downloadURL = asset.BrowserDownloadURL break } } if downloadURL == "" { - return fmt.Errorf("wrapper release %s has no %s asset", wrapperReleaseTag, wrapperAssetName) + return fmt.Errorf("wrapper release %s has no %s asset", expected.tag, expected.assetName) } if mirror { downloadURL = strings.Replace(downloadURL, "github.com", "gh-proxy.com/github.com", 1) @@ -304,7 +332,7 @@ func DownloadWrapperRelease(mirror bool) error { if err := os.MkdirAll("data", 0o755); err != nil { return fmt.Errorf("create data directory: %w", err) } - temp, err := os.CreateTemp("data", "wrapper-x86_64-*.zip") + temp, err := os.CreateTemp("data", "wrapper-*.zip") if err != nil { return fmt.Errorf("create wrapper download: %w", err) } @@ -317,7 +345,7 @@ func DownloadWrapperRelease(mirror bool) error { if err := temp.Close(); err != nil { return fmt.Errorf("close wrapper release: %w", err) } - if err := os.Rename(tempPath, "data/wrapper-x86_64.zip"); err != nil { + if err := os.Rename(tempPath, expected.archivePath); err != nil { return fmt.Errorf("finalize wrapper release: %w", err) } return nil diff --git a/wrapper_download_test.go b/wrapper_download_test.go index 30a3173..3c903fa 100644 --- a/wrapper_download_test.go +++ b/wrapper_download_test.go @@ -12,7 +12,7 @@ import ( "testing" ) -func wrapperZip(t *testing.T) []byte { +func wrapperZip(t *testing.T, contents string) []byte { t.Helper() var buffer bytes.Buffer writer := zip.NewWriter(&buffer) @@ -20,7 +20,7 @@ func wrapperZip(t *testing.T) []byte { if err != nil { t.Fatal(err) } - if _, err := file.Write([]byte("x86-wrapper")); err != nil { + if _, err := file.Write([]byte(contents)); err != nil { t.Fatal(err) } if err := writer.Close(); err != nil { @@ -29,71 +29,89 @@ func wrapperZip(t *testing.T) []byte { return buffer.Bytes() } -func TestPrepareWrapperDownloadsExactX86Asset(t *testing.T) { - archive := wrapperZip(t) +func TestPrepareWrapperDownloadsMatchingArchitectureAsset(t *testing.T) { + archives := map[string][]byte{ + "/x86": wrapperZip(t, "x86-wrapper"), + "/arm64": wrapperZip(t, "arm64-wrapper"), + } server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch r.URL.Path { - case "/release": - fmt.Fprintf(w, `{"assets":[{"name":"Wrapper.arm64.latest.zip","browser_download_url":"%s/arm64"},{"name":"%s","browser_download_url":"%s/x86"}]}`, serverURL(r), wrapperAssetName, serverURL(r)) - case "/x86": + if archive, ok := archives[r.URL.Path]; ok { _, _ = w.Write(archive) - default: - http.NotFound(w, r) + return } + fmt.Fprintf(w, `{"assets":[{"name":"Wrapper.arm64.latest.zip","browser_download_url":"%s/arm64"},{"name":"Wrapper.x86_64.latest.zip","browser_download_url":"%s/x86"}]}`, serverURL(r), serverURL(r)) })) defer server.Close() - oldURL := wrapperReleaseAPIURL - wrapperReleaseAPIURL = server.URL + "/release" - defer func() { wrapperReleaseAPIURL = oldURL }() + oldBaseURL := wrapperReleaseAPIBaseURL + wrapperReleaseAPIBaseURL = server.URL + "/" + defer func() { wrapperReleaseAPIBaseURL = oldBaseURL }() - oldWorkingDirectory, err := os.Getwd() - if err != nil { - t.Fatal(err) - } - if err := os.Chdir(t.TempDir()); err != nil { - t.Fatal(err) - } - defer os.Chdir(oldWorkingDirectory) + for _, test := range []struct { + arch string + wantContents string + wantArchivePath string + }{ + {arch: "amd64", wantContents: "x86-wrapper", wantArchivePath: "wrapper-x86_64.zip"}, + {arch: "arm64", wantContents: "arm64-wrapper", wantArchivePath: "wrapper-arm64.zip"}, + } { + t.Run(test.arch, func(t *testing.T) { + oldWorkingDirectory, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + if err := os.Chdir(t.TempDir()); err != nil { + t.Fatal(err) + } + defer os.Chdir(oldWorkingDirectory) - if err := prepareWrapper(false, "amd64"); err != nil { - t.Fatal(err) - } - contents, err := os.ReadFile(filepath.Join("data", "wrapper", "wrapper")) - if err != nil { - t.Fatal(err) - } - if string(contents) != "x86-wrapper" { - t.Fatalf("unexpected wrapper contents: %q", contents) - } - info, err := os.Stat(filepath.Join("data", "wrapper", "wrapper")) - if err != nil { - t.Fatal(err) - } - if info.Mode().Perm()&0o111 == 0 { - t.Fatal("downloaded wrapper is not executable") + if err := prepareWrapper(false, test.arch); err != nil { + t.Fatal(err) + } + contents, err := os.ReadFile(filepath.Join("data", "wrapper", "wrapper")) + if err != nil { + t.Fatal(err) + } + if string(contents) != test.wantContents { + t.Fatalf("unexpected wrapper contents: %q", contents) + } + if _, err := os.Stat(filepath.Join("data", test.wantArchivePath)); err != nil { + t.Fatalf("expected architecture archive: %v", err) + } + info, err := os.Stat(filepath.Join("data", "wrapper", "wrapper")) + if err != nil { + t.Fatal(err) + } + if info.Mode().Perm()&0o111 == 0 { + t.Fatal("downloaded wrapper is not executable") + } + }) } } func TestDownloadWrapperReleaseRequiresExactAsset(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - _, _ = w.Write([]byte(`{"assets":[{"name":"Wrapper.arm64.latest.zip","browser_download_url":"https://example.invalid/arm64"}]}`)) + _, _ = w.Write([]byte(`{"assets":[{"name":"Wrapper.x86_64.latest.zip","browser_download_url":"https://example.invalid/x86"}]}`)) })) defer server.Close() - oldURL := wrapperReleaseAPIURL - wrapperReleaseAPIURL = server.URL - defer func() { wrapperReleaseAPIURL = oldURL }() + oldBaseURL := wrapperReleaseAPIBaseURL + wrapperReleaseAPIBaseURL = server.URL + "/" + defer func() { wrapperReleaseAPIBaseURL = oldBaseURL }() - err := DownloadWrapperRelease(false) - if err == nil || !strings.Contains(err.Error(), wrapperAssetName) { - t.Fatalf("expected missing x86 asset error, got %v", err) + release, err := wrapperReleaseForArch("arm64") + if err != nil { + t.Fatal(err) + } + err = downloadWrapperRelease(false, release) + if err == nil || !strings.Contains(err.Error(), release.assetName) { + t.Fatalf("expected missing arm64 asset error, got %v", err) } } -func TestPrepareWrapperRejectsNonX86(t *testing.T) { - err := prepareWrapper(false, "arm64") - if err == nil || !strings.Contains(err.Error(), "only supports x86_64") { +func TestPrepareWrapperRejectsUnsupportedArchitecture(t *testing.T) { + err := prepareWrapper(false, "riscv64") + if err == nil || !strings.Contains(err.Error(), "supports amd64 and arm64") { t.Fatalf("expected unsupported architecture error, got %v", err) } }