Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 103 additions & 9 deletions .github/workflows/build&release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -122,19 +122,113 @@ jobs:

cp release/supernode release/${{ steps.vars.outputs.binary_name }}

- name: Publish the Release
- name: Check if release already exists
id: rel_check
uses: actions/github-script@v7
env:
TAG_NAME: ${{ steps.tag_info.outputs.tag_name }}
with:
script: |
const { owner, repo } = context.repo;
const tag_name = process.env.TAG_NAME;
try {
await github.request('GET /repos/{owner}/{repo}/releases/tags/{tag}', {
owner,
repo,
tag: tag_name,
});
core.setOutput('exists', 'true');
} catch (e) {
// 404 means not found => will be created
core.setOutput('exists', 'false');
}

- name: Generate auto release notes
id: auto_notes
uses: actions/github-script@v7
if: steps.rel_check.outputs.exists != 'true'
env:
TAG_NAME: ${{ steps.tag_info.outputs.tag_name }}
with:
script: |
const { owner, repo } = context.repo;
const tag_name = process.env.TAG_NAME;
const res = await github.request('POST /repos/{owner}/{repo}/releases/generate-notes', {
owner,
repo,
tag_name
});
core.setOutput('body', res.data.body || '');

- name: Prepare Release Body
id: rel_body
if: steps.rel_check.outputs.exists != 'true'
run: |
cat > /tmp/REL_BODY <<'EOT'
IMPORTANT: sn-manager setup and auto-update guide:
https://github.com/LumeraProtocol/supernode/blob/master/sn-manager/README.md

${{ steps.tag_info.outputs.tag_message }}

---
Auto-generated release notes:
${{ steps.auto_notes.outputs.body }}
EOT
{
echo "body<<GHOEOF"
cat /tmp/REL_BODY
echo "GHOEOF"
} >> "$GITHUB_OUTPUT"

- name: Create release with composed body (no existing release)
uses: softprops/action-gh-release@v0.1.15
if: success()
if: success() && steps.rel_check.outputs.exists != 'true'
with:
tag_name: ${{ steps.tag_info.outputs.tag_name }}
files: |
release/${{ steps.vars.outputs.binary_name }}.tar.gz
release/${{ steps.vars.outputs.binary_name }}
generate_release_notes: true
body: |
${{ steps.tag_info.outputs.tag_message }}

Version: ${{ steps.vars.outputs.version }}
Git Commit: ${{ steps.vars.outputs.git_commit }}
Build Time: ${{ steps.vars.outputs.build_time }}
body: ${{ steps.rel_body.outputs.body }}
token: ${{ secrets.GITHUB_TOKEN }}

- name: Upload assets to existing release (preserve manual notes)
uses: softprops/action-gh-release@v0.1.15
if: success() && steps.rel_check.outputs.exists == 'true'
with:
tag_name: ${{ steps.tag_info.outputs.tag_name }}
files: |
release/${{ steps.vars.outputs.binary_name }}.tar.gz
release/${{ steps.vars.outputs.binary_name }}
token: ${{ secrets.GITHUB_TOKEN }}

- name: Ensure guide link present in existing release body
if: success() && steps.rel_check.outputs.exists == 'true'
uses: actions/github-script@v7
env:
TAG_NAME: ${{ steps.tag_info.outputs.tag_name }}
GUIDE_URL: https://github.com/LumeraProtocol/supernode/blob/master/sn-manager/README.md
with:
script: |
const { owner, repo } = context.repo;
const tag_name = process.env.TAG_NAME;
const guide = `IMPORTANT: sn-manager setup and auto-update guide:\n${process.env.GUIDE_URL}`;
// Fetch release
const rel = await github.request('GET /repos/{owner}/{repo}/releases/tags/{tag}', {
owner,
repo,
tag: tag_name,
});
const release = rel.data;
const body = release.body || '';
if (body.includes(process.env.GUIDE_URL)) {
core.info('Guide link already present; not modifying release body.');
} else {
const newBody = `${guide}\n\n${body}`;
await github.request('PATCH /repos/{owner}/{repo}/releases/{release_id}', {
owner,
repo,
release_id: release.id,
body: newBody,
});
core.info('Prepended guide link to existing release body.');
}
66 changes: 66 additions & 0 deletions sn-manager/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,21 @@

SuperNode Process Manager with Automatic Updates

## Table of Contents

