diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d07ecc1..7b690b9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,8 +2,6 @@ name: Release on: push: - branches: - - main tags: - "v*" @@ -22,6 +20,14 @@ jobs: with: fetch-depth: 0 # Full history required for changelog generation + - name: Verify tag is on main + run: | + git fetch origin main + if ! git merge-base --is-ancestor HEAD origin/main; then + echo "Tag v${{ github.ref_name }} is not an ancestor of main — refusing release." + exit 1 + fi + - name: Set up Go uses: actions/setup-go@v5 with: diff --git a/internal/plugins/runcommand/runcommand.go b/internal/plugins/runcommand/runcommand.go index 2903373..19ff21c 100644 --- a/internal/plugins/runcommand/runcommand.go +++ b/internal/plugins/runcommand/runcommand.go @@ -20,6 +20,7 @@ type RunCommandPlugin struct { Commands map[string]string CommandsPerDevice map[string]map[string]string // keyed by device ID logger *zap.Logger + wg sync.WaitGroup // exported for tests to synchronize with background goroutines } func NewRunCommandPlugin(commands map[string]string, commandsPerDevice map[string]map[string]string, logger *zap.Logger) *RunCommandPlugin { @@ -110,7 +111,9 @@ func (p *RunCommandPlugin) Handle(ctx context.Context, dev device.Sender, pkt *p // Handlers must not block. Spawning goroutine to run the command // and optionally send a notification with the output. + p.wg.Add(1) go func() { + defer p.wg.Done() execCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() diff --git a/internal/plugins/runcommand/runcommand_test.go b/internal/plugins/runcommand/runcommand_test.go index 552a307..4069251 100644 --- a/internal/plugins/runcommand/runcommand_test.go +++ b/internal/plugins/runcommand/runcommand_test.go @@ -22,6 +22,7 @@ func TestRunCommandPlugin_Handle_GlobalCommand(t *testing.T) { if err := p.Handle(context.Background(), dev, pkt); err != nil { t.Fatalf("Handle failed: %v", err) } + p.wg.Wait() } func TestRunCommandPlugin_Handle_PerDeviceOverrides(t *testing.T) { @@ -41,6 +42,7 @@ func TestRunCommandPlugin_Handle_PerDeviceOverrides(t *testing.T) { if err := p.Handle(context.Background(), dev1, pkt); err != nil { t.Fatalf("Handle for dev1 failed: %v", err) } + p.wg.Wait() // Device dev2 should fall back to the global command. dev2 := device.NewDevice("dev2", "Phone 2", "phone", logger) @@ -48,6 +50,7 @@ func TestRunCommandPlugin_Handle_PerDeviceOverrides(t *testing.T) { if err := p.Handle(context.Background(), dev2, pkt); err != nil { t.Fatalf("Handle for dev2 failed: %v", err) } + p.wg.Wait() } func TestRunCommandPlugin_Handle_RequestCommandList(t *testing.T) {