feat(veans): build and publish veans alongside vikunja

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/<version>/.
This commit is contained in:
kolaente 2026-05-27 11:05:04 +02:00 committed by kolaente
parent d5ab54941f
commit 5f00fca166
4 changed files with 682 additions and 7 deletions

View File

@ -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*

1
veans/.gitignore vendored
View File

@ -1,2 +1,3 @@
/veans
/veans.exe
/dist/

View File

@ -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
// `<veans>/dist/`. The CI workflow uploads dist/zip/* to S3 /veans/<ver>/ 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/<name>-full/ into
// dist/zip/<name>-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 <version>,
// <arch> and <binlocation> 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 <binlocation> 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), "<version>", releaseVersionNumber)
fixed = strings.ReplaceAll(fixed, "<arch>", nfpmArch)
fixed = strings.ReplaceAll(fixed, "<binlocation>", 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,
}

16
veans/nfpm.yaml Normal file
View File

@ -0,0 +1,16 @@
name: "veans"
arch: "<arch>"
platform: "linux"
version: "<version>"
description: "veans is an agent-friendly CLI wrapper for the Vikunja REST API."
maintainer: "Vikunja Maintainers <maintainers@vikunja.io>"
homepage: "https://vikunja.io"
section: "default"
priority: "extra"
license: "AGPLv3"
rpm:
signature:
key_file: ${NFPM_GPG_KEY_FILE}
contents:
- src: <binlocation>
dst: /usr/local/bin/veans