diff --git a/.github/workflows/dectris-release.yml b/.github/workflows/dectris-release.yml index 5146b0fa..cb1d0808 100644 --- a/.github/workflows/dectris-release.yml +++ b/.github/workflows/dectris-release.yml @@ -8,10 +8,12 @@ on: workflow_dispatch: inputs: version: - description: "Release version (e.g. 0.43.8-dc.1). A 'v' prefix will be added for the tag/release." + # Deliberately has no default: it must match GEESEFS_VERSION in + # core/cfg/flags.go, and a default here goes stale the moment that + # constant moves. Pass it explicitly at dispatch time. + description: "Release version, must match GEESEFS_VERSION in core/cfg/flags.go (e.g. 0.43.8-dc.2). A 'v' prefix is added for the tag/release." required: true type: string - default: '0.43.8-dc.1' concurrency: group: ${{ github.workflow }}-${{ github.ref }} diff --git a/CLAUDE.md b/CLAUDE.md index b60714eb..2aee87b0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -80,6 +80,42 @@ cd docker && just test-multipart-boundary **Virtual symlinks**: Symlinks stored in `.geesefs_symlinks` (no S3 object) are distinguished from S3-backed symlinks via `Inode.isVirtualSymlink` bool field. Always use this field for detection, not `userMetadata[SymlinkAttr] != nil`. +## Branches + +**`dev` is the main branch.** All dectris development on geesefs lives here, every PR targets it, and releases are cut from it. Treat it as the trunk. + +**`master` is a read-only mirror of `yandex-cloud/geesefs`.** It must always point at the exact upstream commit the fork is based on, and must never carry a dectris change. Verify with: + +```bash +git fetch upstream +git rev-list --count upstream/master..origin/master # must be 0 +``` + +Upstream syncs go `master` → `dev`: fast-forward `master` to the new upstream commit, then merge `master` into `dev` and resolve there. + +Opening a PR against `master` is always wrong. Its content will be absent from `dev`, and a release cut afterwards silently ships without it, with nothing failing to warn you. If it happens, merge `master` into `dev` to recover the work, then reset `master` back to the upstream commit. + +Older branches on the remote (`symlinks`, `fix/utf-8`, `ci/dev_releases`, `dectris-master`, `perf-fio-sync-master`, …) predate this layout and are superseded. Their content is already in `dev`; do not branch from them. + +## Releasing + +The version lives in one place: `GEESEFS_VERSION` in `core/cfg/flags.go`. Bump it, merge to `dev`, then dispatch: + +```bash +gh workflow run dectris-release.yml -f version= --ref dev +gh release edit v --prerelease=false --latest +``` + +The version is a dispatch parameter, so do not edit the workflow to change it. It has no default on purpose: a default duplicates `GEESEFS_VERSION` and goes stale the moment that constant moves. + +A dispatched run is marked prerelease, hence the `release edit`. Pushing a `v*` tag instead publishes a full release directly, but only the dispatch path is exercised regularly. + +Tag scheme: `v-dc.`, e.g. `v0.43.8-dc.2`. + ## Go Module Module: `github.com/yandex-cloud/geesefs` (Go 1.25). Uses the stock `github.com/aws/aws-sdk-go` with no `replace` directive. Yandex-only S3 extensions (`PatchObject`, `ListObjectsV1Ext`) live in `core/ycs3ext/`, built on the SDK's `request.Request` rather than a forked SDK. + +## Downstream + +`compute-amis` pins the version in `terraform/infrastructure/main.tf` (`geesefs_version`) and its AMI recipe downloads the release asset. A new release needs a matching bump there. diff --git a/core/cfg/flags.go b/core/cfg/flags.go index da32db04..944e227e 100644 --- a/core/cfg/flags.go +++ b/core/cfg/flags.go @@ -30,7 +30,7 @@ import ( "github.com/urfave/cli" ) -const GEESEFS_VERSION = "0.43.8-dc.1" +const GEESEFS_VERSION = "0.43.8-dc.2" var flagCategories map[string]string diff --git a/main.go b/main.go index bebda3ab..670ad9d7 100644 --- a/main.go +++ b/main.go @@ -152,6 +152,10 @@ func main() { log.Println("File system has been successfully mounted.") if !flags.Foreground { daemonizer.NotifySuccess(true) + // Runtime panics write to fd 2, which the two Close calls + // below discard, so a crashed daemon leaves no trace anywhere. + // Route crash output to a file first (Go keeps a dup of it). + setupCrashLog(bucketName, flags.MountPoint) os.Stderr.Close() os.Stdout.Close() } diff --git a/main_nowindows.go b/main_nowindows.go index 22dcff1e..96890b0c 100644 --- a/main_nowindows.go +++ b/main_nowindows.go @@ -22,9 +22,11 @@ import ( "fmt" "os" "os/signal" + "runtime/debug" "strings" "sync" "syscall" + "time" daemon "github.com/sevlyar/go-daemon" @@ -32,6 +34,34 @@ import ( "github.com/yandex-cloud/geesefs/core/cfg" ) +// Where Go runtime crash output goes once the daemon has closed its stderr. +// Overridable for non-root mounts via GEESEFS_CRASH_LOG. +const defaultCrashLogPath = "/var/log/geesefs-crash.log" + +// setupCrashLog routes runtime crash output (panics, fatal errors, deadlock +// dumps) to a shared append-only file. These bypass the logger and write to +// fd 2, which the daemon closes right after mounting, so without this a +// crashed daemon leaves no trace anywhere. The header line ties the pid to +// its mount so a following dump can be attributed. +func setupCrashLog(bucketName string, mountPoint string) { + path := os.Getenv("GEESEFS_CRASH_LOG") + if path == "" { + path = defaultCrashLogPath + } + f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0600) + if err != nil { + log.Warnf("Cannot open crash log %v: %v; runtime panics will be lost", path, err) + return + } + fmt.Fprintf(f, "--- %s geesefs pid %d serving %s at %s: crash output armed ---\n", + time.Now().UTC().Format(time.RFC3339), os.Getpid(), bucketName, mountPoint) + if err = debug.SetCrashOutput(f, debug.CrashOptions{}); err != nil { + log.Warnf("Cannot set crash output to %v: %v", path, err) + } + // SetCrashOutput keeps its own duplicate of the descriptor. + f.Close() +} + var signalsToHandle = []os.Signal{os.Interrupt, syscall.SIGTERM, syscall.SIGUSR1} func isSigUsr1(s os.Signal) bool { diff --git a/main_windows.go b/main_windows.go index afbdf14d..d6a3c219 100644 --- a/main_windows.go +++ b/main_windows.go @@ -25,6 +25,11 @@ import ( var signalsToHandle = []os.Signal{os.Interrupt, syscall.SIGTERM} +// Unreachable on Windows: canDaemonize is false, so the daemon-only branch +// that calls this never runs. Present only to keep the build compiling. +func setupCrashLog(bucketName string, mountPoint string) { +} + func isSigUsr1(s os.Signal) bool { return false }