- [Installation](#installation)
- [Systemd Service Setup](#systemd-service-setup)
- [Ensure PATH points to user install](#ensure-path-points-to-user-install-required-for-self-update)
- [Initialization](#initialization)
- [Commands](#commands)
- [Version Update Scenarios](#version-update-scenarios)
- [Start/Stop Behavior](#startstop-behavior)
- [Migration for Existing sn-manager Users](#migration-for-existing-sn-manager-users)
- [Troubleshooting](#troubleshooting)
- [Fix non-writable install](#fix-non-writable-install)
- [Configuration](#configuration)
- [Notes](#notes)

## Installation

Download and install sn-manager:
Expand Down Expand Up @@ -86,6 +101,14 @@ readlink -f "$(command -v sn-manager)"

The systemd unit uses an absolute `ExecStart` pointing to your home directory, so the service will always run the intended binary regardless of PATH.

Note: Auto-upgrade requires the sn-manager binary directory to be writable by the service user. If you encounter an error like:

```
auto-upgrade is enabled but sn-manager binary directory is not writable (...)
```

follow the steps in [Fix non-writable install](#fix-non-writable-install).

## Initialization

### Interactive Mode
Expand Down Expand Up @@ -270,6 +293,49 @@ sudo systemctl restart sn-manager
- Check updates: `sn-manager check`


## Troubleshooting

### Fix non-writable install

Symptom:

```
auto-upgrade is enabled but sn-manager binary directory is not writable (...)
```

Cause: `sn-manager` is installed in a root-owned directory such as `/usr/local/bin`, so the auto-updater cannot write `sn-manager.new` during self-update.

Fix:

1. Reinstall `sn-manager` to a user-writable path:
```bash
curl -L https://github.com/LumeraProtocol/supernode/releases/latest/download/supernode-linux-amd64.tar.gz | tar -xz
install -D -m 0755 sn-manager "$HOME/.sn-manager/bin/sn-manager"
echo 'export PATH="$HOME/.sn-manager/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc && hash -r
sn-manager version
```
2. Update the systemd unit to point to the user install and set HOME/workdir:
```ini
[Service]
User=<YOUR_USER>
ExecStart=/home/<YOUR_USER>/.sn-manager/bin/sn-manager start
Environment="HOME=/home/<YOUR_USER>"
WorkingDirectory=/home/<YOUR_USER>
Restart=on-failure
RestartSec=10
```
3. Remove the global copy so PATH doesn’t pick it:
```bash
sudo rm -f /usr/local/bin/sn-manager && hash -r
```
4. Restart the service:
```bash
sudo systemctl daemon-reload
sudo systemctl restart sn-manager
```


## Configuration

### SN-Manager (`~/.sn-manager/config.yml`)
Expand Down
19 changes: 19 additions & 0 deletions sn-manager/cmd/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,25 @@ func runStart(cmd *cobra.Command, args []string) error {
}
}

// Sanity check: if auto-upgrade is enabled and sn-manager binary dir is not writable, error and exit with guidance
if exePath, err := os.Executable(); err == nil {
if exeReal, err := filepath.EvalSymlinks(exePath); err == nil {
exeDir := filepath.Dir(exeReal)
if ok, _ := utils.IsDirWritable(exeDir); !ok {
if cfg.Updates.AutoUpgrade {
return fmt.Errorf(
"auto-upgrade is enabled but sn-manager binary directory is not writable (%s).\nInstall sn-manager to a user-writable path and update your systemd unit as per the README: %s\nRecommended path: %s",
exeDir,
"https://github.com/LumeraProtocol/supernode/blob/master/sn-manager/README.md#fix-non-writable-install",
filepath.Join(home, "bin", "sn-manager"),
)
}
// If auto-upgrade is disabled, warn but continue
log.Printf("Warning: sn-manager binary directory is not writable (%s). Self-update is disabled.", exeDir)
}
}
}

// Start auto-updater if enabled
var autoUpdater *updater.AutoUpdater
if cfg.Updates.AutoUpgrade {
Expand Down
34 changes: 34 additions & 0 deletions sn-manager/internal/utils/fs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package utils

import (
"fmt"
"os"
"path/filepath"
)

// IsDirWritable returns true if the directory is writable by the current process.
// It attempts to create and remove a temporary file inside the directory.
func IsDirWritable(dir string) (bool, error) {
if dir == "" {
return false, fmt.Errorf("empty directory path")
}
// Ensure directory exists
if fi, err := os.Stat(dir); err != nil || !fi.IsDir() {
if err != nil {
return false, err
}
return false, fmt.Errorf("not a directory: %s", dir)
}
// Try to create a temp file
tmpPath := filepath.Join(dir, ".wtest.tmp")
f, err := os.OpenFile(tmpPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
if err != nil {
return false, nil
}
_ = f.Close()
if err := os.Remove(tmpPath); err != nil && !os.IsNotExist(err) {
// Consider directory writable even if cleanup failed, but report error
return true, err
}
return true, nil
}