From 5f00fca16628a1b360b7437786bc7956154bc9bf Mon Sep 17 00:00:00 2001 From: kolaente Date: Wed, 27 May 2026 11:05:04 +0200 Subject: [PATCH] feat(veans): build and publish veans alongside vikunja MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cross-compile veans for the same OS/arch matrix as the main vikunja binary, wrap each into a signed zip, build deb/rpm/apk/archlinux packages via nfpm, and merge those into the existing dl.vikunja.io package repos so `apt install veans` works from the same source. - veans/magefile.go: Release namespace (xgo cross-compile, upx, sha256, per-target zip bundle, nfpm.yaml templating). - veans/nfpm.yaml: minimal — binary at /usr/local/bin/veans, no service or postinstall. - .github/workflows/release.yml: veans-binaries + veans-os-package jobs, veans artifacts merged into publish-repos and create-release. S3 layout mirrors vikunja under /veans//. --- .github/workflows/release.yml | 199 ++++++++++++++ veans/.gitignore | 1 + veans/magefile.go | 473 +++++++++++++++++++++++++++++++++- veans/nfpm.yaml | 16 ++ 4 files changed, 682 insertions(+), 7 deletions(-) create mode 100644 veans/nfpm.yaml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d86576881..f019417ef 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -141,6 +141,77 @@ jobs: name: vikunja_bin_packages path: ./dist/zip/* + veans-binaries: + runs-on: blacksmith-8vcpu-ubuntu-2204 + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 + - name: Git describe + id: ghd + uses: proudust/gh-describe@v2 + - uses: useblacksmith/setup-go@647ac649bd5b480f2a262e3e3e5f4d150ed452ad # v6 + with: + go-version: stable + - name: Install mage + # The cached mage-static artifact has the parent magefile compiled + # in — we need a generic mage to pick up veans/magefile.go. + run: go install github.com/magefile/mage@v1.17.2 + - name: install upx + run: | + wget https://github.com/upx/upx/releases/download/v5.0.0/upx-5.0.0-amd64_linux.tar.xz + echo 'b32abf118d721358a50f1aa60eacdbf3298df379c431c3a86f139173ab8289a1 upx-5.0.0-amd64_linux.tar.xz' > upx-5.0.0-amd64_linux.tar.xz.sha256 + sha256sum -c upx-5.0.0-amd64_linux.tar.xz.sha256 + tar xf upx-5.0.0-amd64_linux.tar.xz + mv upx-5.0.0-amd64_linux/upx /usr/local/bin + - name: setup xgo cache + uses: useblacksmith/cache@71c7c918062ba3861252d84b07fe5ab2a6b467a6 # v5 + with: + path: /home/runner/.xgo-cache + key: veans-${{ hashFiles('veans/go.sum') }} + restore-keys: | + veans-${{ runner.os }}-go- + - name: build and release + working-directory: veans + env: + RELEASE_VERSION: ${{ steps.ghd.outputs.describe }} + XGO_OUT_NAME: veans-${{ github.ref_type == 'tag' && steps.ghd.outputs.describe || 'unstable' }} + run: | + export PATH=$PATH:$GOPATH/bin + mage release + - name: GPG setup + uses: kolaente/action-gpg@main + with: + gpg-passphrase: "${{ secrets.RELEASE_GPG_PASSPHRASE }}" + gpg-sign-key: "${{ secrets.RELEASE_GPG_SIGN_KEY }}" + - name: sign + working-directory: veans + run: | + ls -hal dist/zip/* + for file in dist/zip/*; do + gpg -v --default-key 7D061A4AA61436B40713D42EFF054DACD908493A -b --batch --yes --passphrase "${{ secrets.RELEASE_GPG_PASSPHRASE }}" --pinentry-mode loopback --sign "$file" + done + - name: Upload + uses: kolaente/s3-action@main + with: + s3-access-key-id: ${{ secrets.S3_ACCESS_KEY }} + s3-secret-access-key: ${{ secrets.S3_SECRET_KEY }} + s3-endpoint: ${{ secrets.S3_ENDPOINT }} + s3-bucket: ${{ secrets.S3_BUCKET }} + s3-region: ${{ secrets.S3_REGION }} + target-path: /veans/${{ github.ref_type == 'tag' && steps.ghd.outputs.describe || 'unstable' }} + files: "veans/dist/zip/*" + strip-path-prefix: veans/dist/zip/ + - name: Store Binaries + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 + with: + name: veans_bins + path: ./veans/dist/binaries/* + - name: Store Binary Packages + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 + if: ${{ github.ref_type == 'tag' }} + with: + name: veans_bin_packages + path: ./veans/dist/zip/* + os-package: runs-on: ubuntu-latest needs: @@ -231,10 +302,110 @@ jobs: name: vikunja_os_package_${{ matrix.package }}_${{ matrix.arch.pkg }} path: ./dist/os-packages/* + veans-os-package: + runs-on: ubuntu-latest + needs: + - veans-binaries + strategy: + matrix: + package: + - rpm + - deb + - apk + - archlinux + arch: + - go_name: linux-amd64 + nfpm: amd64 + pkg: x86_64 + - go_name: linux-arm64 + nfpm: arm64 + pkg: aarch64 + - go_name: linux-arm-7 + nfpm: arm7 + pkg: armv7 + + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 + - name: Download Veans Binaries + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 + with: + name: veans_bins + path: ./veans-binaries + - name: Git describe + id: ghd + uses: proudust/gh-describe@v2 + - uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6 + with: + go-version: stable + - name: Install mage + # Generic mage to pick up veans/magefile.go (the cached mage-static + # has the parent magefile compiled in). + run: go install github.com/magefile/mage@v1.17.2 + - name: Write GPG key for nfpm + if: matrix.package == 'rpm' + run: echo -n "${{ secrets.RELEASE_GPG_SIGN_KEY }}" > /tmp/nfpm-signing-key.gpg + - name: GPG setup for package signing + if: matrix.package == 'archlinux' + uses: kolaente/action-gpg@main + with: + gpg-passphrase: "${{ secrets.RELEASE_GPG_PASSPHRASE }}" + gpg-sign-key: "${{ secrets.RELEASE_GPG_SIGN_KEY }}" + - name: Prepare + env: + RELEASE_VERSION: ${{ steps.ghd.outputs.describe }} + NFPM_ARCH: ${{ matrix.arch.nfpm }} + # The nfpm action runs from $GITHUB_WORKSPACE while the source dir + # is also called ./veans — stage the binary under a distinct name + # so the two don't collide. + NFPM_BIN_PATH: ./veans/veans-bin + working-directory: veans + run: | + export PATH=$PATH:$GOPATH/bin + mage release:prepare-nfpm-config + mkdir -p ./dist/os-packages + mv ../veans-binaries/veans-*-${{ matrix.arch.go_name }} ./veans-bin + chmod +x ./veans-bin + - name: Create package + id: nfpm + uses: kolaente/action-gh-nfpm@master + with: + packager: ${{ matrix.package }} + target: ./veans/dist/os-packages/veans-${{ github.ref_type == 'tag' && steps.ghd.outputs.describe || 'unstable' }}-${{ matrix.arch.pkg }}.${{ matrix.package }} + config: ./veans/nfpm.yaml + env: + NFPM_GPG_KEY_FILE: ${{ (matrix.package == 'rpm') && '/tmp/nfpm-signing-key.gpg' || '' }} + NFPM_PASSPHRASE: ${{ (matrix.package == 'rpm') && secrets.RELEASE_GPG_PASSPHRASE || '' }} + - name: Sign package + if: matrix.package == 'archlinux' + run: | + gpg --default-key 7D061A4AA61436B40713D42EFF054DACD908493A \ + --batch --yes \ + --passphrase "${{ secrets.RELEASE_GPG_PASSPHRASE }}" \ + --pinentry-mode loopback \ + --detach-sign \ + ./veans/dist/os-packages/veans-${{ github.ref_type == 'tag' && steps.ghd.outputs.describe || 'unstable' }}-${{ matrix.arch.pkg }}.${{ matrix.package }} + - name: Upload + uses: kolaente/s3-action@main + with: + s3-access-key-id: ${{ secrets.S3_ACCESS_KEY }} + s3-secret-access-key: ${{ secrets.S3_SECRET_KEY }} + s3-endpoint: ${{ secrets.S3_ENDPOINT }} + s3-bucket: ${{ secrets.S3_BUCKET }} + s3-region: ${{ secrets.S3_REGION }} + target-path: /veans/${{ github.ref_type == 'tag' && steps.ghd.outputs.describe || 'unstable' }} + files: "veans/dist/os-packages/*" + strip-path-prefix: veans/dist/os-packages/ + - name: Store OS Packages + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 + with: + name: veans_os_package_${{ matrix.package }}_${{ matrix.arch.pkg }} + path: ./veans/dist/os-packages/* + publish-repos: runs-on: ubuntu-latest needs: - os-package + - veans-os-package - desktop strategy: fail-fast: false @@ -272,6 +443,16 @@ jobs: merge-multiple: true path: dist/repo-work/incoming + - name: Download all veans OS packages + # Merged into the same incoming dir so reprepro / createrepo_c / + # repo-add / the apk loop pick them up alongside vikunja's packages + # — same suite, same arch fan-out, no extra source entry for users. + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 + with: + pattern: veans_os_package_* + merge-multiple: true + path: dist/repo-work/incoming + - name: Download desktop packages (Linux) uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 with: @@ -538,6 +719,8 @@ jobs: needs: - binaries - os-package + - veans-binaries + - veans-os-package - desktop - publish-repos if: ${{ github.ref_type == 'tag' }} @@ -555,6 +738,17 @@ jobs: pattern: vikunja_os_package_* merge-multiple: true + - name: Download Veans Binaries + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 + with: + name: veans_bin_packages + + - name: Download Veans OS Packages + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 + with: + pattern: veans_os_package_* + merge-multiple: true + - name: Download Desktop Package Linux uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 with: @@ -581,4 +775,9 @@ jobs: vikunja*.deb vikunja*.apk vikunja*.archlinux + veans*.zip + veans*.rpm + veans*.deb + veans*.apk + veans*.archlinux Vikunja Desktop* diff --git a/veans/.gitignore b/veans/.gitignore index 5dacfec7d..106e7b3fe 100644 --- a/veans/.gitignore +++ b/veans/.gitignore @@ -1,2 +1,3 @@ /veans /veans.exe +/dist/ diff --git a/veans/magefile.go b/veans/magefile.go index 68ec9a503..814960ccd 100644 --- a/veans/magefile.go +++ b/veans/magefile.go @@ -21,11 +21,16 @@ package main import ( + "context" + "crypto/sha256" "fmt" + "io" "os" "os/exec" "path/filepath" "runtime" + "strings" + "sync" "github.com/magefile/mage/mg" "github.com/magefile/mage/sh" @@ -42,9 +47,9 @@ func Build() error { // Clean removes built artifacts. func Clean() error { - for _, p := range []string{"./veans", "./veans.exe"} { + for _, p := range []string{"./veans", "./veans.exe", "./" + releaseDist} { if _, err := os.Stat(p); err == nil { - if err := os.Remove(p); err != nil { + if err := os.RemoveAll(p); err != nil { return err } } @@ -122,12 +127,466 @@ func (Lint) Fix() error { return sh.RunV("golangci-lint", "run", "--fix", "./...") } +// ----------------------------------------------------------------------------- +// Release +// +// Cross-compiles the veans binary for every OS/arch the parent vikunja binary +// targets, runs upx where supported, bundles each into a zip with the LICENSE +// and a sha256, and templates nfpm.yaml so the CI can build deb/rpm/apk/ +// archlinux packages from the same artifacts. Everything lands under +// `/dist/`. The CI workflow uploads dist/zip/* to S3 /veans// and +// hands dist/binaries/* off to the nfpm job. + +const releaseDist = "dist" + +type Release mg.Namespace + +var ( + releaseVersionNumber string + releaseVersionString string + releaseLdflags string + releaseTags = "netgo osusergo" + releaseInitOnce sync.Once + releaseInitErr error +) + +func releaseInitVars(ctx context.Context) error { + releaseInitOnce.Do(func() { + num := os.Getenv("RELEASE_VERSION") + if num == "" { + out, err := exec.CommandContext(ctx, "git", "describe", "--tags", "--always", "--abbrev=10").Output() + if err != nil { + releaseInitErr = fmt.Errorf("git describe: %w", err) + return + } + num = strings.TrimSpace(string(out)) + } + releaseVersionNumber = strings.Replace(strings.Trim(num, "\n"), "-g", "-", 1) + switch releaseVersionNumber { + case "", "main": + releaseVersionString = "unstable" + default: + releaseVersionString = releaseVersionNumber + } + releaseLdflags = fmt.Sprintf(`-X main.version=%s`, releaseVersionNumber) + }) + return releaseInitErr +} + +// Release runs all release steps end-to-end: dirs → xgo (windows/linux/darwin +// in parallel) → upx → copy → sha256 → per-target bundle dirs → zip. +func (Release) Release(ctx context.Context) error { + mg.Deps(releaseInitVars) + if err := (Release{}).Dirs(); err != nil { + return err + } + if err := releasePrepareXgo(ctx); err != nil { + return err + } + + // Run cross-compilation per OS in parallel; xgo serializes targets + // inside the docker container so each OS still gets full CPU. + var wg sync.WaitGroup + var ( + mu sync.Mutex + firstErr error + ) + record := func(err error) { + if err == nil { + return + } + mu.Lock() + if firstErr == nil { + firstErr = err + } + mu.Unlock() + } + for _, fn := range []func(context.Context) error{ + (Release{}).Windows, + (Release{}).Linux, + (Release{}).Darwin, + } { + wg.Add(1) + go func(f func(context.Context) error) { + defer wg.Done() + record(f(ctx)) + }(fn) + } + wg.Wait() + if firstErr != nil { + return firstErr + } + + if err := (Release{}).Compress(ctx); err != nil { + return err + } + if err := (Release{}).Copy(); err != nil { + return err + } + if err := (Release{}).Check(); err != nil { + return err + } + if err := (Release{}).OsPackage(); err != nil { + return err + } + return (Release{}).Zip(ctx) +} + +// Dirs creates all directories needed to release veans. +func (Release) Dirs() error { + for _, d := range []string{"binaries", "release", "zip"} { + if err := os.MkdirAll(filepath.Join(releaseDist, d), 0o755); err != nil { + return err + } + } + return nil +} + +func releasePrepareXgo(_ context.Context) error { + if _, err := exec.LookPath("xgo"); err != nil { + fmt.Println("xgo not found, installing src.techknowlogick.com/xgo...") + if err := sh.RunV("go", "install", "src.techknowlogick.com/xgo@latest"); err != nil { + return fmt.Errorf("installing xgo: %w", err) + } + } + fmt.Println("Pulling latest xgo docker image...") + return sh.RunV("docker", "pull", "ghcr.io/techknowlogick/xgo:latest") +} + +func runXgo(ctx context.Context, targets string) error { + mg.Deps(releaseInitVars) + if err := releasePrepareXgo(ctx); err != nil { + return err + } + + extraLdflags := `-linkmode external -extldflags "-static" ` + // xgo's darwin builds can't use the static external linker. + if strings.HasPrefix(targets, "darwin") { + extraLdflags = "" + } + + outName := os.Getenv("XGO_OUT_NAME") + if outName == "" { + outName = "veans-" + releaseVersionString + } + + return sh.RunV("xgo", + "-dest", filepath.Join(releaseDist, "binaries"), + "-tags", releaseTags, + "-ldflags", extraLdflags+releaseLdflags, + "-targets", targets, + "-out", outName, + "./cmd/veans", + ) +} + +// Windows builds binaries for windows. Same target set as parent vikunja. +func (Release) Windows(ctx context.Context) error { + return runXgo(ctx, "windows/*") +} + +// Linux builds binaries for linux. Same target set as parent vikunja. +func (Release) Linux(ctx context.Context) error { + targets := []string{ + "linux/amd64", + "linux/arm-5", + "linux/arm-6", + "linux/arm-7", + "linux/arm64", + "linux/mips", + "linux/mipsle", + "linux/mips64", + "linux/mips64le", + "linux/riscv64", + } + return runXgo(ctx, strings.Join(targets, ",")) +} + +// Darwin builds binaries for macOS. Same minimum (10.15) as parent. +func (Release) Darwin(ctx context.Context) error { + return runXgo(ctx, "darwin-10.15/*") +} + +// Xgo cross-compiles a single os/arch[-variant] target. +func (Release) Xgo(ctx context.Context, target string) error { + parts := strings.Split(target, "/") + if len(parts) < 2 { + return fmt.Errorf("invalid target %q (expected os/arch[/variant])", target) + } + variant := "" + if len(parts) > 2 && parts[2] != "" { + variant = "-" + strings.ReplaceAll(parts[2], "v", "") + } + return runXgo(ctx, parts[0]+"/"+parts[1]+variant) +} + +// Compress runs upx -9 over each built binary that upx can actually handle. +// Skip list matches the parent vikunja magefile. +func (Release) Compress(_ context.Context) error { + var wg sync.WaitGroup + var ( + mu sync.Mutex + firstErr error + ) + record := func(err error) { + if err == nil { + return + } + mu.Lock() + if firstErr == nil { + firstErr = err + } + mu.Unlock() + } + + walkErr := filepath.Walk(filepath.Join(releaseDist, "binaries"), func(path string, info os.FileInfo, err error) error { + if err != nil || info.IsDir() { + return err + } + name := info.Name() + if !strings.Contains(name, "veans") { + return nil + } + if strings.Contains(name, "mips") || + strings.Contains(name, "s390x") || + strings.Contains(name, "riscv64") || + strings.Contains(name, "darwin") || + (strings.Contains(name, "windows") && strings.Contains(name, "arm64")) { + // upx can't compress these targets. + return nil + } + wg.Add(1) + go func(p string) { + defer wg.Done() + if err := sh.RunV("chmod", "+x", p); err != nil { + record(err) + return + } + record(sh.RunV("upx", "-9", p)) + }(path) + return nil + }) + if walkErr != nil { + return walkErr + } + wg.Wait() + return firstErr +} + +// Copy copies all built binaries to dist/release/ as the staging area for +// per-target bundles and nfpm. +func (Release) Copy() error { + return filepath.Walk(filepath.Join(releaseDist, "binaries"), func(path string, info os.FileInfo, err error) error { + if err != nil || info.IsDir() { + return err + } + if !strings.Contains(info.Name(), "veans") { + return nil + } + return copyFile(path, filepath.Join(releaseDist, "release", info.Name())) + }) +} + +// Check writes a sha256 file next to each binary in dist/release/. +func (Release) Check() error { + p := filepath.Join(releaseDist, "release") + return filepath.Walk(p, func(path string, info os.FileInfo, err error) error { + if err != nil || info.IsDir() { + return err + } + if strings.HasSuffix(info.Name(), ".sha256") { + return nil + } + sum, err := sha256File(path) + if err != nil { + return err + } + return os.WriteFile(path+".sha256", []byte(sum+" "+info.Name()+"\n"), 0o644) + }) +} + +// OsPackage creates one folder per binary in dist/release/, populated with +// the binary, its sha256, and the LICENSE so the bundle is self-contained. +func (Release) OsPackage() error { + p := filepath.Join(releaseDist, "release") + + // Snapshot first so we don't walk into the newly-created folders. + bins := map[string]os.FileInfo{} + if err := filepath.Walk(p, func(path string, info os.FileInfo, err error) error { + if err != nil || info.IsDir() { + return err + } + if strings.HasSuffix(info.Name(), ".sha256") { + return nil + } + bins[path] = info + return nil + }); err != nil { + return err + } + + licensePath, err := licenseSource() + if err != nil { + return err + } + + for binPath, info := range bins { + folder := filepath.Join(p, info.Name()+"-full") + string(os.PathSeparator) + if err := os.MkdirAll(folder, 0o755); err != nil { + return err + } + if err := moveFile(binPath+".sha256", filepath.Join(folder, info.Name()+".sha256")); err != nil { + return err + } + if err := moveFile(binPath, filepath.Join(folder, info.Name())); err != nil { + return err + } + if err := copyFile(licensePath, filepath.Join(folder, "LICENSE")); err != nil { + return err + } + } + return nil +} + +// Zip turns each per-target folder under dist/release/-full/ into +// dist/zip/-full.zip. Uses the system `zip` so we get the same on-wire +// format as the parent's release artifacts. +func (Release) Zip(ctx context.Context) error { + rootDir, err := os.Getwd() + if err != nil { + return err + } + p := filepath.Join(releaseDist, "release") + return filepath.Walk(p, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if !info.IsDir() || info.Name() == "release" { + return nil + } + fmt.Printf("Zipping %s...\n", info.Name()) + zipFile := filepath.Join(rootDir, releaseDist, "zip", info.Name()+".zip") + //nolint:gosec // mage build helper; arguments are derived from the local fs walk above. + c := exec.CommandContext(ctx, "zip", "-r", zipFile, ".", "-i", "*") + c.Dir = path + c.Stdout = os.Stdout + c.Stderr = os.Stderr + return c.Run() + }) +} + +// PrepareNFPMConfig templates ./nfpm.yaml in place, substituting , +// and the same way the parent magefile does. Set +// NFPM_ARCH to the nfpm arch name (amd64, arm64, arm7, 386) before calling. +// The substituted file is meant to be consumed by `nfpm pkg` immediately +// after; this is destructive and intentional (the CI checks the repo out +// fresh per job). +func (Release) PrepareNFPMConfig() error { + mg.Deps(releaseInitVars) + cfgPath := "./nfpm.yaml" + raw, err := os.ReadFile(cfgPath) + if err != nil { + return err + } + + var nfpmArch string + switch os.Getenv("NFPM_ARCH") { + case "arm64": + nfpmArch = "arm64" + case "arm7": + nfpmArch = "arm7" + case "386": + nfpmArch = "386" + default: + nfpmArch = "amd64" + } + + // nfpm resolves relative to its working directory. In CI the + // nfpm action runs from $GITHUB_WORKSPACE while the veans source already + // occupies ./veans, so the CI stages the binary at ./veans/veans-bin and + // passes NFPM_BIN_PATH=./veans/veans-bin. Outside CI the default works for + // a local `mage build && mage release:prepare-nfpm-config && nfpm pkg + // --config nfpm.yaml` from inside veans/. + binLocation := os.Getenv("NFPM_BIN_PATH") + if binLocation == "" { + binLocation = "./veans" + } + + fixed := strings.ReplaceAll(string(raw), "", releaseVersionNumber) + fixed = strings.ReplaceAll(fixed, "", nfpmArch) + fixed = strings.ReplaceAll(fixed, "", binLocation) + return os.WriteFile(cfgPath, []byte(fixed), 0o600) +} + +// ----------------------------------------------------------------------------- +// helpers + +func copyFile(src, dst string) error { + in, err := os.Open(src) + if err != nil { + return err + } + defer in.Close() + out, err := os.Create(dst) + if err != nil { + return err + } + defer out.Close() + if _, err := io.Copy(out, in); err != nil { + return err + } + si, err := os.Stat(src) + if err != nil { + return err + } + if err := os.Chmod(dst, si.Mode()); err != nil { + return err + } + return out.Close() +} + +func moveFile(src, dst string) error { + if err := copyFile(src, dst); err != nil { + return err + } + return os.Remove(src) +} + +func sha256File(path string) (string, error) { + f, err := os.Open(path) + if err != nil { + return "", err + } + defer f.Close() + h := sha256.New() + if _, err := io.Copy(h, f); err != nil { + return "", err + } + return fmt.Sprintf("%x", h.Sum(nil)), nil +} + +// licenseSource resolves the AGPLv3 LICENSE file. veans intentionally doesn't +// vendor its own copy — the parent repo's LICENSE applies to both. Look in +// ../LICENSE (the normal layout when running from veans/) and fall back to +// ./LICENSE for unusual checkouts. +func licenseSource() (string, error) { + for _, p := range []string{"../LICENSE", "./LICENSE"} { + if _, err := os.Stat(p); err == nil { + return p, nil + } + } + return "", fmt.Errorf("could not find LICENSE in ../ or ./") +} + // Aliases lets `mage test` resolve to `Test.All` (and the others) without // having to spell out the namespace. Mirrors the parent magefile's pattern. var Aliases = map[string]any{ - "test": Test.All, - "test:filter": Test.Filter, - "test:e2e": Test.E2E, - "lint": Lint.All, - "lint:fix": Lint.Fix, + "test": Test.All, + "test:filter": Test.Filter, + "test:e2e": Test.E2E, + "lint": Lint.All, + "lint:fix": Lint.Fix, + "release": Release.Release, + "release:xgo": Release.Xgo, + "release:prepare-nfpm-config": Release.PrepareNFPMConfig, } diff --git a/veans/nfpm.yaml b/veans/nfpm.yaml new file mode 100644 index 000000000..d7fccae41 --- /dev/null +++ b/veans/nfpm.yaml @@ -0,0 +1,16 @@ +name: "veans" +arch: "" +platform: "linux" +version: "" +description: "veans is an agent-friendly CLI wrapper for the Vikunja REST API." +maintainer: "Vikunja Maintainers " +homepage: "https://vikunja.io" +section: "default" +priority: "extra" +license: "AGPLv3" +rpm: + signature: + key_file: ${NFPM_GPG_KEY_FILE} +contents: + - src: + dst: /usr/local/bin/veans