feat!: package bec-slurm-worker for Linux and RPM deployment

This commit is contained in:
wakonig_k authored and Klaus Wakonig committed 2026-10-28 18:05:33 +01:00
1 parent 177936846f
commit 4477cc11be
13 files changed
+307 -16

No files matched your search

+20
View File
@@ -29,3 +29,23 @@ jobs:
needs: check_pr_status
if: needs.check_pr_status.outputs.branch-pr == ''
uses: ./.github/workflows/go_worker.yml
publish_worker:
needs: [formatter, unit_tests, go_worker]
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
permissions:
contents: read
actions: read
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/download-artifact@v4
with:
name: bec-slurm-worker-packages
path: worker/dist
- name: Publish Linux archives and RPMs to Gitea
env:
GITEA_REGISTRY_TOKEN: ${{ secrets.GITEA_REGISTRY_TOKEN }}
run: bash worker/scripts/upload-packages.sh "${GITHUB_REF_NAME#v}"
+28 -4
View File
@@ -8,6 +8,8 @@ jobs:
working-directory: worker
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-go@v6
with:
go-version-file: worker/go.mod
@@ -16,9 +18,31 @@ jobs:
run: test -z "$(gofmt -l .)"
- name: Test and vet
run: make test
- name: Build static Linux binary
run: make build
- uses: goreleaser/goreleaser-action@v7
with:
distribution: goreleaser
version: v2.18.0
install-only: true
- name: Select package version
shell: bash
run: |
if [[ "$GITHUB_REF" == refs/tags/v* ]]; then
version="${GITHUB_REF_NAME#v}"
else
version="0.0.0-dev.${GITHUB_RUN_NUMBER}.g${GITHUB_SHA:0:12}"
fi
if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$ ]]; then
echo 'Expected a semantic version after the v tag prefix.' >&2
exit 1
fi
printf 'BUILD_VERSION=%s\n' "$version" >> "$GITHUB_ENV"
- name: Build Linux archives and RPMs
run: make package VERSION="$BUILD_VERSION"
- uses: actions/upload-artifact@v4
with:
name: slurm-worker-linux-amd64
path: dist/slurm-worker-linux-amd64
name: bec-slurm-worker-packages
if-no-files-found: error
path: |
worker/dist/*.tar.gz
worker/dist/*.rpm
worker/dist/checksums.txt
+1 -1
View File
@@ -72,5 +72,5 @@ if __name__ == "__main__":
script_path=Path(__file__).with_name("print_for_10s.sh"),
working_directory="/das/home/your_username",
log_directory="/das/home/your_username/slurm_logs",
worker_path="/das/home/your_username/slurm-worker",
worker_path="/das/home/your_username/bec-slurm-worker",
)
+2 -2
View File
@@ -53,7 +53,7 @@ class SlurmManager:
*,
env_file: str | Path = ".env",
rest_client: SlurmRestClient | None = None,
worker_path: str = "/usr/local/bin/slurm-worker",
worker_path: str = "/usr/bin/bec-slurm-worker",
) -> None:
"""Create REST/Redis clients and start background job monitoring.
@@ -342,7 +342,7 @@ class SlurmManager:
env_path: str | Path | None,
job_id: str,
*,
worker_path: str = "/usr/local/bin/slurm-worker",
worker_path: str = "/usr/bin/bec-slurm-worker",
redis_address: str = "localhost:6379",
) -> str:
"""Package local Bash file contents for the compute-node Go worker.
+1 -1
View File
@@ -92,7 +92,7 @@ def test_job_is_held_until_redis_acknowledges_subscriptions(manager):
assert job["current_working_directory"] == "/remote/node/work"
assert '"activation": ""' in script
assert "stdout-test %02d/10" in script
assert "/usr/local/bin/slurm-worker" in script
assert "/usr/bin/bec-slurm-worker" in script
future = next(iter(manager._job_tracker._future_registry.values()))
topics = TopicInfo(future._job_id)
assert manager.sub_client._subscription_ready[topics.event].is_set()
+112
View File
@@ -0,0 +1,112 @@
"""Check Gitea package publishing without contacting external registries."""
import json
import os
import subprocess
import sys
from pathlib import Path
import pytest
PUBLISH_SCRIPT = Path(__file__).parents[1] / "worker/scripts/upload-packages.sh"
@pytest.fixture
def publishing_environment(tmp_path):
artifacts = tmp_path / "artifacts"
artifacts.mkdir()
names = [
"bec-slurm-worker_0.2.0_linux_amd64.tar.gz",
"bec-slurm-worker_0.2.0_linux_arm64.tar.gz",
"bec-slurm-worker-0.2.0-1.x86_64.rpm",
"bec-slurm-worker-0.2.0-1.aarch64.rpm",
"checksums.txt",
]
for name in names:
(artifacts / name).write_text("test artifact")
calls = tmp_path / "calls.jsonl"
curl = tmp_path / "curl"
curl.write_text(
f"#!{sys.executable}\n"
"import json, os, sys\n"
"from pathlib import Path\n"
"path = Path(os.environ['MOCK_CURL_CALLS'])\n"
"with path.open('a') as output:\n"
" output.write(json.dumps(sys.argv[1:]) + '\\n')\n"
"sys.exit(int(os.environ.get('MOCK_CURL_EXIT', '0')))\n"
)
curl.chmod(0o700)
env = {
**os.environ,
"PATH": f"{tmp_path}{os.pathsep}{os.defpath}",
"GITEA_REGISTRY_TOKEN": "example:test-token",
"MOCK_CURL_CALLS": str(calls),
}
return artifacts, calls, env
def publish(artifacts, env, version="0.2.0"):
return subprocess.run(
["bash", str(PUBLISH_SCRIPT), version, str(artifacts)],
env=env,
capture_output=True,
text=True,
check=False,
)
def test_uploads_complete_bundle_and_indexes_rpms(publishing_environment):
artifacts, calls, env = publishing_environment
result = publish(artifacts, env)
assert result.returncode == 0, result.stderr
requests = [json.loads(line) for line in calls.read_text().splitlines()]
assert len(requests) == 7
generic = "https://gitea.psi.ch/api/packages/bec/generic/bec-slurm-worker/0.2.0/"
assert {request[-1] for request in requests[:5]} == {
generic + path.name for path in artifacts.iterdir()
}
assert all(
request[-1] == "https://gitea.psi.ch/api/packages/bec/rpm/upload"
for request in requests[5:]
)
for request in requests:
assert request[request.index("--user") + 1] == "example:test-token"
assert "--fail-with-body" in request
assert "--max-time" in request
assert "test-token" not in result.stdout + result.stderr
@pytest.mark.parametrize("missing", ["*.rpm", "*.tar.gz", "checksums.txt"])
def test_incomplete_packages_are_rejected_before_upload(publishing_environment, missing):
artifacts, calls, env = publishing_environment
for path in artifacts.glob(missing):
path.unlink()
result = publish(artifacts, env)
assert result.returncode != 0
assert "refusing an incomplete upload" in result.stderr
assert not calls.exists()
def test_upload_failure_stops_publication(publishing_environment):
artifacts, calls, env = publishing_environment
result = publish(artifacts, {**env, "MOCK_CURL_EXIT": "22"})
assert result.returncode == 22
assert len(calls.read_text().splitlines()) == 1
@pytest.mark.parametrize("version", ["../other-package", "v0.2.0", "0.2.0/extra"])
def test_invalid_version_cannot_change_upload_path(publishing_environment, version):
artifacts, calls, env = publishing_environment
result = publish(artifacts, env, version)
assert result.returncode != 0
assert "semantic version" in result.stderr
assert not calls.exists()
def test_missing_credentials_are_rejected_before_upload(publishing_environment):
artifacts, calls, env = publishing_environment
env.pop("GITEA_REGISTRY_TOKEN")
result = publish(artifacts, env)
assert result.returncode != 0
assert "GITEA_REGISTRY_TOKEN" in result.stderr
assert not calls.exists()
+51
View File
@@ -0,0 +1,51 @@
version: 2
project_name: bec-slurm-worker
builds:
- id: bec-slurm-worker
main: ./cmd/bec-slurm-worker
binary: bec-slurm-worker
env:
- CGO_ENABLED=0
goos: [linux]
goarch: [amd64, arm64]
flags: [-trimpath]
ldflags: [-s -w]
archives:
- formats: [tar.gz]
name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}'
files:
- README.md
nfpms:
- id: bec-slurm-worker-rpm
ids: [bec-slurm-worker]
package_name: bec-slurm-worker
# Gitea generic package filenames do not allow RPM's prerelease tilde.
file_name_template: '{{ replace .ConventionalFileName "~" "_" }}'
homepage: https://github.com/bec-project/slurm_manager
maintainer: BEC Project
description: Execute Slurm batch workloads and forward logs, status, and heartbeats to Redis.
formats: [rpm]
bindir: /usr/bin
dependencies:
- bash
- /usr/bin/getent
contents:
- src: README.md
dst: /usr/share/doc/bec-slurm-worker/README.md
checksum:
name_template: checksums.txt
# CI supplies the release-tag version or a unique development version.
# Publishing to Gitea is a separate job after all CI checks have passed.
snapshot:
version_template: '{{ .Env.BUILD_VERSION }}'
changelog:
disable: true
release:
disable: true
+7 -2
View File
@@ -1,8 +1,13 @@
TARGET_ARCH ?= amd64
VERSION ?= 0.0.0-dev
.PHONY: build test
.PHONY: build package test
build:
CGO_ENABLED=0 GOOS=linux GOARCH=$(TARGET_ARCH) go build -trimpath -ldflags='-s -w' -o ../dist/slurm-worker-linux-$(TARGET_ARCH) ./cmd/slurm-worker
CGO_ENABLED=0 GOOS=linux GOARCH=$(TARGET_ARCH) go build -trimpath -ldflags='-s -w' -o ../dist/bec-slurm-worker-linux-$(TARGET_ARCH) ./cmd/bec-slurm-worker
package: export BUILD_VERSION = $(VERSION)
package:
goreleaser release --snapshot --clean
test:
go test -race -timeout 60s ./...
+34
View File
@@ -0,0 +1,34 @@
# bec-slurm-worker
Compute-node executable for Slurm Manager. It runs one uploaded Bash workload
and forwards stdout, stderr, lifecycle status, and heartbeats to Redis.
It is launched by Slurm Manager, not as a system service.
## Builds
With Go installed, `make build` creates `../dist/bec-slurm-worker-linux-amd64`.
Use `TARGET_ARCH=arm64` for ARM64.
With GoReleaser 2.18.0 installed, `make package VERSION=0.2.0` creates static Linux
archives, RPMs for x86_64 and aarch64, and SHA-256 checksums in `dist/`.
The RPM installs `/usr/bin/bec-slurm-worker` and requires Bash and `getent` for
compute-node account lookup. No Python or Go runtime is required on the node.
## CI and Gitea
Every CI run tests the worker and builds both architectures and RPMs. GitHub Actions
retains them in the `bec-slurm-worker-packages` artifact. Branch and PR builds use a
development version; version tags use the matching release version.
After all CI jobs pass for a pushed `v*` tag, CI publishes the archives, RPMs,
and checksums to the Gitea generic package `bec/bec-slurm-worker`, and indexes
the RPMs in the BEC RPM registry, following CAGO's packaging approach.
`GITEA_REGISTRY_TOKEN` must be available to this repository as an Actions secret,
with the value `username:token` and permission to publish BEC packages.
Generic downloads:
`https://gitea.psi.ch/api/packages/bec/generic/bec-slurm-worker/VERSION/FILENAME`
The publisher refuses incomplete artifact sets and propagates upload failures.
Existing package files are not overwritten. No packages are uploaded for PRs or
untagged branch builds.
@@ -1,4 +1,4 @@
// slurm-worker executes one uploaded Bash workload and forwards feedback to Redis.
// bec-slurm-worker executes one uploaded Bash workload and forwards feedback to Redis.
package main
import (
@@ -38,7 +38,7 @@ func main() {
stop()
_ = publisher.Close()
if err != nil {
fmt.Fprintln(os.Stderr, "slurm-worker:", err)
fmt.Fprintln(os.Stderr, "bec-slurm-worker:", err)
}
os.Exit(code)
}
@@ -18,7 +18,7 @@ func TestCLIHelper(t *testing.T) {
if os.Getenv("SLURM_WORKER_TEST_HELPER") != "1" {
return
}
os.Args = []string{"slurm-worker", "--job-id", "cli-job", "--redis-address", os.Getenv("SLURM_WORKER_TEST_REDIS")}
os.Args = []string{"bec-slurm-worker", "--job-id", "cli-job", "--redis-address", os.Getenv("BEC_SLURM_WORKER_TEST_REDIS")}
main()
}
@@ -38,7 +38,7 @@ func TestCLIExecutesUploadedPayload(t *testing.T) {
t.Fatal(err)
}
command := exec.CommandContext(ctx, executable, "-test.run=^TestCLIHelper$")
command.Env = append(os.Environ(), "SLURM_WORKER_TEST_HELPER=1", "SLURM_WORKER_TEST_REDIS="+server.Addr())
command.Env = append(os.Environ(), "SLURM_WORKER_TEST_HELPER=1", "BEC_SLURM_WORKER_TEST_REDIS="+server.Addr())
command.Stdin = strings.NewReader(`{"script":"printf 'hello from CLI\\n'","activation":"export WORKLOAD_TEST=1"}`)
output, err := command.CombinedOutput()
if err != nil {
+2 -2
View File
@@ -100,7 +100,7 @@ func Run(ctx context.Context, config Config, payload Payload, publisher Publishe
if config.Stderr == nil {
config.Stderr = io.Discard
}
directory, err := os.MkdirTemp("", "slurm-worker-")
directory, err := os.MkdirTemp("", "bec-slurm-worker-")
if err != nil {
return 1, err
}
@@ -162,7 +162,7 @@ func Run(ctx context.Context, config Config, payload Payload, publisher Publishe
stdout := &lineWriter{local: config.Stdout, stream: "stdout", publish: publish, onError: cancelWork}
stderr := &lineWriter{local: config.Stderr, stream: "stderr", publish: publish, onError: cancelWork}
command := exec.Command("/bin/bash", "-c", shellSetup, "slurm-worker", activation, script)
command := exec.Command("/bin/bash", "-c", shellSetup, "bec-slurm-worker", activation, script)
command.Env = append(os.Environ(), "HOME="+home)
command.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
command.Stdout, command.Stderr = stdout, stderr
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env bash
# Match CAGO's Gitea authentication: username:token in GITEA_REGISTRY_TOKEN.
set -euo pipefail
if (( $# < 1 || $# > 2 )); then
echo 'Usage: upload-packages.sh VERSION [ARTIFACT_DIRECTORY]' >&2
exit 1
fi
: "${GITEA_REGISTRY_TOKEN:?Set GITEA_REGISTRY_TOKEN to username:token}"
version=$1
if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$ ]]; then
echo 'Expected a semantic version without a leading v.' >&2
exit 1
fi
artifact_directory=${2:-worker/dist}
shopt -s nullglob
archives=("$artifact_directory"/bec-slurm-worker_*.tar.gz)
rpms=("$artifact_directory"/bec-slurm-worker-*.rpm)
checksums="$artifact_directory/checksums.txt"
if (( ${#archives[@]} == 0 || ${#rpms[@]} == 0 )) || [[ ! -s "$checksums" ]]; then
echo 'Linux archives, RPMs, and checksums are required; refusing an incomplete upload.' >&2
exit 1
fi
# Keep a downloadable release bundle in the generic registry, including RPMs so
# every entry in checksums.txt can be downloaded from the same package version.
for package in "${archives[@]}" "${rpms[@]}" "$checksums"; do
file_name=$(basename "$package")
echo "Uploading $file_name to the BEC generic registry"
curl --fail-with-body --silent --show-error \
--connect-timeout 15 --max-time 300 \
--user "$GITEA_REGISTRY_TOKEN" \
--upload-file "$package" \
"https://gitea.psi.ch/api/packages/bec/generic/bec-slurm-worker/$version/$file_name"
done
# Also index RPMs in the existing BEC RPM repository for Puppet/dnf deployment.
for package in "${rpms[@]}"; do
echo "Uploading $(basename "$package") to the BEC RPM registry"
curl --fail-with-body --silent --show-error \
--connect-timeout 15 --max-time 300 \
--user "$GITEA_REGISTRY_TOKEN" \
--upload-file "$package" \
'https://gitea.psi.ch/api/packages/bec/rpm/upload'
done