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:
parent
d5ab54941f
commit
5f00fca166
|
|
@ -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,2 +1,3 @@
|
|||
/veans
|
||||
/veans.exe
|
||||
/dist/
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
Loading…
Reference in New Issue