diff --git a/services/forgejo-nsc/internal/nsc/macos.go b/services/forgejo-nsc/internal/nsc/macos.go index 30be465..d20fd57 100644 --- a/services/forgejo-nsc/internal/nsc/macos.go +++ b/services/forgejo-nsc/internal/nsc/macos.go @@ -645,7 +645,9 @@ fi mkdir -p bin export PATH="${PWD}/bin:${PATH}" -runner_version="v12.6.4" +# Keep the ad-hoc macOS bootstrap on the same Forgejo runner major/minor line +# as the server-side LTS package and the Linux runner image. +runner_version="v11.0.11" runner_src_tgz="forgejo-runner-${runner_version}.tar.gz" runner_src_tgz_path="${cache_root}/downloads/${runner_src_tgz}" runner_src_url="https://code.forgejo.org/forgejo/runner/archive/${runner_version}.tar.gz" diff --git a/services/forgejo-nsc/internal/nsc/macos_nsc.go b/services/forgejo-nsc/internal/nsc/macos_nsc.go index a337572..1656028 100644 --- a/services/forgejo-nsc/internal/nsc/macos_nsc.go +++ b/services/forgejo-nsc/internal/nsc/macos_nsc.go @@ -64,6 +64,13 @@ func normalizeMacOSNSCMachineType(machineType string) (normalized string, change return normalized, changed, nil } +type macosNSCSSHOutcome int + +const ( + macosNSCSSHCompleted macosNSCSSHOutcome = iota + macosNSCSSHHandoff +) + func (d *Dispatcher) launchMacOSRunnerViaNSC(ctx context.Context, runnerName string, req LaunchRequest, ttl time.Duration, machineType string) error { if machineType == "" { return errors.New("machine_type is required for macos runners") @@ -216,15 +223,30 @@ func (d *Dispatcher) launchMacOSRunnerViaNSC(ctx context.Context, runnerName str return fmt.Errorf("nsc create failed without producing an instance id\n%s", lastOut) } - // Always attempt cleanup even if the runner fails. - defer d.destroyNSCInstance(context.Background(), runnerName, instanceID) + destroyOnReturn := true + // Always attempt cleanup on failure; successful handoff is allowed to run out + // to its NSC TTL because `nsc ssh` may detach before the Forgejo job exits. + defer func() { + if destroyOnReturn { + d.destroyNSCInstance(context.Background(), runnerName, instanceID) + } + }() script := macosBootstrapWrapperScript(runnerName, req, d.opts.Executor, d.opts.WorkDir) // The CLI fallback is explicitly keychain-backed and does not rely on the // service bearer token, so use `nsc ssh` end-to-end here. - if err := d.runMacOSNSCSSHScript(ctx, runnerName, instanceID, script); err != nil { + outcome, err := d.runMacOSNSCSSHScript(ctx, runnerName, instanceID, script) + if err != nil { return err } + if outcome == macosNSCSSHHandoff { + destroyOnReturn = false + d.log.Info("leaving macos nsc instance running until TTL after runner handoff", + "runner", runnerName, + "instance", instanceID, + "ttl", ttl.String(), + ) + } return nil } @@ -344,7 +366,7 @@ func shellSingleQuote(value string) string { return "'" + strings.ReplaceAll(value, "'", `'\"'\"'`) + "'" } -func (d *Dispatcher) runMacOSNSCSSHScript(ctx context.Context, runnerName, instanceID, script string) error { +func (d *Dispatcher) runMacOSNSCSSHScript(ctx context.Context, runnerName, instanceID, script string) (macosNSCSSHOutcome, error) { sshCtx, cancel := context.WithTimeout(ctx, 5*time.Minute) defer cancel() @@ -361,7 +383,7 @@ func (d *Dispatcher) runMacOSNSCSSHScript(ctx context.Context, runnerName, insta if err := cmd.Run(); err != nil { if errors.Is(sshCtx.Err(), context.DeadlineExceeded) { - return fmt.Errorf("nsc ssh timed out after %s\n%s", 5*time.Minute, strings.TrimSpace(buf.String())) + return macosNSCSSHCompleted, fmt.Errorf("nsc ssh timed out after %s\n%s", 5*time.Minute, strings.TrimSpace(buf.String())) } if nscSSHBootstrapLikelySucceeded(err, buf.String()) { d.log.Warn("nsc ssh exited after runner handoff; treating bootstrap as successful", @@ -370,13 +392,13 @@ func (d *Dispatcher) runMacOSNSCSSHScript(ctx context.Context, runnerName, insta "err", err, ) d.log.Info("macos runner bootstrap completed via nsc ssh", "runner", runnerName, "instance", instanceID) - return nil + return macosNSCSSHHandoff, nil } - return fmt.Errorf("nsc ssh runner bootstrap failed: %w\n%s", err, strings.TrimSpace(buf.String())) + return macosNSCSSHCompleted, fmt.Errorf("nsc ssh runner bootstrap failed: %w\n%s", err, strings.TrimSpace(buf.String())) } d.log.Info("macos runner bootstrap completed via nsc ssh", "runner", runnerName, "instance", instanceID) - return nil + return macosNSCSSHCompleted, nil } func nscSSHBootstrapLikelySucceeded(err error, output string) bool {