Skip to content

Commit de638f5

Browse files
authored
feat(update): default checks and installers to CDN (#37)
1 parent d49fd69 commit de638f5

13 files changed

Lines changed: 455 additions & 99 deletions

File tree

.github/workflows/install-scripts.yml

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -60,22 +60,23 @@ jobs:
6060
aws configure set default.response_checksum_validation when_required
6161
PREFIX="${PREFIX#/}"; PREFIX="${PREFIX%/}"
6262
63-
# Bake the CDN as the default MIRROR_URL into the copy we serve from the
64-
# CDN, so `curl <cdn>/install.sh | sh` pulls binaries from the CDN with
65-
# no MIRROR_URL arg. The repo / GitHub copy stays generic (GitHub default).
6663
src_sh=install.sh
64+
src_ps1=install.ps1
6765
if [ -n "${MIRROR_PUBLIC_URL:-}" ]; then
6866
pub="${MIRROR_PUBLIC_URL%/}${PREFIX:+/${PREFIX}}"
69-
sed "s#MIRROR_URL=\"\${MIRROR_URL:-}\"#MIRROR_URL=\"\${MIRROR_URL:-${pub}}\"#" install.sh > /tmp/install.sh
70-
grep -q "MIRROR_URL:-${pub}" /tmp/install.sh || { echo "ERROR: MIRROR_URL default not injected (install.sh default line changed?)" >&2; exit 1; }
67+
sed "s#^DEFAULT_MIRROR_URL=.*#DEFAULT_MIRROR_URL=\"${pub}\"#" install.sh > /tmp/install.sh
68+
grep -q "DEFAULT_MIRROR_URL=\"${pub}\"" /tmp/install.sh || { echo "ERROR: MIRROR_URL default not injected (install.sh default line changed?)" >&2; exit 1; }
7169
src_sh=/tmp/install.sh
70+
sed "s#^\$DefaultMirrorUrl = .*#\$DefaultMirrorUrl = \"${pub}\"#" install.ps1 > /tmp/install.ps1
71+
grep -q "\$DefaultMirrorUrl = \"${pub}\"" /tmp/install.ps1 || { echo "ERROR: MIRROR_URL default not injected (install.ps1 default line changed?)" >&2; exit 1; }
72+
src_ps1=/tmp/install.ps1
7273
fi
7374
sh_key="${PREFIX:+${PREFIX}/}install.sh"
7475
aws --endpoint-url="$ENDPOINT" s3 cp "$src_sh" "s3://${BUCKET}/${sh_key}" \
7576
--cache-control "public, max-age=300" \
7677
--content-type "text/x-shellscript; charset=utf-8"
7778
7879
ps1_key="${PREFIX:+${PREFIX}/}install.ps1"
79-
aws --endpoint-url="$ENDPOINT" s3 cp install.ps1 "s3://${BUCKET}/${ps1_key}" \
80+
aws --endpoint-url="$ENDPOINT" s3 cp "$src_ps1" "s3://${BUCKET}/${ps1_key}" \
8081
--cache-control "public, max-age=300" \
8182
--content-type "text/plain; charset=utf-8"

.github/workflows/release.yml

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -103,21 +103,22 @@ jobs:
103103
# ships a stale/missing installer (install-scripts.yml only fires when
104104
# install.sh/.ps1 change on main; the scripts are version-agnostic, so
105105
# re-uploading the current copy here is the belt-and-suspenders guarantee).
106-
# Bake the CDN as the default MIRROR_URL into the served copy so
107-
# `curl <cdn>/install.sh | sh` pulls binaries from the CDN with no
108-
# MIRROR_URL arg. The repo / GitHub copy stays generic (GitHub default).
109106
src_sh=install.sh
107+
src_ps1=install.ps1
110108
if [ -n "${MIRROR_PUBLIC_URL:-}" ]; then
111109
pub="${MIRROR_PUBLIC_URL%/}${PREFIX:+/${PREFIX}}"
112-
sed "s#MIRROR_URL=\"\${MIRROR_URL:-}\"#MIRROR_URL=\"\${MIRROR_URL:-${pub}}\"#" install.sh > /tmp/install.sh
113-
grep -q "MIRROR_URL:-${pub}" /tmp/install.sh || { echo "ERROR: MIRROR_URL default not injected (install.sh default line changed?)" >&2; exit 1; }
110+
sed "s#^DEFAULT_MIRROR_URL=.*#DEFAULT_MIRROR_URL=\"${pub}\"#" install.sh > /tmp/install.sh
111+
grep -q "DEFAULT_MIRROR_URL=\"${pub}\"" /tmp/install.sh || { echo "ERROR: MIRROR_URL default not injected (install.sh default line changed?)" >&2; exit 1; }
114112
src_sh=/tmp/install.sh
113+
sed "s#^\$DefaultMirrorUrl = .*#\$DefaultMirrorUrl = \"${pub}\"#" install.ps1 > /tmp/install.ps1
114+
grep -q "\$DefaultMirrorUrl = \"${pub}\"" /tmp/install.ps1 || { echo "ERROR: MIRROR_URL default not injected (install.ps1 default line changed?)" >&2; exit 1; }
115+
src_ps1=/tmp/install.ps1
115116
fi
116117
sh_key="${PREFIX:+${PREFIX}/}install.sh"
117118
aws --endpoint-url="$ENDPOINT" s3 cp "$src_sh" "s3://${BUCKET}/${sh_key}" \
118119
--cache-control "public, max-age=300" \
119120
--content-type "text/x-shellscript; charset=utf-8"
120121
ps1_key="${PREFIX:+${PREFIX}/}install.ps1"
121-
aws --endpoint-url="$ENDPOINT" s3 cp install.ps1 "s3://${BUCKET}/${ps1_key}" \
122+
aws --endpoint-url="$ENDPOINT" s3 cp "$src_ps1" "s3://${BUCKET}/${ps1_key}" \
122123
--cache-control "public, max-age=300" \
123124
--content-type "text/plain; charset=utf-8"

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@ A command-line interface for the [Flashduty](https://flashcat.cloud) platform. M
1414
### macOS / Linux
1515

1616
```bash
17-
curl -sSL https://raw.githubusercontent.com/flashcatcloud/flashduty-cli/main/install.sh | sh
17+
curl -sSL https://static.flashcat.cloud/flashduty-cli/install.sh | sh
1818
```
1919

2020
### Windows (PowerShell)
2121

2222
```powershell
23-
irm https://raw.githubusercontent.com/flashcatcloud/flashduty-cli/main/install.ps1 | iex
23+
irm https://static.flashcat.cloud/flashduty-cli/install.ps1 | iex
2424
```
2525

2626
### Manual Download
@@ -33,6 +33,8 @@ Download the latest release for your platform from [GitHub Releases](https://git
3333
|----------|-------------|---------|
3434
| `FLASHDUTY_VERSION` | Install a specific version (e.g. `v0.1.2`) | latest |
3535
| `FLASHDUTY_INSTALL_DIR` | Custom install directory | `/usr/local/bin` (shell), `~\.flashduty\bin` (PowerShell) |
36+
| `MIRROR_URL` | Override installer release asset mirror | `https://static.flashcat.cloud/flashduty-cli` |
37+
| `FLASHDUTY_UPDATE_BASE_URL` | Override `flashduty update` and auto update-check base URL | `https://static.flashcat.cloud/flashduty-cli` |
3638

3739
## Agent Skills
3840

README_zh.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@
1414
### macOS / Linux
1515

1616
```bash
17-
curl -sSL https://raw.githubusercontent.com/flashcatcloud/flashduty-cli/main/install.sh | sh
17+
curl -sSL https://static.flashcat.cloud/flashduty-cli/install.sh | sh
1818
```
1919

2020
### Windows (PowerShell)
2121

2222
```powershell
23-
irm https://raw.githubusercontent.com/flashcatcloud/flashduty-cli/main/install.ps1 | iex
23+
irm https://static.flashcat.cloud/flashduty-cli/install.ps1 | iex
2424
```
2525

2626
### 手动下载
@@ -33,6 +33,8 @@ irm https://raw.githubusercontent.com/flashcatcloud/flashduty-cli/main/install.p
3333
|------|------|--------|
3434
| `FLASHDUTY_VERSION` | 安装指定版本(如 `v0.1.2`| 最新版 |
3535
| `FLASHDUTY_INSTALL_DIR` | 自定义安装目录 | `/usr/local/bin`(Shell)、`~\.flashduty\bin`(PowerShell) |
36+
| `MIRROR_URL` | 覆盖安装脚本使用的 release 资源镜像 | `https://static.flashcat.cloud/flashduty-cli` |
37+
| `FLASHDUTY_UPDATE_BASE_URL` | 覆盖 `flashduty update` 和自动更新检查的 base URL | `https://static.flashcat.cloud/flashduty-cli` |
3638

3739
## Agent Skills(AI 代理技能)
3840

install.ps1

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
# Flashduty CLI installer for Windows
2-
# Usage: irm https://raw.githubusercontent.com/flashcatcloud/flashduty-cli/main/install.ps1 | iex
2+
# Usage: irm https://static.flashcat.cloud/flashduty-cli/install.ps1 | iex
33
#
44
# Environment variables:
55
# FLASHDUTY_VERSION - specific version to install (e.g. "v0.1.2")
66
# FLASHDUTY_INSTALL_DIR - install directory (default: $HOME\.flashduty\bin)
7-
# MIRROR_URL - fetch release assets from this https mirror prefix
8-
# instead of github.com. The mirror must replicate
7+
# MIRROR_URL - fetch release assets from this https mirror prefix.
8+
# Default: https://static.flashcat.cloud/flashduty-cli.
9+
# The mirror must replicate
910
# GitHub's release layout
1011
# (<MIRROR_URL>/releases/download/<tag>/<asset>) and
1112
# expose a plain-text <MIRROR_URL>/releases/latest file
@@ -18,8 +19,13 @@ $Repo = "flashcatcloud/flashduty-cli"
1819
$Binary = "flashduty-cli.exe"
1920
$InstalledName = "flashduty.exe"
2021

21-
# When set, all release downloads are fetched from this prefix instead of github.com.
22-
$MirrorUrl = $env:MIRROR_URL
22+
# By default release downloads are fetched from the Flashcat CDN. Set MIRROR_URL
23+
# to another prefix to override, or to an empty string to force GitHub fallback.
24+
$DefaultMirrorUrl = "https://static.flashcat.cloud/flashduty-cli"
25+
$MirrorUrl = [Environment]::GetEnvironmentVariable("MIRROR_URL")
26+
if ($null -eq $MirrorUrl) {
27+
$MirrorUrl = $DefaultMirrorUrl
28+
}
2329
if ($MirrorUrl) {
2430
$MirrorUrl = $MirrorUrl.TrimEnd('/')
2531
if ($MirrorUrl -notlike "https://*") {

install.sh

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
#!/bin/sh
22
# Flashduty CLI installer
3-
# Usage: curl -sSL https://raw.githubusercontent.com/flashcatcloud/flashduty-cli/main/install.sh | sh
3+
# Usage: curl -sSL https://static.flashcat.cloud/flashduty-cli/install.sh | sh
44
#
55
# Environment:
66
# FLASHDUTY_VERSION Install a specific version (e.g. v0.1.2). Default: latest.
77
# FLASHDUTY_INSTALL_DIR Install directory. Default: /usr/local/bin.
8-
# MIRROR_URL Fetch release assets from this https mirror prefix
9-
# instead of github.com. The mirror must replicate
8+
# MIRROR_URL Fetch release assets from this https mirror prefix.
9+
# Default: https://static.flashcat.cloud/flashduty-cli.
10+
# The mirror must replicate
1011
# GitHub's release layout
1112
# (<MIRROR_URL>/releases/download/<tag>/<asset>) and expose
1213
# a plain-text <MIRROR_URL>/releases/latest file containing
@@ -18,8 +19,10 @@ BINARY="flashduty-cli"
1819
INSTALLED_NAME="${INSTALLED_NAME:-flashduty}"
1920
INSTALL_DIR="${FLASHDUTY_INSTALL_DIR:-/usr/local/bin}"
2021

21-
# When set, all release downloads are fetched from this prefix instead of github.com.
22-
MIRROR_URL="${MIRROR_URL:-}"
22+
# By default release downloads are fetched from the Flashcat CDN. Set MIRROR_URL
23+
# to another prefix to override, or to an empty string to force GitHub fallback.
24+
DEFAULT_MIRROR_URL="https://static.flashcat.cloud/flashduty-cli"
25+
MIRROR_URL="${MIRROR_URL-${DEFAULT_MIRROR_URL}}"
2326
MIRROR_URL="${MIRROR_URL%/}"
2427
if [ -n "${MIRROR_URL}" ]; then
2528
case "${MIRROR_URL}" in

internal/cli/command_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ func saveAndResetGlobals(t *testing.T) {
2424
origFlagAppKey := flagAppKey
2525
origFlagBaseURL := flagBaseURL
2626
origFlagOutputFormat := flagOutputFormat
27+
origUpdateNotice := updateNotice
28+
origUpdateCheckWarning := updateCheckWarning
2729
origStdinReader := stdinReader
2830

2931
// Reset to defaults so tests start clean.
@@ -32,6 +34,8 @@ func saveAndResetGlobals(t *testing.T) {
3234
flagAppKey = ""
3335
flagBaseURL = ""
3436
flagOutputFormat = ""
37+
updateNotice = nil
38+
updateCheckWarning = ""
3539

3640
t.Cleanup(func() {
3741
newClientFn = origNewClientFn
@@ -40,6 +44,8 @@ func saveAndResetGlobals(t *testing.T) {
4044
flagAppKey = origFlagAppKey
4145
flagBaseURL = origFlagBaseURL
4246
flagOutputFormat = origFlagOutputFormat
47+
updateNotice = origUpdateNotice
48+
updateCheckWarning = origUpdateCheckWarning
4349
stdinReader = origStdinReader
4450
})
4551
}

internal/cli/root.go

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ var (
3232
)
3333

3434
var updateNotice *update.CheckResult
35+
var updateCheckWarning string
36+
var isTerminalFn = term.IsTerminal
37+
var checkForUpdateAutoFn = update.CheckForUpdateAuto
3538

3639
var rootCmd = &cobra.Command{
3740
Use: "flashduty",
@@ -50,27 +53,41 @@ var rootCmd = &cobra.Command{
5053
return err
5154
}
5255
}
56+
updateNotice = nil
57+
updateCheckWarning = ""
5358
if cmd.CommandPath() == "flashduty update" {
5459
return nil
5560
}
56-
if !term.IsTerminal(int(os.Stderr.Fd())) {
61+
if !isTerminalFn(int(os.Stderr.Fd())) {
5762
return nil
5863
}
59-
updateNotice = update.StateHasUpdate(versionStr)
6064
if update.ShouldCheck(versionStr) {
61-
go func() {
62-
_, _ = update.CheckForUpdate(versionStr)
63-
}()
65+
result, err := checkForUpdateAutoFn(versionStr)
66+
if err != nil {
67+
if update.IsTimeout(err) {
68+
updateCheckWarning = "auto update check timeout, please run 'flashduty update --check' manually"
69+
} else {
70+
updateNotice = update.StateHasUpdate(versionStr)
71+
}
72+
return nil
73+
}
74+
if result.UpdateAvailable {
75+
updateNotice = result
76+
}
77+
return nil
6478
}
79+
updateNotice = update.StateHasUpdate(versionStr)
6580
return nil
6681
},
67-
PersistentPostRun: func(_ *cobra.Command, _ []string) {
68-
if updateNotice == nil {
69-
return
82+
PersistentPostRun: func(cmd *cobra.Command, _ []string) {
83+
if updateCheckWarning != "" {
84+
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "\n%s\n", updateCheckWarning)
85+
}
86+
if updateNotice != nil {
87+
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "\nA new version of flashduty is available: v%s -> %s\n",
88+
update.StripV(updateNotice.CurrentVersion), updateNotice.LatestVersion)
89+
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "To update, run: flashduty update\n")
7090
}
71-
_, _ = fmt.Fprintf(os.Stderr, "\nA new version of flashduty is available: v%s -> %s\n",
72-
update.StripV(updateNotice.CurrentVersion), updateNotice.LatestVersion)
73-
_, _ = fmt.Fprintf(os.Stderr, "To update, run: flashduty update\n")
7491
},
7592
}
7693

internal/cli/root_update_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package cli
2+
3+
import (
4+
"context"
5+
"runtime"
6+
"strings"
7+
"testing"
8+
9+
"github.com/flashcatcloud/flashduty-cli/internal/update"
10+
)
11+
12+
func TestRootAutoUpdateCheckTimeoutWarnsAfterCommand(t *testing.T) {
13+
saveAndResetGlobals(t)
14+
tmp := t.TempDir()
15+
t.Setenv("HOME", tmp)
16+
if runtime.GOOS == "windows" {
17+
t.Setenv("USERPROFILE", tmp)
18+
}
19+
t.Setenv("CI", "")
20+
t.Setenv("GITHUB_ACTIONS", "")
21+
t.Setenv("JENKINS_URL", "")
22+
t.Setenv("GITLAB_CI", "")
23+
t.Setenv("FLASHDUTY_NO_UPDATE_CHECK", "")
24+
25+
origVersion := versionStr
26+
versionStr = "0.6.0"
27+
t.Cleanup(func() { versionStr = origVersion })
28+
29+
origIsTerminal := isTerminalFn
30+
isTerminalFn = func(int) bool { return true }
31+
t.Cleanup(func() { isTerminalFn = origIsTerminal })
32+
33+
called := false
34+
origCheck := checkForUpdateAutoFn
35+
checkForUpdateAutoFn = func(string) (*update.CheckResult, error) {
36+
called = true
37+
return nil, context.DeadlineExceeded
38+
}
39+
t.Cleanup(func() { checkForUpdateAutoFn = origCheck })
40+
41+
out, err := execCommand("version")
42+
if err != nil {
43+
t.Fatalf("version command should still run when auto update check times out: %v", err)
44+
}
45+
if !called {
46+
t.Fatal("auto update check was not called")
47+
}
48+
if !strings.Contains(out, "flashduty version 0.6.0") {
49+
t.Fatalf("version output missing, got:\n%s", out)
50+
}
51+
if !strings.Contains(out, "auto update check timeout, please run 'flashduty update --check' manually") {
52+
t.Fatalf("timeout guidance missing, got:\n%s", out)
53+
}
54+
}

internal/cli/update.go

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -50,18 +50,13 @@ func newUpdateCmd() *cobra.Command {
5050
}
5151

5252
func runInstaller(cmd *cobra.Command) error {
53-
var c *exec.Cmd
54-
if runtime.GOOS == "windows" {
55-
c = exec.Command("powershell", "-Command",
56-
fmt.Sprintf("irm %s | iex", update.InstallPowerShellURL()))
57-
} else {
58-
c = exec.Command("sh", "-c",
59-
fmt.Sprintf("curl -fsSL %s | sh", update.InstallShellURL()))
60-
}
53+
name, args := installerCommandSpec(runtime.GOOS, update.InstallShellURL(), update.InstallPowerShellURL())
54+
c := exec.Command(name, args...)
6155

6256
c.Stdout = cmd.OutOrStdout()
6357
c.Stderr = cmd.ErrOrStderr()
6458
c.Stdin = os.Stdin
59+
c.Env = update.InstallerEnv(os.Environ())
6560

6661
if err := c.Run(); err != nil {
6762
return fmt.Errorf("update failed: %w", err)
@@ -70,3 +65,21 @@ func runInstaller(cmd *cobra.Command) error {
7065
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "\nUpdate complete. Run 'flashduty version' to verify.\n")
7166
return nil
7267
}
68+
69+
func installerCommandSpec(goos, shellURL, powerShellURL string) (string, []string) {
70+
if goos == "windows" {
71+
return "powershell", []string{
72+
"-ExecutionPolicy",
73+
"Bypass",
74+
"-Command",
75+
"$u = $args[0]; irm $u | iex",
76+
powerShellURL,
77+
}
78+
}
79+
return "sh", []string{
80+
"-c",
81+
`curl -fsSL "$1" | sh`,
82+
"flashduty-installer",
83+
shellURL,
84+
}
85+
}

0 commit comments

Comments
 (0)