This commit is contained in:
ryan
2026-06-20 13:27:18 +08:00
parent cdac1f8a45
commit 6975a6c290
25 changed files with 1081 additions and 587 deletions
-23
View File
@@ -1,23 +0,0 @@
---
name: 报告问题
about: 使用简练详细的语言描述你遇到的问题
title: ''
labels: bug
assignees: ''
---
**例行检查**
+ [ ] 我已确认目前没有类似 issue
+ [ ] 我已确认我已升级到最新版本
+ [ ] 我理解并愿意跟进此 issue,协助测试和提供反馈
+ [ ] 我理解并认可上述内容,并理解项目维护者精力有限,不遵循规则的 issue 可能会被无视或直接关闭
**问题描述**
**复现步骤**
**预期结果**
**相关截图**
如果没有的话,请删除此节。
+90
View File
@@ -0,0 +1,90 @@
name: 使用时的错误报告
description: 某些事情不按照预期工作。
title: "bug: "
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
感谢您花时间填写此 Bug 报告!
- **提交错误报告前**:请检查 [已有 Issues](https://github.com/Rain-kl/Wavelet/issues) 列表,了解是否有类似问题被报告。如果不确定,请进行搜索,这有助于我们高效地专注于改进项目。
- type: checkboxes
id: issue-check
attributes:
label: 检查现有问题
description: 确认您在提交新报告之前已经检查了现有报告。
options:
- label: 我已经搜索了现有问题和讨论。
required: true
- label: 我正在使用 wavelet 的最新版本或当前部署实例。
required: true
- type: textarea
id: what-happened
attributes:
label: 发生了什么?
description: 请详细描述您正在进行的操作,您期待看到什么,实际发生了什么。
placeholder: 请告诉我们您看到了什么!
validations:
required: true
- type: textarea
id: steps-to-reproduce
attributes:
label: 如何重现此 Bug?
description: 请提供详细的步骤来重现此 Bug。
placeholder: |
1. 在此环境中...
2. 使用此配置...
3. 运行 '...'
4. 看到错误...
validations:
required: true
- type: dropdown
id: browsers
attributes:
label: 在哪些浏览器中出现问题?
multiple: true
options:
- Firefox
- Chrome
- Safari
- Microsoft Edge
- Other (请在“其他信息”中说明)
validations:
required: false
- type: textarea
id: other-info
attributes:
label: 任何其他信息
description: 您有任何其他关于此报告的信息吗?
validations:
required: false
- type: checkboxes
id: confirmation
attributes:
label: 确认
description: 确保已满足以下先决条件。
options:
- label: 我已阅读并遵循了 `README.md` 中的所有说明。
required: true
- label: 我正在使用 Rain-kl/Wavelet 的最新版本。
required: true
- label: 我已提供我能够提供的尽可能多的相关日志,屏幕截图等。
required: true
- label: |
我已详细记录了精确、按顺序且无歧义的逐步重现说明。我的步骤:
- 从正在执行的操作开始,
- 指定进入了什么页面,
- 列出访问的 URL、用户输入(包括所需的示例值/电子邮件/密码),
- 描述所有已启用或更改的选项和开关,
- 包含任何可能的浏览器控制台日志,
- 识别每个阶段的预期和实际结果,
- 确保任何有合理技能的用户都可以遵循并遇到相同的问题。
required: true
- type: markdown
attributes:
value: |
## 注意
如果 Bug 报告不完整或不遵循说明,则可能不会得到处理。请确保您已遵循所有 **README.md** 指南,并提供所有必要信息以便我们重现该问题。
感谢您为 wavelet 做出贡献!
-18
View File
@@ -1,18 +0,0 @@
---
name: 功能请求
about: 使用简练详细的语言描述希望加入的新功能
title: ''
labels: enhancement
assignees: ''
---
**例行检查**
+ [ ] 我已确认目前没有类似 issue
+ [ ] 我已确认我已升级到最新版本
+ [ ] 我理解并愿意跟进此 issue,协助测试和提供反馈
+ [ ] 我理解并认可上述内容,并理解项目维护者精力有限,不遵循规则的 issue 可能会被无视或直接关闭
**功能描述**
**应用场景**
@@ -0,0 +1,79 @@
name: 新功能建议
description: 请求新的功能或对现有功能进行改进。
title: "feature: "
labels: ["enhancement"]
body:
- type: markdown
attributes:
value: |
感谢您花时间填写此功能请求!
- **提交功能请求前**:请检查 [已有 Issues](https://github.com/Rain-kl/Wavelet/issues) 列表和讨论区,了解是否有类似功能已被讨论或请求。这有助于我们避免重复工作,并高效地专注于改进项目。
- type: checkboxes
id: check-existing
attributes:
label: 检查现有问题和讨论
description: 确认您在提交新请求之前已经检查了现有报告和讨论。
options:
- label: 我已经搜索了现有问题和讨论。
required: true
- label: 我正在使用 wavelet 的最新版本或当前部署实例。
required: true
- type: textarea
id: feature-description
attributes:
label: 你希望添加什么功能或改进什么?
description: 请详细描述您希望添加的功能或进行的改进。
placeholder: 我希望可以...
validations:
required: true
- type: textarea
id: why-needed
attributes:
label: 为什么需要此功能?
description: 请说明此功能解决了什么问题,或提供了什么价值。请提供具体的用例和场景,帮助我们理解其重要性。
placeholder: |
目前我遇到...
如果有了此功能,我可以...
这将为用户带来...
validations:
required: true
- type: textarea
id: proposed-solution
attributes:
label: 建议的解决方案(可选)
description: 如果您对如何实现此功能有任何想法,请在此处描述。这可以包括用户界面草图、API 设想、技术方案等。
placeholder: |
我设想此功能可以通过以下方式实现:
1. ...
2. ...
validations:
required: false
- type: textarea
id: other-info
attributes:
label: 任何其他信息
description: 您有任何其他关于此报告的信息吗?例如,您目前如何解决这个问题,或者其他类似项目的实现方式等。
validations:
required: false
- type: checkboxes
id: confirmation
attributes:
label: 确认
description: 确保已满足以下先决条件。
options:
- label: 我已阅读并遵循了 `README.md` 中的所有说明。
required: true
- label: 我正在使用 Rain-kl/Wavelet 的最新版本。
required: true
- label: 我已提供我能够提供的尽可能多的相关信息,包括用例和场景。
required: true
- label: 我理解功能请求的实现取决于项目优先级和资源。
required: true
- type: markdown
attributes:
value: |
## 注意
如果功能请求不完整或不遵循说明,则可能不会得到处理。请确保您已提供所有必要信息以便我们理解您的建议。
感谢您为 wavelet 做出贡献!
+31
View File
@@ -0,0 +1,31 @@
## 基础规范
- 在任何情况都使用简体中文
- 你是一个专业的代码助手,专门为 wavelet 项目提供代码编写和优化服务
- 严格遵循项目的代码规范和最佳实践,确保代码质量和一致性
- 保持代码简洁、可读、高效
- 优先考虑项目的可维护性、性能和安全性,避免引入不必要的复杂性
- 注释和上一行代码之间保留一行空格
- 编写代码前仔细分析需求,确保改动有实际价值和意义
- 避免仅修改格式、注释或无影响力的拼写错误
- 重构代码时必须带来可维护性或功能上的实质提升
- 新增功能时考虑向后兼容性和 API 稳定性
- 遵循项目的 Apache2.0 许可证要求
- 遵循语义化版本控制规范
- 新增异步任务时使用项目技能 `.agent/new-async-task/SKILL.md`
## 后端规范
- 后端开发使用 Go 语言,所有接口需要符合 Restful 风格
- 数据库使用 PostgreSQL 作为主存储,Redis 作为缓存和会话存储
- Go 代码遵循 gofmt 标准格式,使用 snake_case 命名数据库字段
- 所有 API 接口必须编写完整的 Swagger 文档
- API 响应格式统一为 {"error_msg": "", "data": {}} 结构
- 分页数据返回 {"error_msg": "", "data": {"total": 0, "results": []}} 格式
- 数据库设计禁止使用外键,但必须保留对应字段的索引
## 前端规范
- TypeScript 代码严禁使用 any 类型,优先使用 unknown 进行类型安全处理
- 组件按功能分类:公共组件放在 components/common,UI 组件放在 components/ui
- 自定义图标统一放置在 components/icons 目录,常规图标使用 Lucide 库
+3
View File
@@ -0,0 +1,3 @@
- 如果有其他代码文件,忽略 Swagger 变更和版本号变更,只需要关注其他代码文件的变更
- 需要符合 Github 的提交规范,使用 <type>(<scope>): <subject> 格式
- Commit Message 必须有 Scope 信息
+25
View File
@@ -0,0 +1,25 @@
**例行检查**
<!-- 请在下面的 [ ] 中删除空格并打 x ,表示已完成相关检查 -->
- [ ] 我已阅读并理解 [贡献者公约](https://github.com/Rain-kl/Wavelet/blob/main/CODE_OF_CONDUCT.md)
- [ ] 我已阅读并同意 [贡献者许可协议 (CLA)](https://github.com/Rain-kl/Wavelet/blob/main/CLA.md),确认我的贡献将根据项目的 Apache2.0 许可证进行许可
- [ ] 我知晓如果此 PR 并不做出实质性更改,或可被认为是*为了PR被合并而提交PR*的,则可能不会被合并
**关联信息**
<!--
如此 PR 解决了一个 Issue, 请在下方填写
resolves #<issue_number>,例如:
resolves #1234
-->
<!-- 若以上均没有,请删除此节 -->
**变更内容**
<!-- 请在下方简要描述此 PR 的变更内容 -->
**变更原因**
<!-- 请在下方简要描述此 PR 的变更原因 -->
@@ -1,4 +1,4 @@
name: Docker image build (Server)
name: Build Image (openflare-agent)
on:
workflow_dispatch:
@@ -16,6 +16,10 @@ permissions:
attestations: write
id-token: write
env:
IMAGE_NAME: openflare-agent
DOCKERFILE: docker/Dockerfile.agent
jobs:
build:
name: Build (${{ matrix.arch }})
@@ -46,7 +50,8 @@ jobs:
POINTED_TAG="$(git tag --points-at HEAD --list 'v*' | sort -V | tail -n1)"
INPUT_VERSION="${INPUT_VERSION//[[:space:]]/}"
echo "IMAGE=ghcr.io/${GITHUB_REPOSITORY,,}" >> "$GITHUB_ENV"
OWNER="${GITHUB_REPOSITORY_OWNER,,}"
if [[ "${GITHUB_REF}" == refs/tags/* ]]; then
VERSION="${GITHUB_REF_NAME}"
elif [[ -n "$INPUT_VERSION" ]]; then
@@ -58,6 +63,7 @@ jobs:
exit 1
fi
echo "IMAGE=ghcr.io/${OWNER}/openflare-agent" >> "$GITHUB_ENV"
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
- name: Set up Docker Buildx
@@ -75,27 +81,27 @@ jobs:
uses: docker/build-push-action@v7
with:
context: .
file: ./docker/Dockerfile
file: ${{ env.DOCKERFILE }}
platforms: ${{ matrix.platform }}
outputs: type=image,name=${{ env.IMAGE }},push-by-digest=true,name-canonical=true,push=true
build-args: |
VERSION=${{ env.VERSION }}
cache-from: type=gha,scope=docker-server-${{ matrix.arch }}
cache-to: type=gha,mode=max,ignore-error=true,timeout=20m,scope=docker-server-${{ matrix.arch }}
cache-from: type=gha,scope=docker-${{ env.IMAGE_NAME }}-${{ matrix.arch }}
cache-to: type=gha,mode=max,ignore-error=true,timeout=20m,scope=docker-${{ env.IMAGE_NAME }}-${{ matrix.arch }}
- name: Export digest
shell: bash
run: |
mkdir -p /tmp/server-digests
touch "/tmp/server-digests/${DIGEST#sha256:}"
mkdir -p "/tmp/${{ env.IMAGE_NAME }}-digests"
touch "/tmp/${{ env.IMAGE_NAME }}-digests/${DIGEST#sha256:}"
env:
DIGEST: ${{ steps.build.outputs.digest }}
- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: server-digests-${{ matrix.arch }}
path: /tmp/server-digests/*
name: ${{ env.IMAGE_NAME }}-digests-${{ matrix.arch }}
path: /tmp/${{ env.IMAGE_NAME }}-digests/*
if-no-files-found: error
retention-days: 1
@@ -126,7 +132,8 @@ jobs:
POINTED_TAG="$(git tag --points-at HEAD --list 'v*' | sort -V | tail -n1)"
INPUT_VERSION="${INPUT_VERSION//[[:space:]]/}"
echo "IMAGE=ghcr.io/${GITHUB_REPOSITORY,,}" >> "$GITHUB_ENV"
OWNER="${GITHUB_REPOSITORY_OWNER,,}"
if [[ "${GITHUB_REF}" == refs/tags/* ]]; then
VERSION="${GITHUB_REF_NAME}"
elif [[ -n "$INPUT_VERSION" ]]; then
@@ -138,13 +145,14 @@ jobs:
exit 1
fi
echo "IMAGE=ghcr.io/${OWNER}/openflare-agent" >> "$GITHUB_ENV"
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
- name: Download digests
uses: actions/download-artifact@v4
with:
path: /tmp/server-digests
pattern: server-digests-*
path: /tmp/${{ env.IMAGE_NAME }}-digests
pattern: ${{ env.IMAGE_NAME }}-digests-*
merge-multiple: true
- name: Set up Docker Buildx
@@ -158,7 +166,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Create and push manifest list
working-directory: /tmp/server-digests
working-directory: /tmp/${{ env.IMAGE_NAME }}-digests
shell: bash
run: |
shopt -s nullglob
@@ -168,7 +176,7 @@ jobs:
done
if [ ${#references[@]} -eq 0 ]; then
echo "No digests found in /tmp/server-digests" >&2
echo "No digests found in /tmp/${{ env.IMAGE_NAME }}-digests" >&2
exit 1
fi
@@ -182,6 +190,8 @@ jobs:
-t "${IMAGE}:${VERSION}" \
-t "${IMAGE}:${FLOATING_TAG}" \
"${references[@]}"
env:
IMAGE: ${{ env.IMAGE }}
- name: Inspect image
run: docker buildx imagetools inspect "${IMAGE}:${VERSION}"
run: docker buildx imagetools inspect "${{ env.IMAGE }}:${{ env.VERSION }}"
@@ -1,4 +1,4 @@
name: Docker image build (Agent)
name: Build Image (openflare-relay)
on:
workflow_dispatch:
@@ -16,6 +16,10 @@ permissions:
attestations: write
id-token: write
env:
IMAGE_NAME: openflare-relay
DOCKERFILE: docker/Dockerfile.relay
jobs:
build:
name: Build (${{ matrix.arch }})
@@ -46,7 +50,8 @@ jobs:
POINTED_TAG="$(git tag --points-at HEAD --list 'v*' | sort -V | tail -n1)"
INPUT_VERSION="${INPUT_VERSION//[[:space:]]/}"
echo "IMAGE=ghcr.io/${GITHUB_REPOSITORY,,}-agent" >> "$GITHUB_ENV"
OWNER="${GITHUB_REPOSITORY_OWNER,,}"
if [[ "${GITHUB_REF}" == refs/tags/* ]]; then
VERSION="${GITHUB_REF_NAME}"
elif [[ -n "$INPUT_VERSION" ]]; then
@@ -58,6 +63,7 @@ jobs:
exit 1
fi
echo "IMAGE=ghcr.io/${OWNER}/openflare-relay" >> "$GITHUB_ENV"
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
- name: Set up Docker Buildx
@@ -75,27 +81,27 @@ jobs:
uses: docker/build-push-action@v7
with:
context: .
file: ./openflare-agent/Dockerfile
file: ${{ env.DOCKERFILE }}
platforms: ${{ matrix.platform }}
outputs: type=image,name=${{ env.IMAGE }},push-by-digest=true,name-canonical=true,push=true
build-args: |
VERSION=${{ env.VERSION }}
cache-from: type=gha,scope=docker-agent-${{ matrix.arch }}
cache-to: type=gha,mode=max,ignore-error=true,timeout=20m,scope=docker-agent-${{ matrix.arch }}
cache-from: type=gha,scope=docker-${{ env.IMAGE_NAME }}-${{ matrix.arch }}
cache-to: type=gha,mode=max,ignore-error=true,timeout=20m,scope=docker-${{ env.IMAGE_NAME }}-${{ matrix.arch }}
- name: Export digest
shell: bash
run: |
mkdir -p /tmp/agent-digests
touch "/tmp/agent-digests/${DIGEST#sha256:}"
mkdir -p "/tmp/${{ env.IMAGE_NAME }}-digests"
touch "/tmp/${{ env.IMAGE_NAME }}-digests/${DIGEST#sha256:}"
env:
DIGEST: ${{ steps.build.outputs.digest }}
- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: agent-digests-${{ matrix.arch }}
path: /tmp/agent-digests/*
name: ${{ env.IMAGE_NAME }}-digests-${{ matrix.arch }}
path: /tmp/${{ env.IMAGE_NAME }}-digests/*
if-no-files-found: error
retention-days: 1
@@ -126,7 +132,8 @@ jobs:
POINTED_TAG="$(git tag --points-at HEAD --list 'v*' | sort -V | tail -n1)"
INPUT_VERSION="${INPUT_VERSION//[[:space:]]/}"
echo "IMAGE=ghcr.io/${GITHUB_REPOSITORY,,}-agent" >> "$GITHUB_ENV"
OWNER="${GITHUB_REPOSITORY_OWNER,,}"
if [[ "${GITHUB_REF}" == refs/tags/* ]]; then
VERSION="${GITHUB_REF_NAME}"
elif [[ -n "$INPUT_VERSION" ]]; then
@@ -138,13 +145,14 @@ jobs:
exit 1
fi
echo "IMAGE=ghcr.io/${OWNER}/openflare-relay" >> "$GITHUB_ENV"
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
- name: Download digests
uses: actions/download-artifact@v4
with:
path: /tmp/agent-digests
pattern: agent-digests-*
path: /tmp/${{ env.IMAGE_NAME }}-digests
pattern: ${{ env.IMAGE_NAME }}-digests-*
merge-multiple: true
- name: Set up Docker Buildx
@@ -158,7 +166,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Create and push manifest list
working-directory: /tmp/agent-digests
working-directory: /tmp/${{ env.IMAGE_NAME }}-digests
shell: bash
run: |
shopt -s nullglob
@@ -168,7 +176,7 @@ jobs:
done
if [ ${#references[@]} -eq 0 ]; then
echo "No digests found in /tmp/agent-digests" >&2
echo "No digests found in /tmp/${{ env.IMAGE_NAME }}-digests" >&2
exit 1
fi
@@ -182,6 +190,8 @@ jobs:
-t "${IMAGE}:${VERSION}" \
-t "${IMAGE}:${FLOATING_TAG}" \
"${references[@]}"
env:
IMAGE: ${{ env.IMAGE }}
- name: Inspect image
run: docker buildx imagetools inspect "${IMAGE}:${VERSION}"
run: docker buildx imagetools inspect "${{ env.IMAGE }}:${{ env.VERSION }}"
@@ -1,4 +1,4 @@
name: Docker image build (OpenFlared)
name: Build Image (openflare)
on:
workflow_dispatch:
@@ -16,6 +16,10 @@ permissions:
attestations: write
id-token: write
env:
IMAGE_NAME: openflare
DOCKERFILE: docker/Dockerfile
jobs:
build:
name: Build (${{ matrix.arch }})
@@ -46,7 +50,8 @@ jobs:
POINTED_TAG="$(git tag --points-at HEAD --list 'v*' | sort -V | tail -n1)"
INPUT_VERSION="${INPUT_VERSION//[[:space:]]/}"
echo "IMAGE=ghcr.io/${GITHUB_REPOSITORY_OWNER,,}/openflared" >> "$GITHUB_ENV"
OWNER="${GITHUB_REPOSITORY_OWNER,,}"
if [[ "${GITHUB_REF}" == refs/tags/* ]]; then
VERSION="${GITHUB_REF_NAME}"
elif [[ -n "$INPUT_VERSION" ]]; then
@@ -58,7 +63,9 @@ jobs:
exit 1
fi
echo "IMAGE=ghcr.io/${OWNER}/openflare" >> "$GITHUB_ENV"
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> "$GITHUB_ENV"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
@@ -75,27 +82,28 @@ jobs:
uses: docker/build-push-action@v7
with:
context: .
file: ./openflared/Dockerfile
file: ${{ env.DOCKERFILE }}
platforms: ${{ matrix.platform }}
outputs: type=image,name=${{ env.IMAGE }},push-by-digest=true,name-canonical=true,push=true
build-args: |
VERSION=${{ env.VERSION }}
cache-from: type=gha,scope=docker-flared-${{ matrix.arch }}
cache-to: type=gha,mode=max,ignore-error=true,timeout=20m,scope=docker-flared-${{ matrix.arch }}
BUILD_DATE=${{ env.BUILD_DATE }}
cache-from: type=gha,scope=docker-${{ env.IMAGE_NAME }}-${{ matrix.arch }}
cache-to: type=gha,mode=max,ignore-error=true,timeout=20m,scope=docker-${{ env.IMAGE_NAME }}-${{ matrix.arch }}
- name: Export digest
shell: bash
run: |
mkdir -p /tmp/flared-digests
touch "/tmp/flared-digests/${DIGEST#sha256:}"
mkdir -p "/tmp/${{ env.IMAGE_NAME }}-digests"
touch "/tmp/${{ env.IMAGE_NAME }}-digests/${DIGEST#sha256:}"
env:
DIGEST: ${{ steps.build.outputs.digest }}
- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: flared-digests-${{ matrix.arch }}
path: /tmp/flared-digests/*
name: ${{ env.IMAGE_NAME }}-digests-${{ matrix.arch }}
path: /tmp/${{ env.IMAGE_NAME }}-digests/*
if-no-files-found: error
retention-days: 1
@@ -126,7 +134,8 @@ jobs:
POINTED_TAG="$(git tag --points-at HEAD --list 'v*' | sort -V | tail -n1)"
INPUT_VERSION="${INPUT_VERSION//[[:space:]]/}"
echo "IMAGE=ghcr.io/${GITHUB_REPOSITORY_OWNER,,}/openflared" >> "$GITHUB_ENV"
OWNER="${GITHUB_REPOSITORY_OWNER,,}"
if [[ "${GITHUB_REF}" == refs/tags/* ]]; then
VERSION="${GITHUB_REF_NAME}"
elif [[ -n "$INPUT_VERSION" ]]; then
@@ -138,13 +147,14 @@ jobs:
exit 1
fi
echo "IMAGE=ghcr.io/${OWNER}/openflare" >> "$GITHUB_ENV"
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
- name: Download digests
uses: actions/download-artifact@v4
with:
path: /tmp/flared-digests
pattern: flared-digests-*
path: /tmp/${{ env.IMAGE_NAME }}-digests
pattern: ${{ env.IMAGE_NAME }}-digests-*
merge-multiple: true
- name: Set up Docker Buildx
@@ -158,7 +168,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Create and push manifest list
working-directory: /tmp/flared-digests
working-directory: /tmp/${{ env.IMAGE_NAME }}-digests
shell: bash
run: |
shopt -s nullglob
@@ -168,7 +178,7 @@ jobs:
done
if [ ${#references[@]} -eq 0 ]; then
echo "No digests found in /tmp/flared-digests" >&2
echo "No digests found in /tmp/${{ env.IMAGE_NAME }}-digests" >&2
exit 1
fi
@@ -182,6 +192,18 @@ jobs:
-t "${IMAGE}:${VERSION}" \
-t "${IMAGE}:${FLOATING_TAG}" \
"${references[@]}"
env:
IMAGE: ${{ env.IMAGE }}
- name: Inspect image
run: docker buildx imagetools inspect "${IMAGE}:${VERSION}"
run: docker buildx imagetools inspect "${{ env.IMAGE }}:${{ env.VERSION }}"
- name: Trigger webhook
env:
WEBHOOK_URL: ${{ secrets.WEBHOOK_URL }}
run: |
if [ -n "$WEBHOOK_URL" ]; then
curl -fsSL "$WEBHOOK_URL"
else
echo "Webhook URL is not set, skipping."
fi
@@ -1,4 +1,4 @@
name: Docker image build (Relay)
name: Build Image (openflared)
on:
workflow_dispatch:
@@ -16,6 +16,10 @@ permissions:
attestations: write
id-token: write
env:
IMAGE_NAME: openflared
DOCKERFILE: docker/Dockerfile.flared
jobs:
build:
name: Build (${{ matrix.arch }})
@@ -46,7 +50,8 @@ jobs:
POINTED_TAG="$(git tag --points-at HEAD --list 'v*' | sort -V | tail -n1)"
INPUT_VERSION="${INPUT_VERSION//[[:space:]]/}"
echo "IMAGE=ghcr.io/${GITHUB_REPOSITORY,,}-relay" >> "$GITHUB_ENV"
OWNER="${GITHUB_REPOSITORY_OWNER,,}"
if [[ "${GITHUB_REF}" == refs/tags/* ]]; then
VERSION="${GITHUB_REF_NAME}"
elif [[ -n "$INPUT_VERSION" ]]; then
@@ -58,6 +63,7 @@ jobs:
exit 1
fi
echo "IMAGE=ghcr.io/${OWNER}/openflared" >> "$GITHUB_ENV"
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
- name: Set up Docker Buildx
@@ -75,27 +81,27 @@ jobs:
uses: docker/build-push-action@v7
with:
context: .
file: ./openflare-relay/Dockerfile
file: ${{ env.DOCKERFILE }}
platforms: ${{ matrix.platform }}
outputs: type=image,name=${{ env.IMAGE }},push-by-digest=true,name-canonical=true,push=true
build-args: |
VERSION=${{ env.VERSION }}
cache-from: type=gha,scope=docker-relay-${{ matrix.arch }}
cache-to: type=gha,mode=max,ignore-error=true,timeout=20m,scope=docker-relay-${{ matrix.arch }}
cache-from: type=gha,scope=docker-${{ env.IMAGE_NAME }}-${{ matrix.arch }}
cache-to: type=gha,mode=max,ignore-error=true,timeout=20m,scope=docker-${{ env.IMAGE_NAME }}-${{ matrix.arch }}
- name: Export digest
shell: bash
run: |
mkdir -p /tmp/relay-digests
touch "/tmp/relay-digests/${DIGEST#sha256:}"
mkdir -p "/tmp/${{ env.IMAGE_NAME }}-digests"
touch "/tmp/${{ env.IMAGE_NAME }}-digests/${DIGEST#sha256:}"
env:
DIGEST: ${{ steps.build.outputs.digest }}
- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: relay-digests-${{ matrix.arch }}
path: /tmp/relay-digests/*
name: ${{ env.IMAGE_NAME }}-digests-${{ matrix.arch }}
path: /tmp/${{ env.IMAGE_NAME }}-digests/*
if-no-files-found: error
retention-days: 1
@@ -126,7 +132,8 @@ jobs:
POINTED_TAG="$(git tag --points-at HEAD --list 'v*' | sort -V | tail -n1)"
INPUT_VERSION="${INPUT_VERSION//[[:space:]]/}"
echo "IMAGE=ghcr.io/${GITHUB_REPOSITORY,,}-relay" >> "$GITHUB_ENV"
OWNER="${GITHUB_REPOSITORY_OWNER,,}"
if [[ "${GITHUB_REF}" == refs/tags/* ]]; then
VERSION="${GITHUB_REF_NAME}"
elif [[ -n "$INPUT_VERSION" ]]; then
@@ -138,13 +145,14 @@ jobs:
exit 1
fi
echo "IMAGE=ghcr.io/${OWNER}/openflared" >> "$GITHUB_ENV"
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
- name: Download digests
uses: actions/download-artifact@v4
with:
path: /tmp/relay-digests
pattern: relay-digests-*
path: /tmp/${{ env.IMAGE_NAME }}-digests
pattern: ${{ env.IMAGE_NAME }}-digests-*
merge-multiple: true
- name: Set up Docker Buildx
@@ -158,7 +166,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Create and push manifest list
working-directory: /tmp/relay-digests
working-directory: /tmp/${{ env.IMAGE_NAME }}-digests
shell: bash
run: |
shopt -s nullglob
@@ -168,7 +176,7 @@ jobs:
done
if [ ${#references[@]} -eq 0 ]; then
echo "No digests found in /tmp/relay-digests" >&2
echo "No digests found in /tmp/${{ env.IMAGE_NAME }}-digests" >&2
exit 1
fi
@@ -182,6 +190,8 @@ jobs:
-t "${IMAGE}:${VERSION}" \
-t "${IMAGE}:${FLOATING_TAG}" \
"${references[@]}"
env:
IMAGE: ${{ env.IMAGE }}
- name: Inspect image
run: docker buildx imagetools inspect "${IMAGE}:${VERSION}"
run: docker buildx imagetools inspect "${{ env.IMAGE }}:${{ env.VERSION }}"
+411
View File
@@ -0,0 +1,411 @@
name: Build Release
on:
push:
tags: ["v*"]
workflow_dispatch:
inputs:
version:
description: "Release version/tag to build, for example v1.0.0-beta"
required: true
type: string
env:
APP_NAME: openflare-server
GO_MAIN: ./main.go
GO_BUILD_TAGS: embed_frontend
GO_LDFLAGS: -s -w
NODE_VERSION: "22"
PNPM_VERSION: "10.10.0"
FRONTEND_DIR: frontend
FRONTEND_BUILD_COMMAND: pnpm build:embed
FRONTEND_OUT_DIR: frontend/out
EMBED_DIST_DIR: internal/router/root/dist
EXTRA_FILES: |
LICENSE
README.md
README_zh.md
config.example.yaml
DEPLOYMENT_zh.md
permissions:
contents: write
jobs:
prepare-message:
runs-on: ubuntu-latest
outputs:
commit_msg: ${{ steps.trans.outputs.commit_msg }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.ref }}
fetch-depth: 0
- name: Prepare Commit Message
id: trans
shell: bash
run: |
msg=$(git log -1 --pretty=%B)
pip install deep-translator > /dev/null 2>&1 || true
export COMMIT_MSG="$msg"
echo "commit_msg<<EOF" >> "$GITHUB_OUTPUT"
if [ -f "scripts/translate_commit.py" ]; then
python3 scripts/translate_commit.py >> "$GITHUB_OUTPUT"
else
echo "Translation script not found, using raw message"
echo "$msg" >> "$GITHUB_OUTPUT"
fi
echo "EOF" >> "$GITHUB_OUTPUT"
create-release:
name: Create Release
needs: prepare-message
runs-on: ubuntu-latest
outputs:
version: ${{ steps.metadata.outputs.version }}
version_without_v: ${{ steps.metadata.outputs.version_without_v }}
build_date: ${{ steps.metadata.outputs.build_date }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-tags: true
- name: Set release metadata
id: metadata
shell: bash
env:
INPUT_VERSION: ${{ github.event.inputs.version }}
run: |
set -euo pipefail
input_version="${INPUT_VERSION//[[:space:]]/}"
if [[ "$GITHUB_REF" == refs/tags/* ]]; then
version="$GITHUB_REF_NAME"
elif [[ -n "$input_version" ]]; then
version="$input_version"
else
echo "workflow_dispatch requires a version input" >&2
exit 1
fi
{
echo "version=$version"
echo "version_without_v=${version#v}"
echo "build_date=$(date -u +'%Y-%m-%dT%H:%M:%SZ')"
} >> "$GITHUB_OUTPUT"
- name: Create release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.metadata.outputs.version }}
name: ${{ steps.metadata.outputs.version }}
body: ${{ needs.prepare-message.outputs.commit_msg }}
prerelease: ${{ contains(steps.metadata.outputs.version, 'alpha') || contains(steps.metadata.outputs.version, 'beta') || contains(steps.metadata.outputs.version, 'rc') }}
build-frontend:
name: Build Embedded Frontend
runs-on: ubuntu-latest
needs: create-release
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
run_install: false
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: pnpm
cache-dependency-path: ${{ env.FRONTEND_DIR }}/pnpm-lock.yaml
- name: Install frontend dependencies
working-directory: ${{ env.FRONTEND_DIR }}
run: pnpm install --frozen-lockfile
- name: Build frontend
env:
NEXT_PUBLIC_APP_VERSION: ${{ needs.create-release.outputs.version }}
NEXT_PUBLIC_APP_BUILD_DATE: ${{ needs.create-release.outputs.build_date }}
run: ${{ env.FRONTEND_BUILD_COMMAND }}
working-directory: ${{ env.FRONTEND_DIR }}
- name: Prepare embed directory
shell: bash
run: |
set -euo pipefail
rm -rf "$EMBED_DIST_DIR"
mkdir -p "$(dirname "$EMBED_DIST_DIR")"
cp -R "$FRONTEND_OUT_DIR" "$EMBED_DIST_DIR"
- name: Upload embedded frontend
uses: actions/upload-artifact@v4
with:
name: embedded-frontend
path: ${{ env.EMBED_DIST_DIR }}
if-no-files-found: error
retention-days: 1
build-binaries:
name: Build ${{ matrix.goos }}/${{ matrix.goarch }}
runs-on: ubuntu-latest
needs:
- create-release
- build-frontend
strategy:
fail-fast: false
matrix:
include:
- goos: linux
goarch: amd64
archive: tar.gz
- goos: linux
goarch: arm64
archive: tar.gz
- goos: darwin
goarch: amd64
archive: tar.gz
- goos: darwin
goarch: arm64
archive: tar.gz
- goos: windows
goarch: amd64
archive: zip
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Download embedded frontend
uses: actions/download-artifact@v4
with:
name: embedded-frontend
path: ${{ env.EMBED_DIST_DIR }}
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: true
- name: Build binary
shell: bash
env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
CGO_ENABLED: "0"
VERSION: ${{ needs.create-release.outputs.version }}
BUILD_DATE: ${{ needs.create-release.outputs.build_date }}
run: |
set -euo pipefail
mkdir -p dist
binary_name="$APP_NAME"
if [[ "$GOOS" == "windows" ]]; then
binary_name="${binary_name}.exe"
fi
ldflags="$GO_LDFLAGS -X github.com/Rain-kl/Wavelet/internal/buildinfo.Version=$VERSION -X github.com/Rain-kl/Wavelet/internal/buildinfo.BuildTime=$BUILD_DATE"
build_args=(
-trimpath
-ldflags "$ldflags"
-o "dist/$binary_name"
)
if [[ -n "$GO_BUILD_TAGS" ]]; then
build_args=(-tags "$GO_BUILD_TAGS" "${build_args[@]}")
fi
go build "${build_args[@]}" "$GO_MAIN"
- name: Package artifact
id: package
shell: bash
env:
VERSION: ${{ needs.create-release.outputs.version }}
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
ARCHIVE_FORMAT: ${{ matrix.archive }}
run: |
set -euo pipefail
package_name="${APP_NAME}_${VERSION}_${GOOS}_${GOARCH}"
staging_dir="dist/$package_name"
mkdir -p "$staging_dir"
if [[ "$GOOS" == "windows" ]]; then
cp "dist/${APP_NAME}.exe" "$staging_dir/"
else
cp "dist/${APP_NAME}" "$staging_dir/"
fi
while IFS= read -r extra_file; do
[[ -z "$extra_file" ]] && continue
if [[ -e "$extra_file" ]]; then
cp -R "$extra_file" "$staging_dir/"
fi
done <<< "$EXTRA_FILES"
if [[ "$ARCHIVE_FORMAT" == "zip" ]]; then
(cd dist && zip -r "${package_name}.zip" "$package_name")
artifact="dist/${package_name}.zip"
else
tar -C dist -czf "dist/${package_name}.tar.gz" "$package_name"
artifact="dist/${package_name}.tar.gz"
fi
echo "artifact=$artifact" >> "$GITHUB_OUTPUT"
- name: Upload release artifact
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.create-release.outputs.version }}
files: ${{ steps.package.outputs.artifact }}
build-agent-binaries:
name: Build agent ${{ matrix.goos }}/${{ matrix.goarch }}
runs-on: ubuntu-latest
needs: create-release
strategy:
fail-fast: false
matrix:
include:
- goos: linux
goarch: amd64
asset_name: openflare-agent-linux-amd64
- goos: linux
goarch: arm64
asset_name: openflare-agent-linux-arm64
- goos: darwin
goarch: amd64
asset_name: openflare-agent-darwin-amd64
- goos: darwin
goarch: arm64
asset_name: openflare-agent-darwin-arm64
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Build Agent
env:
CGO_ENABLED: 0
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
ASSET_NAME: ${{ matrix.asset_name }}
VERSION: ${{ needs.create-release.outputs.version }}
run: |
go mod download
mkdir -p dist
go build -trimpath -ldflags "-s -w -X 'github.com/Rain-kl/Wavelet/internal/apps/agent/config.Version=$VERSION'" -o "dist/$ASSET_NAME" ./cmd/agent/main.go
- name: Upload release artifact
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.create-release.outputs.version }}
files: dist/${{ matrix.asset_name }}
build-relay-binaries:
name: Build relay ${{ matrix.goos }}/${{ matrix.goarch }}
runs-on: ubuntu-latest
needs: create-release
strategy:
fail-fast: false
matrix:
include:
- goos: linux
goarch: amd64
asset_name: openflare-relay-linux-amd64
- goos: linux
goarch: arm64
asset_name: openflare-relay-linux-arm64
- goos: darwin
goarch: amd64
asset_name: openflare-relay-darwin-amd64
- goos: darwin
goarch: arm64
asset_name: openflare-relay-darwin-arm64
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Build Relay
env:
CGO_ENABLED: 0
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
ASSET_NAME: ${{ matrix.asset_name }}
VERSION: ${{ needs.create-release.outputs.version }}
run: |
go mod download
mkdir -p dist
go build -trimpath -ldflags "-s -w -X 'github.com/Rain-kl/Wavelet/internal/apps/relay/config.Version=$VERSION'" -o "dist/$ASSET_NAME" ./cmd/relay/main.go
- name: Upload release artifact
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.create-release.outputs.version }}
files: dist/${{ matrix.asset_name }}
build-flared-binaries:
name: Build flared ${{ matrix.goos }}/${{ matrix.goarch }}
runs-on: ubuntu-latest
needs: create-release
strategy:
fail-fast: false
matrix:
include:
- goos: linux
goarch: amd64
asset_name: openflared-linux-amd64
- goos: linux
goarch: arm64
asset_name: openflared-linux-arm64
- goos: darwin
goarch: amd64
asset_name: openflared-darwin-amd64
- goos: darwin
goarch: arm64
asset_name: openflared-darwin-arm64
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Build Flared
env:
CGO_ENABLED: 0
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
ASSET_NAME: ${{ matrix.asset_name }}
VERSION: ${{ needs.create-release.outputs.version }}
run: |
go mod download
mkdir -p dist
go build -trimpath -ldflags "-s -w -X 'github.com/Rain-kl/Wavelet/internal/apps/flared/config.Version=$VERSION'" -o "dist/$ASSET_NAME" ./cmd/flared/main.go
- name: Upload release artifact
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.create-release.outputs.version }}
files: dist/${{ matrix.asset_name }}
@@ -1,4 +1,4 @@
name: Cleanup prerelease tags
name: Cleanup Prerelease
on:
workflow_dispatch:
+24
View File
@@ -0,0 +1,24 @@
name: Close Ticket
on:
schedule:
- cron: "0 0 * * *"
jobs:
close_ticket:
runs-on: ubuntu-24.04
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/stale@v9
with:
days-before-issue-stale: 14
days-before-issue-close: 14
stale-issue-message: "此 issue 长期无活动,将在 14 天后自动关闭。如需继续讨论请回复"
close-issue-message: "此 issue 因长期无活动已自动关闭,如有需要请重新开启"
days-before-pr-stale: 14
days-before-pr-close: 14
stale-pr-message: "此 PR 长期无活动,将在 14 天后自动关闭。如需继续讨论请回复"
close-pr-message: "此 PR 因长期无活动已自动关闭,如有需要请重新开启"
+40
View File
@@ -0,0 +1,40 @@
name: "CodeQL"
on:
pull_request:
branches: [ "*" ]
push:
branches:
- "dev"
- "main"
jobs:
analyze:
name: Analyze (${{ matrix.language }})
runs-on: ubuntu-24.04
permissions:
security-events: write
packages: read
actions: read
contents: read
strategy:
fail-fast: false
matrix:
include:
- language: go
build-mode: autobuild
- language: javascript-typescript
build-mode: none
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{matrix.language}}"
+48
View File
@@ -0,0 +1,48 @@
name: "Copilot Setup Steps"
on:
workflow_dispatch:
push:
paths:
- .github/workflows/copilot-setup-steps.yml
pull_request:
paths:
- .github/workflows/copilot-setup-steps.yml
jobs:
copilot-setup-steps:
runs-on: ubuntu-24.04
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10.10.0
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
cache: "pnpm"
cache-dependency-path: frontend/pnpm-lock.yaml
- name: Install JavaScript dependencies
working-directory: frontend
run: pnpm install
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.25"
check-latest: true
- name: Install dependencies
run: |
go mod download
go install github.com/swaggo/swag/cmd/swag@v1.16.6
+35
View File
@@ -0,0 +1,35 @@
name: ESLint
on:
pull_request:
branches: [ "*" ]
push:
branches:
- "dev"
- "main"
jobs:
lint:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10.10.0
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
cache: 'pnpm'
cache-dependency-path: frontend/pnpm-lock.yaml
- name: Install dependencies
working-directory: frontend
run: pnpm install
- name: Run ESLint
working-directory: frontend
run: npx eslint . --max-warnings 0
-30
View File
@@ -1,30 +0,0 @@
name: Build GitHub Pages
on:
workflow_dispatch:
inputs:
name:
description: 'Reason'
required: false
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout 🛎️
uses: actions/checkout@v2 # If you're using actions/checkout@v2 you must set persist-credentials to false in most cases for the deployment to work correctly.
with:
persist-credentials: false
- name: Install and Build 🔧 # This example project is built using npm and outputs the result to the 'build' folder. Replace with the commands required to build your project, or remove this step entirely if your site is pre-built.
env:
CI: ""
run: |
cd frontend
corepack enable
pnpm install --frozen-lockfile
pnpm build:embed
- name: Deploy 🚀
uses: JamesIves/github-pages-deploy-action@releases/v3
with:
ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }}
BRANCH: gh-pages # The branch the action should deploy to.
FOLDER: frontend/out # The folder the action should deploy.
+32
View File
@@ -0,0 +1,32 @@
name: Check PR Template Checklist
on:
pull_request:
types: [opened, edited, synchronize]
jobs:
check-pr-template:
runs-on: ubuntu-24.04
steps:
- name: check all checklist items are checked
uses: actions/github-script@v7
with:
script: |
// get the pull request body
const prBody = context.payload.pull_request.body || '';
// regex to match all checklist items in the template
// matches lines like: - [ ] ... or - [x] ...
const checklistRegex = /^- \[( |x|X)\] .+$/gm;
const matches = prBody.match(checklistRegex) || [];
// check if any checklist item is not checked
const unchecked = matches.filter(line => line.startsWith('- [ ]'));
// if any unchecked, fail the workflow
if (unchecked.length > 0) {
core.setFailed(`PR checklist 未全部勾选,请确保所有 checklist 项都已勾选。未勾选项如下:\n${unchecked.join('\n')}`);
} else {
console.log('all checklist items are checked.');
}
-384
View File
@@ -1,384 +0,0 @@
name: Release
permissions:
contents: write
on:
workflow_dispatch:
inputs:
version:
description: "Release version/tag to publish, for example v1.0.0-beta"
required: false
type: string
push:
tags: ["v*"]
jobs:
prepare:
runs-on: ubuntu-latest
outputs:
should_run: ${{ steps.version.outputs.should_run }}
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
env:
INPUT_VERSION: ${{ github.event.inputs.version }}
run: |
SHOULD_RUN=true
POINTED_TAG="$(git tag --points-at HEAD --list 'v*' | sort -V | tail -n1)"
INPUT_VERSION="${INPUT_VERSION//[[:space:]]/}"
if [[ "${GITHUB_REF}" == refs/tags/* ]]; then
VERSION="${GITHUB_REF_NAME}"
elif [[ -n "$INPUT_VERSION" ]]; then
VERSION="$INPUT_VERSION"
elif [[ -n "$POINTED_TAG" ]]; then
VERSION="$POINTED_TAG"
elif [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then
echo "workflow_dispatch requires an explicit version input when HEAD is not tagged" >&2
exit 1
elif [[ "${GITHUB_REF}" == refs/heads/main ]]; then
echo "main branch release requires the current commit to be tagged" >&2
exit 1
else
echo "unable to resolve release version from the current ref" >&2
exit 1
fi
echo "should_run=$SHOULD_RUN" >> "$GITHUB_OUTPUT"
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
if: needs.prepare.outputs.should_run == 'true'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10.10.0
run_install: false
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
cache-dependency-path: frontend/pnpm-lock.yaml
- name: Build Frontend
working-directory: frontend
env:
CI: ""
NEXT_PUBLIC_APP_VERSION: ${{ needs.prepare.outputs.version }}
run: |
pnpm install --frozen-lockfile
pnpm build:embed
- name: Prepare embed directory
run: |
rm -rf internal/router/root/dist
cp -R frontend/out internal/router/root/dist
- name: Upload Frontend Artifact
uses: actions/upload-artifact@v4
with:
name: frontend-build
path: internal/router/root/dist
retention-days: 1
build-binaries:
needs:
- prepare
- build-frontend
if: needs.prepare.outputs.should_run == 'true'
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- goos: linux
goarch: amd64
asset_name: openflare-server-linux-amd64
- goos: linux
goarch: arm64
asset_name: openflare-server-linux-arm64
- goos: darwin
goarch: amd64
asset_name: openflare-server-darwin-amd64
- goos: darwin
goarch: arm64
asset_name: openflare-server-darwin-arm64
- goos: windows
goarch: amd64
asset_name: openflare-server-windows-amd64.exe
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Download Frontend Artifact
uses: actions/download-artifact@v4
with:
name: frontend-build
path: internal/router/root/dist
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache-dependency-path: go.sum
- name: Build Server
env:
CGO_ENABLED: 0
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
ASSET_NAME: ${{ matrix.asset_name }}
VERSION: ${{ needs.prepare.outputs.version }}
run: |
go mod download
mkdir -p dist
go build -trimpath -tags embed_frontend \
-ldflags "-s -w -X 'github.com/Rain-kl/Wavelet/internal/buildinfo.Version=$VERSION'" \
-o "dist/$ASSET_NAME" ./main.go
- name: Upload Binary Artifact
uses: actions/upload-artifact@v4
with:
name: server-${{ matrix.goos }}-${{ matrix.goarch }}
path: dist/${{ matrix.asset_name }}
retention-days: 1
build-agent-binaries:
needs: prepare
if: needs.prepare.outputs.should_run == 'true'
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- goos: linux
goarch: amd64
asset_name: openflare-agent-linux-amd64
- goos: linux
goarch: arm64
asset_name: openflare-agent-linux-arm64
- goos: darwin
goarch: amd64
asset_name: openflare-agent-darwin-amd64
- goos: darwin
goarch: arm64
asset_name: openflare-agent-darwin-arm64
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Build Agent
env:
CGO_ENABLED: 0
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
ASSET_NAME: ${{ matrix.asset_name }}
VERSION: ${{ needs.prepare.outputs.version }}
run: |
go mod download
mkdir -p dist
go build -trimpath -ldflags "-s -w -X 'github.com/Rain-kl/Wavelet/internal/apps/agent/config.Version=$VERSION'" -o "dist/$ASSET_NAME" ./cmd/agent/main.go
(cd dist && sha256sum "$ASSET_NAME" > "$ASSET_NAME.sha256")
- name: Upload Agent Artifact
uses: actions/upload-artifact@v4
with:
name: agent-${{ matrix.goos }}-${{ matrix.goarch }}
path: |
dist/${{ matrix.asset_name }}
dist/${{ matrix.asset_name }}.sha256
retention-days: 1
build-relay-binaries:
needs: prepare
if: needs.prepare.outputs.should_run == 'true'
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- goos: linux
goarch: amd64
asset_name: openflare-relay-linux-amd64
- goos: linux
goarch: arm64
asset_name: openflare-relay-linux-arm64
- goos: darwin
goarch: amd64
asset_name: openflare-relay-darwin-amd64
- goos: darwin
goarch: arm64
asset_name: openflare-relay-darwin-arm64
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Build Relay
env:
CGO_ENABLED: 0
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
ASSET_NAME: ${{ matrix.asset_name }}
VERSION: ${{ needs.prepare.outputs.version }}
run: |
go mod download
mkdir -p dist
go build -trimpath -ldflags "-s -w -X 'github.com/Rain-kl/Wavelet/internal/apps/relay/config.Version=$VERSION'" -o "dist/$ASSET_NAME" ./cmd/relay/main.go
(cd dist && sha256sum "$ASSET_NAME" > "$ASSET_NAME.sha256")
- name: Upload Relay Artifact
uses: actions/upload-artifact@v4
with:
name: relay-${{ matrix.goos }}-${{ matrix.goarch }}
path: |
dist/${{ matrix.asset_name }}
dist/${{ matrix.asset_name }}.sha256
retention-days: 1
build-flared-binaries:
needs: prepare
if: needs.prepare.outputs.should_run == 'true'
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- goos: linux
goarch: amd64
asset_name: openflared-linux-amd64
- goos: linux
goarch: arm64
asset_name: openflared-linux-arm64
- goos: darwin
goarch: amd64
asset_name: openflared-darwin-amd64
- goos: darwin
goarch: arm64
asset_name: openflared-darwin-arm64
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Build Flared
env:
CGO_ENABLED: 0
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
ASSET_NAME: ${{ matrix.asset_name }}
VERSION: ${{ needs.prepare.outputs.version }}
run: |
go mod download
mkdir -p dist
go build -trimpath -ldflags "-s -w -X 'github.com/Rain-kl/Wavelet/internal/apps/flared/config.Version=$VERSION'" -o "dist/$ASSET_NAME" ./cmd/flared/main.go
(cd dist && sha256sum "$ASSET_NAME" > "$ASSET_NAME.sha256")
- name: Upload Flared Artifact
uses: actions/upload-artifact@v4
with:
name: flared-${{ matrix.goos }}-${{ matrix.goarch }}
path: |
dist/${{ matrix.asset_name }}
dist/${{ matrix.asset_name }}.sha256
retention-days: 1
release:
needs:
- prepare
- build-binaries
- build-agent-binaries
- build-relay-binaries
- build-flared-binaries
if: needs.prepare.outputs.should_run == 'true'
runs-on: ubuntu-latest
steps:
- name: Download Server Artifacts
uses: actions/download-artifact@v4
with:
pattern: "server-*"
path: dist
merge-multiple: true
- name: Download Agent Artifacts
uses: actions/download-artifact@v4
with:
pattern: "agent-*"
path: dist
merge-multiple: true
- name: Download Relay Artifacts
uses: actions/download-artifact@v4
with:
pattern: "relay-*"
path: dist
merge-multiple: true
- name: Download Flared Artifacts
uses: actions/download-artifact@v4
with:
pattern: "flared-*"
path: dist
merge-multiple: true
- name: Release
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ needs.prepare.outputs.version }}
name: ${{ needs.prepare.outputs.version }}
target_commitish: ${{ github.sha }}
files: dist/*
draft: false
prerelease: ${{ needs.prepare.outputs.is_prerelease == 'true' }}
body: |
查看完整更新日志: https://open-flare.pages.dev/changelog/
generate_release_notes: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-44
View File
@@ -16,50 +16,6 @@ sidebar: false
## [Unreleased]
### 变更
- 优化认证性能:引入 Redis 与本地二级缓存缓存 Token 数据库查询和用户 Active 状态,并在用户注销或用户状态修改/删除时进行缓存失效清理。
- 重构用户与令牌管理:完成 User 控制器和 AccessToken 控制器的 Handler 和数据库操作逻辑的层级解耦(移入 `logics.go`),并统一对数据库异常进行脱敏封装,避免底层数据库错误泄露。
- 优化观测数据缓存:`ObservabilityBufferStore` 引入内存缓存,避免每次心跳时从磁盘重复读取和解析 JSON 缓存文件。
- 优化 ClickHouse 写入去重效率:`dedupSet` 的 `markIfNew` 改为定期清理过期 Key,避免在高并发指标入队时同步遍历整个 Map。
- 优化大文件打包下载:`BatchDownloadFiles` 批量打包 ZIP 下载流中引入 `bufio.Writer` 缓冲写入,避免直接向网络 Socket 进行无缓冲碎片化写入。
- 优化 Pages 静态部署切换性能:在支持的系统与文件系统下,优先通过软链接(Symlink)进行 Pages deployments 目录快速切换,失败时自动退回到全量目录复制(Copy)。
### 修复
- 修复上传模块(`upload`)的 API 错误响应绕过:将 `c.AbortWithStatus` 和自定义 `c.JSON` 错误响应统一替换为标准的 `response.Abort*` 辅助函数,确保响应格式符合全局信封规范。
- 修复通用工具包(`pkg/utils`)中的网络和格式化工具 Bug:优化 `isPrivateIPv4` 使其通过 `net.ParseIP` 解析并调用标准库 `ip.IsPrivate()` 检查;修复 `Bytes2Size` 的边界判定,将大小限制变量(`sizeKB`、`sizeMB`、`sizeGB`)改为只读常量以增强不变性。
- 修复 CAP 模块路由错误响应:将 CAP 接口中的所有直接 JSON 错误响应改造为统一的 `response.Abort*` 抛出并挂载到中间件统一写出 JSON,保证全局 `{ "error_msg": "...", "data": null }` 信封规范。
- 修复 ClickHouse 批量写入(`batchwriter` / `chwriter`)在服务退出时无法安全停机和刷出剩余日志的问题,统一在 Server 优雅停机流程中调用 `bootstrap.Stop()`。
- 修复 OpenFlare 系统参数并发读取的数据竞争(data race)问题,在读取 OpenResty 配置快照、Agent 和 Relay 配置时引入 `OptionMapRWMutex` 读锁保护。
### 移除
### 新增
- 新增 `goose` 数据库平滑升级桥接机制:在全新命名空间中通过数据迁移(而非改表名)迁移合并 legacy 旧版 SQLite/PostgreSQL 生产数据(Schema `202606040004` 及以下),并在完成后清理 `legacy_` 临时表。
- 新增 SQLite 迁移底层 `goose_db_version` 表的主键 `AUTOINCREMENT` 自动补全修复,避免由于旧版 Goose schema 限制导致的新升级写入冲突。
- 新增 `goose.NewProvider` 及 `goose.WithDisableGlobalRegistry` 用于隔离 ClickHouse 与 SQLite/PostgreSQL 间的 Go 代码全局迁移污染。
- 新增 `internal/db/batchwriter` 通用批量写入框架,支持各业务域独立队列实例、按条数/时间 flush、非阻塞入队与优雅停机。
- 业务层接入批量写入:`risk_control` 审计日志迁移至 `batchwriter`;OpenFlare 可观测时序与节点访问日志通过 `internal/apps/openflare/chwriter` 异步 flush,移除写前 `SELECT count()` 去重。
### 变更
### 修复
- 修复 PostgreSQL 下 legacy 桥接迁移 `202606050001` / `202606200006` 使用 `?` 占位符导致 `syntax error at end of input` 的启动失败问题。
- 修复 PostgreSQL legacy 数据迁移时旧表可空字段写入新表 `NOT NULL` 列触发约束错误的问题(`INSERT ... SELECT` 不会自动套用列默认值)。
- 修复 PostgreSQL legacy `dns_accounts` 迁移未转义保留字列名 `authorization` 导致语法错误的问题。
- 修复总览看板「24 小时请求趋势」摘要误展示 24 小时累计值的问题:趋势图摘要改为「当前小时」桶数据,顶部 24h 统计改为按小时趋势聚合。
- 修复访问日志 ClickHouse 聚合查询因 `trim(x) AS x` 别名与表列同名导致总览看板地域分布及 IP 统计失败的问题。
- 修复访问日志页 `count()` 扫描类型不匹配(ClickHouse `UInt64` 写入 `int64`)导致列表计数失败的问题。
- 修复访问日志 Snowflake ID 超出 JS 安全整数范围导致列表 React key 重复告警的问题:API `id` 改为字符串序列化。
### 变更
- 将节点可观测时序表(`of_node_metric_snapshots`、`of_node_request_reports`、`of_node_obs_openresty`、`of_node_obs_frps`、`of_node_obs_frpc`)从 PostgreSQL/SQLite 迁移至 ClickHouse,主库迁移 `202606200005` 删除对应 PG 表。
## [v2.3.4] - 2026-06-17
+1 -1
View File
@@ -229,7 +229,7 @@ Server:
Agent:
* Agent 默认只跟随正式版自动更新。
* Agent 自更新会要求 GitHub Release 同时包含目标二进制和同名 `.sha256` 校验文件,下载后必须通过 SHA-256 校验才会替换本地可执行文件。
* Agent 自更新从 GitHub Release 拉取目标二进制,优先使用 Release API 的 `digest` 字段做 SHA-256 校验;仅当 digest 为空(历史 Release)时才回退读取同名 `.sha256` 侧车文件,校验通过后才替换本地可执行文件。
* 安装脚本可重复执行,用于重装或升级 Agent。
* preview 升级需要手动触发。
+1 -1
View File
@@ -109,7 +109,7 @@ go run . --port 3000 --log-dir ./logs
* `DatabaseAutoCleanupEnabled` 开启后,Server 会在每天凌晨 3 点自动清理 `node_access_logs`、`node_metric_snapshots`、`node_request_reports` 三类观测数据。
* `DatabaseAutoCleanupRetentionDays` 为统一保留天数,必须大于等于 1。
* 管理端支持手动清理时留空保留天数,以直接删除对应数据集的全部历史记录。
* `AgentUpdateRepo` 指向的 GitHub Release 必须为每个 Agent 二进制提供同名 `.sha256` 校验文件,例如 `openflare-agent-linux-amd64.sha256`;Agent 自更新会在替换可执行文件前校验 SHA-256。
* `AgentUpdateRepo` 指向的 GitHub Release 需包含目标 Agent 二进制;自更新优先使用 GitHub Release API 返回的 `assets[].digest`(`sha256:...`)校验,旧版 Release 无 digest 时会回退读取同名 `.sha256` 侧车文件。
* 第三方登录不再通过 `GitHubOAuthEnabled`、`GitHubClientId`、`GitHubClientSecret` 作为主配置入口;这些旧 Option 仅用于升级时迁移默认 GitHub 认证源。
* 微信登录旧 Option 保留为兼容字段,但管理端不再提供微信登录配置入口。
* Turnstile 旧 Option 与后端校验能力保留,已有配置仍会生效。
+52 -21
View File
@@ -70,6 +70,7 @@ type githubRelease struct {
type githubAsset struct {
Name string `json:"name"`
BrowserDownloadURL string `json:"browser_download_url"`
Digest string `json:"digest"`
}
// CheckAndUpdate checks for a newer release on GitHub and performs an update if available.
@@ -99,29 +100,13 @@ func (s *Service) CheckAndUpdate(ctx context.Context, repo string, options Updat
slog.Info(s.logLabel+" update available", "from", localVersion, "to", remoteVersion)
assetName := s.assetNameForGOOSGOARCH(runtime.GOOS, runtime.GOARCH)
checksumAssetName := assetName + ".sha256"
var downloadURL string
var checksumURL string
for _, asset := range release.Assets {
switch asset.Name {
case assetName:
downloadURL = asset.BrowserDownloadURL
case checksumAssetName:
checksumURL = asset.BrowserDownloadURL
}
}
if downloadURL == "" {
s.lastCheckKey = checkKey
return fmt.Errorf("no matching asset %q in release %s", assetName, release.TagName)
}
if checksumURL == "" {
return fmt.Errorf("no matching checksum asset %q in release %s", checksumAssetName, release.TagName)
}
expectedChecksum, err := s.downloadChecksum(ctx, checksumURL, assetName)
downloadURL, expectedChecksum, err := s.resolveReleaseAsset(ctx, release, assetName)
if err != nil {
return fmt.Errorf("download checksum: %w", err)
if downloadURL == "" {
s.lastCheckKey = checkKey
}
return err
}
execPath, err := os.Executable()
@@ -223,6 +208,52 @@ func decodeRelease(reader io.Reader) (*githubRelease, error) {
return &release, nil
}
func (s *Service) resolveReleaseAsset(ctx context.Context, release *githubRelease, assetName string) (downloadURL string, expectedChecksum string, err error) {
checksumAssetName := assetName + ".sha256"
var checksumURL string
for _, asset := range release.Assets {
switch asset.Name {
case assetName:
downloadURL = asset.BrowserDownloadURL
expectedChecksum = normalizeGitHubDigest(asset.Digest)
case checksumAssetName:
checksumURL = asset.BrowserDownloadURL
}
}
if downloadURL == "" {
return "", "", fmt.Errorf("no matching asset %q in release %s", assetName, release.TagName)
}
if expectedChecksum != "" {
return downloadURL, expectedChecksum, nil
}
if checksumURL == "" {
return downloadURL, "", fmt.Errorf("no sha256 digest or checksum asset %q in release %s", checksumAssetName, release.TagName)
}
expectedChecksum, err = s.downloadChecksum(ctx, checksumURL, assetName)
if err != nil {
return downloadURL, "", fmt.Errorf("download checksum: %w", err)
}
return downloadURL, expectedChecksum, nil
}
func normalizeGitHubDigest(digest string) string {
digest = strings.TrimSpace(digest)
if digest == "" {
return ""
}
const prefix = "sha256:"
if strings.HasPrefix(strings.ToLower(digest), prefix) {
digest = digest[len(prefix):]
}
digest = strings.ToLower(digest)
if isSHA256Hex(digest) {
return digest
}
return ""
}
func (s *Service) downloadChecksum(ctx context.Context, url string, assetName string) (string, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
+96 -4
View File
@@ -77,7 +77,7 @@ func TestGetReleaseByTag(t *testing.T) {
}
}
func TestCheckAndUpdateRequiresChecksumAsset(t *testing.T) {
func TestCheckAndUpdateRequiresChecksumSource(t *testing.T) {
assetName := testService(nil).assetNameForGOOSGOARCH(runtime.GOOS, runtime.GOARCH)
service := testService(&http.Client{
Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
@@ -98,8 +98,100 @@ func TestCheckAndUpdateRequiresChecksumAsset(t *testing.T) {
})
err := service.CheckAndUpdate(context.Background(), "Rain-kl/OpenFlare", UpdateOptions{})
if err == nil || !strings.Contains(err.Error(), "no matching checksum asset") {
t.Fatalf("expected missing checksum asset error, got %v", err)
if err == nil || !strings.Contains(err.Error(), "no sha256 digest or checksum asset") {
t.Fatalf("expected missing checksum source error, got %v", err)
}
}
func TestNormalizeGitHubDigest(t *testing.T) {
checksum := strings.Repeat("a", sha256.Size*2)
testCases := []struct {
name string
input string
want string
}{
{name: "prefixed digest", input: "sha256:" + checksum, want: checksum},
{name: "bare hex", input: checksum, want: checksum},
{name: "empty", input: "", want: ""},
{name: "invalid", input: "sha256:not-a-digest", want: ""},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
if got := normalizeGitHubDigest(testCase.input); got != testCase.want {
t.Fatalf("unexpected digest: got %q want %q", got, testCase.want)
}
})
}
}
func TestResolveReleaseAssetPrefersDigest(t *testing.T) {
assetName := testService(nil).assetNameForGOOSGOARCH(runtime.GOOS, runtime.GOARCH)
checksum := strings.Repeat("b", sha256.Size*2)
service := testService(nil)
downloadURL, expectedChecksum, err := service.resolveReleaseAsset(context.Background(), &githubRelease{
TagName: "v1.0.1",
Assets: []githubAsset{
{
Name: assetName,
BrowserDownloadURL: "https://example.test/agent",
Digest: "sha256:" + checksum,
},
{
Name: assetName + ".sha256",
BrowserDownloadURL: "https://example.test/agent.sha256",
},
},
}, assetName)
if err != nil {
t.Fatalf("expected digest resolution to succeed: %v", err)
}
if downloadURL != "https://example.test/agent" {
t.Fatalf("unexpected download url: %s", downloadURL)
}
if expectedChecksum != checksum {
t.Fatalf("unexpected checksum: got %s want %s", expectedChecksum, checksum)
}
}
func TestResolveReleaseAssetFallsBackToChecksumAsset(t *testing.T) {
assetName := testService(nil).assetNameForGOOSGOARCH(runtime.GOOS, runtime.GOARCH)
checksum := strings.Repeat("c", sha256.Size*2)
service := testService(&http.Client{
Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
if req.URL.String() != "https://example.test/agent.sha256" {
t.Fatalf("unexpected request url: %s", req.URL.String())
}
return &http.Response{
StatusCode: http.StatusOK,
Header: make(http.Header),
Body: io.NopCloser(strings.NewReader(checksum + "\n")),
}, nil
}),
})
downloadURL, expectedChecksum, err := service.resolveReleaseAsset(context.Background(), &githubRelease{
TagName: "v1.0.1",
Assets: []githubAsset{
{
Name: assetName,
BrowserDownloadURL: "https://example.test/agent",
},
{
Name: assetName + ".sha256",
BrowserDownloadURL: "https://example.test/agent.sha256",
},
},
}, assetName)
if err != nil {
t.Fatalf("expected checksum fallback to succeed: %v", err)
}
if downloadURL != "https://example.test/agent" {
t.Fatalf("unexpected download url: %s", downloadURL)
}
if expectedChecksum != checksum {
t.Fatalf("unexpected checksum: got %s want %s", expectedChecksum, checksum)
}
}
@@ -229,4 +321,4 @@ func TestIsNewerSupportsPrerelease(t *testing.T) {
}
})
}
}
}