From 460a7d3324b309e7491d34add3a4c61671bb5ca1 Mon Sep 17 00:00:00 2001 From: Nikolay Govorov Date: Mon, 30 Mar 2026 03:41:10 +0100 Subject: Setup packaging for mirum --- .github/workflows/build.yml | 209 ++++++++++++++++++++++++++++++++++++ README.md | 3 + REUSE.toml | 4 + Taskfile.yml | 20 +++- VERSION | 1 + cmd/mirumd/main.go | 70 ++++++++++-- go.mod | 5 +- go.sum | 2 + nfpm.yaml | 62 +++++++++++ pkg/apt-ftparchive.conf | 29 +++++ pkg/logo.svg | 25 +++++ pkg/mirumd.service | 51 +++++++++ pkg/mirumd.yaml | 4 + pkg/scripts/postinstall.sh | 10 ++ pkg/scripts/preinstall.sh | 17 +++ pkg/scripts/preremove.sh | 10 ++ 16 files changed, 513 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/build.yml create mode 100644 README.md create mode 100644 VERSION create mode 100644 nfpm.yaml create mode 100644 pkg/apt-ftparchive.conf create mode 100644 pkg/logo.svg create mode 100644 pkg/mirumd.service create mode 100644 pkg/mirumd.yaml create mode 100644 pkg/scripts/postinstall.sh create mode 100644 pkg/scripts/preinstall.sh create mode 100644 pkg/scripts/preremove.sh diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..2f773c6 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,209 @@ +# Copyright (c) 2026 Nikolay Govorov +# SPDX-License-Identifier: AGPL-3.0-or-later + +name: Build + +on: + push: + branches: [main] + tags: ["v*"] + pull_request: + branches: [main] + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + + - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 + with: + go-version-file: go.mod + + - name: Check licenses + uses: fsfe/reuse-action@676e2d560c9a403aa252096d99fcab3e1132b0f5 # v6.0.0 + + - name: Check formatting + run: test -z "$(gofmt -l .)" + + - name: Vet + run: go vet ./... + + build: + name: Build + runs-on: ubuntu-latest + outputs: + channel: ${{ steps.version.outputs.channel }} + version: ${{ steps.version.outputs.version }} + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + + - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 + with: + go-version-file: go.mod + + - name: Install tools + run: | + echo 'deb [trusted=yes] https://repo.goreleaser.com/apt/ /' | sudo tee /etc/apt/sources.list.d/goreleaser.list + sudo apt update && sudo apt install nfpm + sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d -b /usr/local/bin + + - name: Import GPG key + uses: crazy-max/ghaction-import-gpg@e89d40939c28e39f97cf32126055eeae86ba74ec # v6.3.0 + with: + passphrase: ${{ secrets.GPG_PASSPHRASE }} + gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} + + - name: Determine version + id: version + run: | + if [[ "$GITHUB_REF" == refs/tags/v* ]]; then + echo "version=${GITHUB_REF#refs/tags/v}" >> "$GITHUB_OUTPUT" + echo "channel=stable" >> "$GITHUB_OUTPUT" + else + echo "version=$(cat VERSION)~nightly.$(git log -1 --format=%ct)" >> "$GITHUB_OUTPUT" + echo "channel=nightly" >> "$GITHUB_OUTPUT" + fi + + - name: Build and package + run: | + printf '%s' "$GPG_PRIVATE_KEY" > /tmp/private.asc + chmod 600 /tmp/private.asc + export SIGNING_PRIVATE_KEY="/tmp/private.asc" + task package VERSION=${{ steps.version.outputs.version }} + rm -f /tmp/private.asc + env: + GPG_KEY_ID: ${{ vars.GPG_KEY_ID }} + GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + NFPM_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + + - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: packages + path: build/dist/* + + publish: + name: Publish + runs-on: ubuntu-latest + needs: [lint, build] + if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') + permissions: + contents: write + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - name: Install tools + run: | + sudo apt-get update + sudo apt-get install -y apt-utils rclone createrepo-c + + - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + with: + name: packages + path: dist + + # Configure GPG keys + - name: Import GPG key + uses: crazy-max/ghaction-import-gpg@e89d40939c28e39f97cf32126055eeae86ba74ec # v6.3.0 + with: + passphrase: ${{ secrets.GPG_PASSPHRASE }} + gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} + - name: Update nightly tag + if: needs.build.outputs.channel == 'nightly' + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git tag -f nightly + git push origin --force tag nightly + + - name: Delete old nightly assets + if: needs.build.outputs.channel == 'nightly' + run: | + for asset in $(gh release view nightly --json assets --jq '.assets[].name' 2>/dev/null || true); do + gh release delete-asset nightly "$asset" --yes + done + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Create GitHub Release + uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0 + with: + name: ${{ needs.build.outputs.channel == 'stable' && needs.build.outputs.version || 'nightly' }} + tag_name: ${{ needs.build.outputs.channel == 'stable' && format('v{0}', needs.build.outputs.version) || 'nightly' }} + prerelease: ${{ needs.build.outputs.channel == 'nightly' }} + files: dist/* + body: | + **Build**: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # Update APT/RPM repos on S3 + - name: Configure rclone + run: | + mkdir -p ~/.config/rclone + cat > ~/.config/rclone/rclone.conf << EOF + [s3] + type = s3 + provider = ${{ vars.S3_PROVIDER || 'Other' }} + access_key_id = ${{ secrets.S3_ACCESS_KEY_ID }} + secret_access_key = ${{ secrets.S3_SECRET_ACCESS_KEY }} + region = ${{ vars.S3_REGION }} + endpoint = ${{ vars.S3_ENDPOINT }} + EOF + + - name: Upload GPG public key + run: | + gpg --export --armor "${{ vars.GPG_KEY_ID }}" > public.gpg + rclone copyto public.gpg "s3:${{ vars.S3_BUCKET }}/public.gpg" + + - name: Create APT repository + run: | + CHANNEL=${{ needs.build.outputs.channel }} + + mkdir -p apt-repo + rclone copy "s3:${{ vars.S3_BUCKET }}/apt/" apt-repo/ + + mkdir -p /tmp/apt-cache + for ch in stable nightly; do + mkdir -p apt-repo/pool/$ch + for arch in amd64 arm64 riscv64 ppc64le; do + mkdir -p apt-repo/dists/$ch/main/binary-$arch + done + done + cp dist/*.deb apt-repo/pool/$CHANNEL/ + + cd apt-repo + apt-ftparchive generate "${{ github.workspace }}/pkg/apt-ftparchive.conf" + apt-ftparchive \ + -o APT::FTPArchive::Release::Codename=$CHANNEL \ + -o APT::FTPArchive::Release::Components=main \ + -o APT::FTPArchive::Release::Architectures="amd64 arm64 riscv64 ppc64le" \ + release dists/$CHANNEL/ > dists/$CHANNEL/Release + gpg --default-key="${{ vars.GPG_KEY_ID }}" --batch --yes -abs -o dists/$CHANNEL/Release.gpg dists/$CHANNEL/Release + gpg --default-key="${{ vars.GPG_KEY_ID }}" --batch --yes --clearsign -o dists/$CHANNEL/InRelease dists/$CHANNEL/Release + cd .. + + rclone copy apt-repo/ "s3:${{ vars.S3_BUCKET }}/apt/" --progress + + - name: Create RPM repository + run: | + CHANNEL=${{ needs.build.outputs.channel }} + + mkdir -p rpm-repo + rclone copy "s3:${{ vars.S3_BUCKET }}/rpm/$CHANNEL/" rpm-repo/ + + cp dist/*.rpm rpm-repo/ + + createrepo_c --update rpm-repo/ + gpg --default-key="${{ vars.GPG_KEY_ID }}" --batch --yes --detach-sign --armor rpm-repo/repodata/repomd.xml + + cat > rpm-repo/mirum-${CHANNEL}.repo << EOF + [mirum-${CHANNEL}] + name=Mirum ${CHANNEL} + gpgkey=${{ vars.S3_PUBLIC_URL }}/public.gpg + baseurl=${{ vars.S3_PUBLIC_URL }}/rpm/${CHANNEL}/ + enabled=1 + gpgcheck=1 + EOF + + rclone copy rpm-repo/ "s3:${{ vars.S3_BUCKET }}/rpm/$CHANNEL/" --progress diff --git a/README.md b/README.md new file mode 100644 index 0000000..581b179 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Mirum + +An experimental CI built around virtual machines and Starlark diff --git a/REUSE.toml b/REUSE.toml index 82918bf..0d40574 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -7,6 +7,10 @@ version = 1 path = [ "go.mod", "go.sum", + "VERSION", + "CLA.md", + "README.md", + "pkg/logo.svg", ] SPDX-FileCopyrightText = "2026 Nikolay Govorov " SPDX-License-Identifier = "AGPL-3.0-or-later" diff --git a/Taskfile.yml b/Taskfile.yml index 2c923c3..8693c22 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -5,12 +5,30 @@ version: '3' vars: BUILD_DIR: build + DIST_DIR: build/dist + ARCHES: amd64 arm64 riscv64 ppc64le tasks: build: - desc: Build master binary + desc: Build for current arch cmds: - mkdir -p {{.BUILD_DIR}} - go build -o {{.BUILD_DIR}}/mirumd ./cmd/mirumd + package: + desc: Build deb/rpm packages for all architectures + requires: + vars: [VERSION] + env: + CGO_ENABLED: "0" + VERSION: "{{.VERSION}}" + cmds: + - mkdir -p {{.DIST_DIR}} {{.BUILD_DIR}} + - for: { var: ARCHES } + cmd: | + GOARCH={{.ITEM}} go build -o {{.BUILD_DIR}}/mirumd-linux-{{.ITEM}} ./cmd/mirumd + cp {{.BUILD_DIR}}/mirumd-linux-{{.ITEM}} {{.BUILD_DIR}}/mirumd + ARCH={{.ITEM}} nfpm package --packager deb --target {{.DIST_DIR}}/ + ARCH={{.ITEM}} nfpm package --packager rpm --target {{.DIST_DIR}}/ + # yaml-language-server: $schema=https://taskfile.dev/schema.json diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..6e8bf73 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.1.0 diff --git a/cmd/mirumd/main.go b/cmd/mirumd/main.go index 062d1d7..c3c05a9 100644 --- a/cmd/mirumd/main.go +++ b/cmd/mirumd/main.go @@ -5,6 +5,7 @@ package main import ( "bytes" + "context" "crypto/hmac" "crypto/sha256" "encoding/hex" @@ -13,13 +14,20 @@ import ( "fmt" "io" "log/slog" + "net" "net/http" "net/url" "os" "os/exec" + "os/signal" "path/filepath" + "strconv" "strings" + "syscall" + "time" + "github.com/coreos/go-systemd/v22/activation" + "github.com/coreos/go-systemd/v22/daemon" "go.starlark.net/starlark" ) @@ -145,11 +153,11 @@ type taskCtx struct { var _ starlark.HasAttrs = (*taskCtx)(nil) func (c *taskCtx) String() string { return "ctx" } -func (c *taskCtx) Type() string { return "ctx" } -func (c *taskCtx) Freeze() {} -func (c *taskCtx) Truth() starlark.Bool { return true } -func (c *taskCtx) Hash() (uint32, error) { return 0, fmt.Errorf("unhashable: ctx") } -func (c *taskCtx) AttrNames() []string { return []string{"shell"} } +func (c *taskCtx) Type() string { return "ctx" } +func (c *taskCtx) Freeze() {} +func (c *taskCtx) Truth() starlark.Bool { return true } +func (c *taskCtx) Hash() (uint32, error) { return 0, fmt.Errorf("unhashable: ctx") } +func (c *taskCtx) AttrNames() []string { return []string{"shell"} } func (c *taskCtx) Attr(name string) (starlark.Value, error) { if name == "shell" { @@ -260,10 +268,58 @@ func main() { go processPush(push) }) - slog.Info("listening", "addr", *addr) + ln, err := socketActivationListener() + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + + slog.Info("listening", "addr", ln.Addr()) + + srv := &http.Server{Handler: mux} + + go func() { + sig := make(chan os.Signal, 1) + signal.Notify(sig, syscall.SIGTERM, syscall.SIGINT) + <-sig + + slog.Info("shutting down") + daemon.SdNotify(false, daemon.SdNotifyStopping) + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + srv.Shutdown(ctx) + }() + + daemon.SdNotify(false, daemon.SdNotifyReady) + go watchdog() - if err := http.ListenAndServe(*addr, mux); err != nil { + if err := srv.Serve(ln); err != http.ErrServerClosed { fmt.Fprintln(os.Stderr, err) os.Exit(1) } } + +func socketActivationListener() (net.Listener, error) { + listeners, _ := activation.Listeners() + if len(listeners) > 0 { + return listeners[0], nil + } + return net.Listen("tcp", *addr) +} + +func watchdog() { + usecStr := os.Getenv("WATCHDOG_USEC") + if usecStr == "" { + return + } + usec, err := strconv.ParseInt(usecStr, 10, 64) + if err != nil || usec <= 0 { + return + } + interval := time.Duration(usec) * time.Microsecond / 2 + for { + daemon.SdNotify(false, daemon.SdNotifyWatchdog) + time.Sleep(interval) + } +} diff --git a/go.mod b/go.mod index 632a160..5e5f10b 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,9 @@ module mrdimidium/mirum go 1.26.1 -require go.starlark.net v0.0.0-20260326113308-fadfc96def35 +require ( + github.com/coreos/go-systemd/v22 v22.7.0 + go.starlark.net v0.0.0-20260326113308-fadfc96def35 +) require golang.org/x/sys v0.42.0 // indirect diff --git a/go.sum b/go.sum index fa78fcb..305c53c 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA= +github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= go.starlark.net v0.0.0-20260326113308-fadfc96def35 h1:VYAqieSOJNxBDX8KJneTAwvdf4J4zRDE2u+UFXtt9h4= diff --git a/nfpm.yaml b/nfpm.yaml new file mode 100644 index 0000000..beea2ff --- /dev/null +++ b/nfpm.yaml @@ -0,0 +1,62 @@ +# Copyright (c) 2026 Nikolay Govorov +# SPDX-License-Identifier: AGPL-3.0-or-later + +# yaml-language-server: $schema=https://nfpm.goreleaser.com/schema.json +# vim: set ts=2 sw=2 tw=0 fo=cnqoj + +name: mirum +arch: ${ARCH} +version: ${VERSION} +license: AGPL-3.0-or-later +platform: linux +maintainer: Nikolay Govorov +description: Modern CI platform + +contents: + - src: ./LICENSE + dst: /usr/share/doc/mirum/LICENSE + - src: ./README.md + dst: /usr/share/doc/mirum/README.md + + - src: build/mirumd + dst: /usr/local/bin/mirumd + file_info: + mode: 0755 + + - src: pkg/mirumd.yaml + dst: /etc/mirumd.yaml + type: config|noreplace + file_info: + mode: 0640 + owner: root + group: mirum + + - src: pkg/mirumd.service + dst: /usr/lib/systemd/system/mirumd.service + file_info: + mode: 0644 + + - dst: /var/lib/mirumd + type: dir + file_info: + mode: 0755 + owner: root + group: mirum + + +scripts: + preinstall: pkg/scripts/preinstall.sh + postinstall: pkg/scripts/postinstall.sh + preremove: pkg/scripts/preremove.sh + +deb: + signature: + method: debsign + key_id: ${GPG_KEY_ID} + key_file: ${SIGNING_PRIVATE_KEY} + +rpm: + group: System Environment/Daemons + signature: + key_id: ${GPG_KEY_ID} + key_file: ${SIGNING_PRIVATE_KEY} diff --git a/pkg/apt-ftparchive.conf b/pkg/apt-ftparchive.conf new file mode 100644 index 0000000..d581965 --- /dev/null +++ b/pkg/apt-ftparchive.conf @@ -0,0 +1,29 @@ +// Copyright (c) 2026 Nikolay Govorov +// SPDX-License-Identifier: AGPL-3.0-or-later + +Dir { + ArchiveDir "."; + CacheDir "/tmp/apt-cache"; +}; + +Default { + Packages::Compress ". gzip"; + Packages::Extensions ".deb"; +}; + +TreeDefault { + Packages "$(DIST)/$(SECTION)/binary-$(ARCH)/Packages"; + BinCacheDB "packages-$(DIST)-$(ARCH).db"; +}; + +Tree "dists/stable" { + Sections "main"; + Architectures "amd64 arm64 riscv64 ppc64le"; + Directory "pool/stable"; +}; + +Tree "dists/nightly" { + Sections "main"; + Architectures "amd64 arm64 riscv64 ppc64le"; + Directory "pool/nightly"; +}; diff --git a/pkg/logo.svg b/pkg/logo.svg new file mode 100644 index 0000000..0a2bfc2 --- /dev/null +++ b/pkg/logo.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pkg/mirumd.service b/pkg/mirumd.service new file mode 100644 index 0000000..dedf83a --- /dev/null +++ b/pkg/mirumd.service @@ -0,0 +1,51 @@ +# Copyright (c) 2026 Nikolay Govorov +# SPDX-License-Identifier: AGPL-3.0-or-later + +[Unit] +Description=Mirum daemon (modern CI platform) +Requires=network-online.target +After=time-sync.target network-online.target remote-fs.target nss-lookup.target +Wants=time-sync.target + +[Service] +Type=notify +User=mirum +Group=mirum +Restart=always +RestartSec=30 +WatchdogSec=30 +NotifyAccess=main +ExecPaths=/usr/local/bin/mirumd /usr/lib +ExecStart=/usr/local/bin/mirumd --config=/etc/mirumd.yaml +LimitCORE=infinity +LimitNOFILE=500000 +AmbientCapabilities=CAP_NET_BIND_SERVICE + +# %p is resolved to the systemd unit name +LogsDirectory=%p +StateDirectory=%p +CacheDirectory=%p +RuntimeDirectory=%p + +UMask=0077 +LockPersonality=yes +NoNewPrivileges=yes +PrivateDevices=yes +PrivateTmp=true +ProcSubset=pid +ProtectClock=yes +ProtectControlGroups=yes +ProtectHome=yes +ProtectHostname=yes +ProtectKernelLogs=yes +ProtectKernelModules=yes +ProtectKernelTunables=yes +ProtectProc=invisible +ProtectSystem=strict +RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX +RestrictNamespaces=yes +RestrictSUIDSGID=yes + +[Install] +# service should not start from the rescue shell (rescue.target). +WantedBy=multi-user.target diff --git a/pkg/mirumd.yaml b/pkg/mirumd.yaml new file mode 100644 index 0000000..1ced7ab --- /dev/null +++ b/pkg/mirumd.yaml @@ -0,0 +1,4 @@ +# Copyright (c) 2026 Nikolay Govorov +# SPDX-License-Identifier: AGPL-3.0-or-later + +address: :2026 diff --git a/pkg/scripts/postinstall.sh b/pkg/scripts/postinstall.sh new file mode 100644 index 0000000..13b970c --- /dev/null +++ b/pkg/scripts/postinstall.sh @@ -0,0 +1,10 @@ +#!/bin/sh +# Copyright (c) 2026 Nikolay Govorov +# SPDX-License-Identifier: AGPL-3.0-or-later + +set -e + +if [ -x "/bin/systemctl" ] && [ -d /run/systemd/system ] && [ -f /usr/lib/systemd/system/mirumd.service ]; then + /bin/systemctl daemon-reload + /bin/systemctl enable mirumd +fi diff --git a/pkg/scripts/preinstall.sh b/pkg/scripts/preinstall.sh new file mode 100644 index 0000000..6f7b91f --- /dev/null +++ b/pkg/scripts/preinstall.sh @@ -0,0 +1,17 @@ +#!/bin/sh +# Copyright (c) 2026 Nikolay Govorov +# SPDX-License-Identifier: AGPL-3.0-or-later + +set -e + +PROGRAM=mirum +MIRUM_USER=${MIRUM_USER:-mirum} +MIRUM_GROUP=${MIRUM_GROUP:-${MIRUM_USER}} + +if ! getent group $MIRUM_GROUP >/dev/null; then + groupadd --system $MIRUM_GROUP +fi + +if ! getent passwd $MIRUM_USER >/dev/null; then + useradd --system --gid $MIRUM_GROUP --no-create-home --shell /usr/sbin/nologin $MIRUM_USER +fi diff --git a/pkg/scripts/preremove.sh b/pkg/scripts/preremove.sh new file mode 100644 index 0000000..c77f55b --- /dev/null +++ b/pkg/scripts/preremove.sh @@ -0,0 +1,10 @@ +#!/bin/sh +# Copyright (c) 2026 Nikolay Govorov +# SPDX-License-Identifier: AGPL-3.0-or-later + +set -e + +if [ -x "/bin/systemctl" ] && [ -d /run/systemd/system ] && [ -f /usr/lib/systemd/system/mirumd.service ]; then + /bin/systemctl stop mirumd.service || true + /bin/systemctl disable mirumd.service || true +fi -- Gilti