diff --git a/.github/workflows/test-update.yml b/.github/workflows/test-update.yml new file mode 100644 index 00000000..165a7a17 --- /dev/null +++ b/.github/workflows/test-update.yml @@ -0,0 +1,31 @@ +name: Build & Update with Arduino CLI + +on: + push: + branches: + - main + - test_package_update + workflow_dispatch: + +permissions: + contents: read + + +jobs: + build-and-update: + runs-on: ubuntu-22.04 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + - name: Run dep package update test + env: + GH_TOKEN: ${{ secrets.ARDUINOBOT_TOKEN }} + run: | + go test -v ./internal/testtools/deb_test.go --arch amd64 \ No newline at end of file diff --git a/Taskfile.yml b/Taskfile.yml index 06e54550..82f91b35 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -12,6 +12,9 @@ vars: RUNNER_VERSION: "0.5.0" VERSION: # if version is not passed we hack the semver by encoding the commit as pre-release sh: echo "${VERSION:-0.0.0-$(git rev-parse --short HEAD)}" + NEW_PACKAGE: + sh: ls -1 ./build/arduino-app-cli_*.deb 2>/dev/null | head -n 1 + GITHUB_TOKEN_FILE: ./github_token.txt tasks: init: @@ -102,9 +105,10 @@ tasks: deps: - build-deb:clone-examples cmds: - - docker build --build-arg BINARY_NAME=arduino-app-cli --build-arg DEB_NAME=arduino-app-cli --build-arg VERSION={{ .VERSION }} --build-arg ARCH={{ .ARCH }} --build-arg RELEASE={{ .RELEASE }} --output=./build -f debian/Dockerfile . + - docker build --build-arg BINARY_NAME=arduino-app-cli --build-arg DEB_NAME=arduino-app-cli --build-arg VERSION={{ .VERSION }} --build-arg ARCH={{ .ARCH }} --build-arg RELEASE={{ .RELEASE }} --output={{ .OUTPUT }} -f debian/Dockerfile . vars: ARCH: '{{.ARCH | default "arm64"}}' + OUTéPUT: '{{.OUTPUT | default "./build"}}' build-deb:clone-examples: desc: "Clones the examples repo directly into the debian structure" @@ -123,6 +127,44 @@ tasks: echo "Examples successfully cloned." silent: false + build-image: + desc: "Builds the mock-repo Docker image (requires GITHUB_TOKEN_FILE)" + deps: [build-deb] + vars: + PKG_PATH: "{{.NEW_PACKAGE}}" + cmds: + # --- MODIFIED --- + # Check for both the package and the token file + - | + if [ ! -f "{{.GITHUB_TOKEN_FILE}}" ]; then + echo "Error: GitHub token file not found at {{.GITHUB_TOKEN_FILE}}" + echo "Please create this file and add your GitHub PAT to it." + exit 1 + fi + - | + echo "Using package: {{.PKG_PATH}}" + echo "Using GitHub token from: {{.GITHUB_TOKEN_FILE}}" + + # Enable BuildKit and pass the secret + DOCKER_BUILDKIT=1 docker build \ + --secret id=github_token,src={{.GITHUB_TOKEN_FILE}} \ + --build-arg NEW_PACKAGE_PATH={{.PKG_PATH}} \ + -t newdeb \ + -f test.Dockerfile . + status: + - '[[ -f "{{.PKG_PATH}}" ]]' + - '[[ -f "{{.DOCKERFILE_NAME}}" ]]' + # Re-build if token file changes + - '[[ -f "{{.GITHUB_TOKEN_FILE}}" ]]' + + test-deb: + desc: Test the debian package locally + deps: + - build-deb + cmds: + - docker build --no-cache -t mock-apt-repo -f test.Dockerfile . + - docker run --rm -it --privileged -v /sys/fs/cgroup:/sys/fs/cgroup:ro --name apt-test-update mock-apt-repo + arduino-app-cli:build:local: desc: "Build the arduino-app-cli locally" cmds: diff --git a/internal/testtools/deb_test.go b/internal/testtools/deb_test.go new file mode 100644 index 00000000..af813bce --- /dev/null +++ b/internal/testtools/deb_test.go @@ -0,0 +1,264 @@ +package testtools + +import ( + "bytes" + "flag" + "fmt" + "io" + "log" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +var arch = flag.String("arch", "amd64", "target architecture") + +func TestStableToUnstable(t *testing.T) { + tagAppCli := FetchDebPackage(t, "arduino-app-cli", "latest", *arch) + FetchDebPackage(t, "arduino-router", "latest", *arch) + majorTag := majorTag(t, tagAppCli) + _ = minorTag(t, tagAppCli) + + fmt.Printf("Updating from stable version %s to unstable version %s \n", tagAppCli, majorTag) + fmt.Printf("Building local deb version %s \n", majorTag) + buildDebVersion(t, majorTag, *arch) + + fmt.Println("**** BUILD docker image *****") + buildDockerImage(t, "test.Dockerfile", "test-apt-update") + fmt.Println("**** RUN docker image *****") + runDockerCommand(t, "test-apt-update") + preUpdateVersion := runDockerSystemVersion(t, "apt-test-update") + runDockerSystemUpdate(t, "apt-test-update") + postUpdateVersion := runDockerSystemVersion(t, "apt-test-update") + runDockerCleanUp(t, "apt-test-update") + require.Equal(t, preUpdateVersion, "Arduino App CLI "+tagAppCli) + require.Equal(t, postUpdateVersion, "Arduino App CLI "+majorTag) + +} + +func TestUnstableToStable(t *testing.T) { + tagAppCli := FetchDebPackage(t, "arduino-app-cli", "latest", *arch) + FetchDebPackage(t, "arduino-router", "latest", *arch) + minorTag := minorTag(t, tagAppCli) + moveDeb(t, "build/stable", "build/", "arduino-app-cli", tagAppCli, *arch) + + fmt.Printf("Updating from unstable version %s to stable version %s \n", minorTag, tagAppCli) + fmt.Printf("Building local deb version %s \n", minorTag) + buildDebVersion(t, minorTag, *arch) + moveDeb(t, "build/", "build/stable", "arduino-app-cli", tagAppCli, *arch) + + fmt.Println("**** BUILD docker image *****") + buildDockerImage(t, "test.Dockerfile", "test-apt-update") + fmt.Println("**** RUN docker image *****") + runDockerCommand(t, "test-apt-update") + preUpdateVersion := runDockerSystemVersion(t, "apt-test-update") + runDockerSystemUpdate(t, "apt-test-update") + postUpdateVersion := runDockerSystemVersion(t, "apt-test-update") + runDockerCleanUp(t, "apt-test-update") + require.Equal(t, preUpdateVersion, "Arduino App CLI "+tagAppCli) + require.Equal(t, postUpdateVersion, "Arduino App CLI "+minorTag) + +} + +func FetchDebPackage(t *testing.T, repo, version, arch string) string { + t.Helper() + + cmd := exec.Command( + "gh", "release", "list", + "--repo", "github.com/arduino/"+repo, + "--exclude-pre-releases", + "--limit", "1", + ) + + output, err := cmd.CombinedOutput() + if err != nil { + log.Fatalf("command failed: %v\nOutput: %s", err, output) + } + + fmt.Println(string(output)) + + fields := strings.Fields(string(output)) + if len(fields) == 0 { + log.Fatal("could not parse tag from gh release list output") + } + tag := fields[0] + tagPath := strings.TrimPrefix(tag, "v") + + debFile := fmt.Sprintf("build/stable/%s_%s-1_%s.deb", repo, tagPath, arch) + fmt.Println(debFile) + if _, err := os.Stat(debFile); err == nil { + fmt.Printf("✅ %s already exists, skipping download.\n", debFile) + return tag + } + fmt.Println("Detected tag:", tag) + cmd2 := exec.Command( + "gh", "release", "download", + tag, + "--repo", "github.com/arduino/"+repo, + "--pattern", "*", + "--dir", "./build/stable", + ) + + out, err := cmd2.CombinedOutput() + if err != nil { + log.Fatalf("download failed: %v\nOutput: %s", err, out) + } + + return tag + +} + +func buildDebVersion(t *testing.T, tagVersion, arch string) { + t.Helper() + cwd, err := os.Getwd() + if err != nil { + panic(err) + } + outputDir := filepath.Join(cwd, "build") + + cmd := exec.Command( + "go", "tool", "task", "build-deb", + fmt.Sprintf("VERSION=%s", tagVersion), + fmt.Sprintf("ARCH=%s", arch), + fmt.Sprintf("OUTPUT=%s", outputDir), + ) + + if err := cmd.Run(); err != nil { + log.Fatalf("failed to run build command: %v", err) + } +} + +func majorTag(t *testing.T, tag string) string { + t.Helper() + + parts := strings.Split(tag, ".") + last := parts[len(parts)-1] + + // Remove potential prefix 'v' from the first part, but not from the patch + lastNum, _ := strconv.Atoi(strings.TrimPrefix(last, "v")) + lastNum++ + + parts[len(parts)-1] = strconv.Itoa(lastNum) + newTag := strings.Join(parts, ".") + + return newTag +} + +func minorTag(t *testing.T, tag string) string { + t.Helper() + + parts := strings.Split(tag, ".") + last := parts[len(parts)-1] + + lastNum, _ := strconv.Atoi(strings.TrimPrefix(last, "v")) + if lastNum > 0 { + lastNum-- + } + + parts[len(parts)-1] = strconv.Itoa(lastNum) + newTag := strings.Join(parts, ".") + + if !strings.HasPrefix(newTag, "v") { + newTag = "v" + newTag + } + return newTag +} + +func buildDockerImage(t *testing.T, dockerfile, name string) { + t.Helper() + + cmd := exec.Command("docker", "build", "-t", name, "-f", dockerfile, ".") + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("docker build failed: %v\nOutput:\n%s", err, string(out)) + } + +} + +func runDockerCommand(t *testing.T, containerImageName string) { + t.Helper() + + cmd := exec.Command( + "docker", "run", "--rm", "-d", + "--privileged", + "--cgroupns=host", + "-v", "/sys/fs/cgroup:/sys/fs/cgroup:rw", + "-v", "/var/run/docker.sock:/var/run/docker.sock", + "-e", "DOCKER_HOST=unix:///var/run/docker.sock", + "--name", "apt-test-update", + containerImageName, + ) + + if err := cmd.Run(); err != nil { + t.Fatalf("failed to run container: %v", err) + } + +} + +func runDockerSystemVersion(t *testing.T, containerName string) string { + t.Helper() + + cmd := exec.Command( + "docker", "exec", + "--user", "arduino", + containerName, + "arduino-app-cli", "version", + ) + + output, err := cmd.CombinedOutput() + if err != nil { + log.Fatalf("command failed: %v\nOutput: %s", err, output) + } + + return string(output) + +} + +func runDockerSystemUpdate(t *testing.T, containerName string) { + t.Helper() + var buf bytes.Buffer + + cmd := exec.Command( + "docker", "exec", + containerName, + "sh", "-lc", + `su - arduino -c "yes | arduino-app-cli system update"`, + ) + + cmd.Stdout = io.MultiWriter(os.Stdout, &buf) + + if err := cmd.Run(); err != nil { + fmt.Fprintf(os.Stderr, "Error running system update: %v\n", err) + os.Exit(1) + } + +} + +func runDockerCleanUp(t *testing.T, containerName string) { + + cleanupCmd := exec.Command("docker", "rm", "-f", containerName) + + fmt.Println("🧹 Removing Docker container " + containerName) + if err := cleanupCmd.Run(); err != nil { + fmt.Printf("⚠️ Warning: could not remove container (might not exist): %v\n", err) + } + +} + +func moveDeb(t *testing.T, startDir, targetDir, repo string, tagVersion string, arch string) { + tagPath := strings.TrimPrefix(tagVersion, "v") + + debFile := fmt.Sprintf("%s/%s_%s-1_%s.deb", startDir, repo, tagPath, arch) + + moveCmd := exec.Command("mv", debFile, targetDir) + + fmt.Printf("📦 Moving %s → %s\n", debFile, targetDir) + if err := moveCmd.Run(); err != nil { + panic(fmt.Errorf("failed to move deb file: %w", err)) + } +} diff --git a/internal/testtools/package_update.go b/internal/testtools/package_update.go new file mode 100644 index 00000000..f0fd39d7 --- /dev/null +++ b/internal/testtools/package_update.go @@ -0,0 +1,38 @@ +// This file is part of arduino-app-cli. +// +// Copyright 2025 ARDUINO SA (http://www.arduino.cc/) +// +// This software is released under the GNU General Public License version 3, +// which covers the main part of arduino-app-cli. +// The terms of this license can be found at: +// https://www.gnu.org/licenses/gpl-3.0.en.html +// +// You can be released from the requirements of the above licenses by purchasing +// a commercial license. Buying such a license is mandatory if you want to +// modify or otherwise use the software for commercial activities involving the +// Arduino software without disclosing the source code of your own applications. +// To purchase a commercial license, send an email to license@arduino.cc. +package testtools + +import ( + "os" + "os/exec" + "runtime" + "testing" +) + +func DockerBuild(t *testing.T) { + + if runtime.GOOS != "linux" && os.Getenv("CI") != "" { + t.Skip("Skipping tests in CI that requires docker on non-Linux systems") + } + t.Helper() + + cmd := exec.Command("docker", "build", "-t", "adbd", "-f", "test.Dockerfile", ".") + cmd.Dir = getBaseProjectPath(t) + err := cmd.Run() + if err != nil { + t.Fatalf("failed to build adb daemon: %v", err) + } + +} diff --git a/internal/testtools/test.Dockerfile b/internal/testtools/test.Dockerfile new file mode 100644 index 00000000..e95a7689 --- /dev/null +++ b/internal/testtools/test.Dockerfile @@ -0,0 +1,32 @@ +FROM debian:trixie + +RUN apt update && \ + apt install -y systemd systemd-sysv dbus \ + sudo docker.io ca-certificates curl gnupg \ + dpkg-dev apt-utils adduser gzip && \ + rm -rf /var/lib/apt/lists/* + +ARG ARCH=arm64 + +COPY build/stable/arduino-app-cli*.deb /tmp/stable.deb +COPY build/arduino-app-cli*.deb /tmp/unstable.deb +COPY build/stable/arduino-router*.deb /tmp/router.deb + +RUN apt update && apt install -y /tmp/stable.deb /tmp/router.deb \ + && rm /tmp/stable.deb /tmp/router.deb \ + && mkdir -p /var/www/html/myrepo/dists/trixie/main/binary-${ARCH} \ + && mv /tmp/unstable.deb /var/www/html/myrepo/dists/trixie/main/binary-${ARCH}/ + +WORKDIR /var/www/html/myrepo +RUN dpkg-scanpackages dists/trixie/main/binary-${ARCH} /dev/null | gzip -9c > dists/trixie/main/binary-${ARCH}/Packages.gz +WORKDIR / + +RUN usermod -s /bin/bash arduino || true +RUN mkdir -p /home/arduino && chown -R arduino:arduino /home/arduino +RUN usermod -aG docker arduino + +RUN echo "deb [trusted=yes arch=${ARCH}] file:/var/www/html/myrepo trixie main" \ + > /etc/apt/sources.list.d/my-mock-repo.list + +# CMD: systemd must be PID 1 +CMD ["/sbin/init"]