feat: 删除旧的发布工作流,添加新的发布工作流,重构更新逻辑以支持跨平台重启

This commit is contained in:
ryan
2026-03-10 23:02:51 +08:00
parent fbc27e9d5d
commit 6fe9ad4af6
5 changed files with 189 additions and 114 deletions
-66
View File
@@ -1,66 +0,0 @@
name: Agent Release
permissions:
contents: write
on:
push:
tags:
- "*"
workflow_dispatch:
inputs:
reason:
description: "Reason for manual trigger"
required: false
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: ">=1.18.0"
- name: Build Agent (linux/amd64)
working-directory: atsf_agent
run: |
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-s -w" -o ../atsflare-agent-linux-amd64 ./cmd/agent
- name: Build Agent (linux/arm64)
working-directory: atsf_agent
run: |
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags "-s -w" -o ../atsflare-agent-linux-arm64 ./cmd/agent
- name: Build Agent (darwin/arm64)
working-directory: atsf_agent
run: |
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags "-s -w" -o ../atsflare-agent-darwin-arm64 ./cmd/agent
- name: Determine prerelease
id: prerelease
run: |
TAG="${GITHUB_REF#refs/tags/}"
if [[ "$TAG" == *alpha* ]] || [[ "$TAG" == *beta* ]] || [[ "$TAG" == *rc* ]]; then
echo "is_prerelease=true" >> "$GITHUB_OUTPUT"
else
echo "is_prerelease=false" >> "$GITHUB_OUTPUT"
fi
- name: Release
uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/')
with:
files: |
atsflare-agent-linux-amd64
atsflare-agent-linux-arm64
atsflare-agent-darwin-arm64
draft: false
prerelease: ${{ steps.prerelease.outputs.is_prerelease }}
generate_release_notes: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -1,17 +1,38 @@
name: Server Release
name: Release
permissions:
contents: write
on:
workflow_dispatch:
push:
tags:
- "*"
env:
VERSION: ${{ github.ref_name }}
branches: ["main"]
tags: ["v*"]
jobs:
prepare:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
is_prerelease: ${{ steps.version.outputs.is_prerelease }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Resolve version metadata
id: version
run: |
VERSION="$(git describe --tags)"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
if [[ "$VERSION" =~ ^v[0-9]+(\.[0-9]+)*$ ]]; then
echo "is_prerelease=false" >> "$GITHUB_OUTPUT"
else
echo "is_prerelease=true" >> "$GITHUB_OUTPUT"
fi
build-frontend:
needs: prepare
runs-on: ubuntu-latest
steps:
- name: Checkout
@@ -27,6 +48,7 @@ jobs:
- name: Build Frontend
env:
CI: ""
VERSION: ${{ needs.prepare.outputs.version }}
run: |
cd atsf_server/web
npm install
@@ -40,7 +62,9 @@ jobs:
retention-days: 1
build-binaries:
needs: build-frontend
needs:
- prepare
- build-frontend
runs-on: ubuntu-latest
strategy:
fail-fast: false
@@ -86,6 +110,7 @@ jobs:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
ASSET_NAME: ${{ matrix.asset_name }}
VERSION: ${{ needs.prepare.outputs.version }}
run: |
go mod download
mkdir -p ../dist
@@ -98,34 +123,82 @@ jobs:
path: dist/${{ matrix.asset_name }}
retention-days: 1
build-agent-binaries:
needs: prepare
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- goos: linux
goarch: amd64
asset_name: atsflare-agent-linux-amd64
- goos: linux
goarch: arm64
asset_name: atsflare-agent-linux-arm64
- goos: darwin
goarch: amd64
asset_name: atsflare-agent-darwin-amd64
- goos: darwin
goarch: arm64
asset_name: atsflare-agent-darwin-arm64
- goos: windows
goarch: amd64
asset_name: atsflare-agent-windows-amd64.exe
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: atsf_agent/go.mod
- name: Build Agent
working-directory: atsf_agent
env:
CGO_ENABLED: 0
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
ASSET_NAME: ${{ matrix.asset_name }}
run: |
go mod download
mkdir -p ../dist
go build -trimpath -ldflags "-s -w" -o "../dist/$ASSET_NAME" ./cmd/agent
- name: Upload Agent Artifact
uses: actions/upload-artifact@v4
with:
name: agent-${{ matrix.goos }}-${{ matrix.goarch }}
path: dist/${{ matrix.asset_name }}
retention-days: 1
release:
needs: build-binaries
needs:
- prepare
- build-binaries
- build-agent-binaries
runs-on: ubuntu-latest
steps:
- name: Download Binary Artifacts
uses: actions/download-artifact@v4
with:
pattern: server-*
pattern: "*-*"
path: dist
merge-multiple: true
- name: Determine prerelease
id: prerelease
run: |
TAG="$VERSION"
if [[ "$TAG" == *alpha* ]] || [[ "$TAG" == *beta* ]] || [[ "$TAG" == *rc* ]]; then
echo "is_prerelease=true" >> "$GITHUB_OUTPUT"
else
echo "is_prerelease=false" >> "$GITHUB_OUTPUT"
fi
- name: Release
uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/')
with:
tag_name: ${{ needs.prepare.outputs.version }}
name: ${{ needs.prepare.outputs.version }}
target_commitish: ${{ github.sha }}
files: dist/*
draft: false
prerelease: ${{ steps.prerelease.outputs.is_prerelease }}
prerelease: ${{ needs.prepare.outputs.is_prerelease == 'true' }}
generate_release_notes: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -0,0 +1,27 @@
//go:build !windows
package updater
import (
"fmt"
"os"
"syscall"
)
func replaceAndRestart(execPath string, tmpPath string) error {
backupPath := execPath + ".bak"
os.Remove(backupPath)
if err := os.Rename(execPath, backupPath); err != nil {
os.Remove(tmpPath)
return fmt.Errorf("backup current binary: %w", err)
}
if err := os.Rename(tmpPath, execPath); err != nil {
os.Rename(backupPath, execPath)
return fmt.Errorf("replace binary: %w", err)
}
os.Remove(backupPath)
if err := syscall.Exec(execPath, os.Args, os.Environ()); err != nil {
return fmt.Errorf("exec restart: %w", err)
}
return fmt.Errorf("unreachable after exec")
}
@@ -0,0 +1,53 @@
//go:build windows
package updater
import (
"fmt"
"os"
"os/exec"
"strings"
)
func replaceAndRestart(execPath string, tmpPath string) error {
backupPath := execPath + ".bak"
scriptPath := execPath + ".update.cmd"
script := fmt.Sprintf(`@echo off
setlocal
:waitloop
move /Y "%s" "%s" >nul 2>nul
if errorlevel 1 (
ping 127.0.0.1 -n 2 >nul
goto waitloop
)
move /Y "%s" "%s" >nul 2>nul
if errorlevel 1 exit /b 1
start "" %s
del /Q "%s" >nul 2>nul
del /Q "%%~f0" >nul 2>nul
`, execPath, backupPath, tmpPath, execPath, buildWindowsCommandLine(execPath, os.Args[1:]), backupPath)
if err := os.WriteFile(scriptPath, []byte(script), 0o700); err != nil {
os.Remove(tmpPath)
return fmt.Errorf("write restart script: %w", err)
}
cmd := exec.Command("cmd", "/C", "start", "", scriptPath)
if err := cmd.Start(); err != nil {
os.Remove(scriptPath)
os.Remove(tmpPath)
return fmt.Errorf("schedule restart: %w", err)
}
os.Exit(0)
return nil
}
func buildWindowsCommandLine(execPath string, args []string) string {
parts := []string{quoteWindowsArg(execPath)}
for _, arg := range args {
parts = append(parts, quoteWindowsArg(arg))
}
return strings.Join(parts, " ")
}
func quoteWindowsArg(value string) string {
return `"` + strings.ReplaceAll(value, `"`, `""`) + `"`
}
+15 -27
View File
@@ -3,7 +3,6 @@ package updater
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"log"
@@ -11,7 +10,6 @@ import (
"os"
"runtime"
"strings"
"syscall"
"time"
"atsflare-agent/internal/config"
@@ -59,7 +57,7 @@ func (s *Service) CheckAndUpdate(ctx context.Context, repo string) error {
}
log.Printf("agent update available: %s -> %s", localVersion, remoteVersion)
assetName := fmt.Sprintf("atsflare-agent-%s-%s", runtime.GOOS, runtime.GOARCH)
assetName := assetNameForGOOSGOARCH(runtime.GOOS, runtime.GOARCH)
var downloadURL string
for _, asset := range release.Assets {
@@ -77,12 +75,10 @@ func (s *Service) CheckAndUpdate(ctx context.Context, repo string) error {
if err != nil {
return fmt.Errorf("get executable path: %w", err)
}
if err = s.downloadAndReplace(ctx, downloadURL, execPath); err != nil {
return fmt.Errorf("download and replace: %w", err)
if err = s.downloadAndRestart(ctx, downloadURL, execPath); err != nil {
return fmt.Errorf("download and restart: %w", err)
}
log.Printf("agent binary updated, restarting...")
return s.restart(execPath)
return nil
}
func (s *Service) getLatestRelease(ctx context.Context, repo string) (*githubRelease, error) {
@@ -113,7 +109,7 @@ func (s *Service) getLatestRelease(ctx context.Context, repo string) (*githubRel
return &release, nil
}
func (s *Service) downloadAndReplace(ctx context.Context, url string, targetPath string) error {
func (s *Service) downloadAndRestart(ctx context.Context, url string, targetPath string) error {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return err
@@ -129,6 +125,9 @@ func (s *Service) downloadAndReplace(ctx context.Context, url string, targetPath
}
tmpPath := targetPath + ".update"
if runtime.GOOS == "windows" && !strings.HasSuffix(strings.ToLower(tmpPath), ".exe") {
tmpPath += ".exe"
}
tmpFile, err := os.OpenFile(tmpPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o755)
if err != nil {
return err
@@ -140,27 +139,16 @@ func (s *Service) downloadAndReplace(ctx context.Context, url string, targetPath
}
tmpFile.Close()
backupPath := targetPath + ".bak"
os.Remove(backupPath)
if err = os.Rename(targetPath, backupPath); err != nil {
os.Remove(tmpPath)
return fmt.Errorf("backup current binary: %w", err)
}
if err = os.Rename(tmpPath, targetPath); err != nil {
// Attempt to restore backup
os.Rename(backupPath, targetPath)
return fmt.Errorf("replace binary: %w", err)
}
os.Remove(backupPath)
return nil
log.Printf("agent binary updated, restarting...")
return replaceAndRestart(targetPath, tmpPath)
}
func (s *Service) restart(execPath string) error {
argv := os.Args
if err := syscall.Exec(execPath, argv, os.Environ()); err != nil {
return fmt.Errorf("exec restart: %w", err)
func assetNameForGOOSGOARCH(goos string, goarch string) string {
name := fmt.Sprintf("atsflare-agent-%s-%s", goos, goarch)
if goos == "windows" {
return name + ".exe"
}
return errors.New("unreachable after exec")
return name
}
func normalizeVersion(v string) string {