Compare commits
13 Commits
secrets-0.
...
secrets-0.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1d86bc96d | ||
|
|
f87cf3fd20 | ||
|
|
1aef267bbd | ||
|
|
2ad1abe846 | ||
|
|
010001a4f4 | ||
|
|
0f7c151c89 | ||
|
|
a3a92e073f | ||
|
|
3d00b65f55 | ||
|
|
1acc2537b3 | ||
|
|
9a562be4e4 | ||
|
|
3203984fb4 | ||
|
|
52ee858fd7 | ||
|
|
3b338be2d2 |
@@ -24,120 +24,192 @@ env:
|
||||
RUST_BACKTRACE: short
|
||||
|
||||
jobs:
|
||||
# ========== 版本检查(只跑一次,供后续 job 共用)==========
|
||||
version:
|
||||
name: 检查版本
|
||||
name: 版本 & Release
|
||||
runs-on: debian
|
||||
outputs:
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
tag: ${{ steps.version.outputs.tag }}
|
||||
tag_exists: ${{ steps.version.outputs.tag_exists }}
|
||||
version: ${{ steps.ver.outputs.version }}
|
||||
tag: ${{ steps.ver.outputs.tag }}
|
||||
tag_exists: ${{ steps.ver.outputs.tag_exists }}
|
||||
release_id: ${{ steps.release.outputs.release_id }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 检查版本
|
||||
id: version
|
||||
- name: 解析版本
|
||||
id: ver
|
||||
run: |
|
||||
version=$(grep -m1 '^version' Cargo.toml | sed 's/.*"\(.*\)".*/\1/')
|
||||
tag="secrets-${version}"
|
||||
echo "version=${version}" >> $GITHUB_OUTPUT
|
||||
echo "tag=${tag}" >> $GITHUB_OUTPUT
|
||||
previous_tag=$(git tag --list 'secrets-*' --sort=-v:refname | awk -v tag="$tag" '$0 != tag { print; exit }')
|
||||
|
||||
echo "version=${version}" >> "$GITHUB_OUTPUT"
|
||||
echo "tag=${tag}" >> "$GITHUB_OUTPUT"
|
||||
echo "previous_tag=${previous_tag}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
if git rev-parse "refs/tags/${tag}" >/dev/null 2>&1; then
|
||||
echo "tag_exists=true" >> $GITHUB_OUTPUT
|
||||
echo "tag_exists=true" >> "$GITHUB_OUTPUT"
|
||||
echo "版本 ${tag} 已存在"
|
||||
else
|
||||
echo "tag_exists=false" >> $GITHUB_OUTPUT
|
||||
echo "tag_exists=false" >> "$GITHUB_OUTPUT"
|
||||
echo "将创建新版本 ${tag}"
|
||||
fi
|
||||
|
||||
- name: 创建 Tag
|
||||
if: steps.version.outputs.tag_exists == 'false'
|
||||
if: steps.ver.outputs.tag_exists == 'false'
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git tag -a "${{ steps.version.outputs.tag }}" -m "Release ${{ steps.version.outputs.tag }}"
|
||||
git push origin "${{ steps.version.outputs.tag }}"
|
||||
git tag -a "${{ steps.ver.outputs.tag }}" -m "Release ${{ steps.ver.outputs.tag }}"
|
||||
git push origin "${{ steps.ver.outputs.tag }}"
|
||||
|
||||
# ========== 矩阵构建 ==========
|
||||
build:
|
||||
name: Build (${{ matrix.target }})
|
||||
needs: version
|
||||
continue-on-error: true # 某平台失败/超时不阻断其他平台和 notify
|
||||
timeout-minutes: 30 # runner 不在线时 30 分钟后放弃
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: debian
|
||||
target: x86_64-unknown-linux-musl
|
||||
archive_suffix: x86_64-linux-musl
|
||||
os: linux
|
||||
- runner: darwin-arm64
|
||||
target: aarch64-apple-darwin
|
||||
archive_suffix: aarch64-macos
|
||||
os: macos
|
||||
- runner: windows
|
||||
target: x86_64-pc-windows-msvc
|
||||
archive_suffix: x86_64-windows
|
||||
os: windows
|
||||
- name: 解析或创建 Release
|
||||
id: release
|
||||
env:
|
||||
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
run: |
|
||||
if [ -z "$RELEASE_TOKEN" ]; then
|
||||
echo "release_id=" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
runs-on: ${{ matrix.runner }}
|
||||
command -v jq >/dev/null 2>&1 || (sudo apt-get update -qq && sudo apt-get install -y -qq jq)
|
||||
|
||||
tag="${{ steps.ver.outputs.tag }}"
|
||||
version="${{ steps.ver.outputs.version }}"
|
||||
release_api="${{ github.server_url }}/api/v1/repos/${{ github.repository }}/releases"
|
||||
|
||||
http_code=$(curl -sS -o /tmp/release.json -w '%{http_code}' \
|
||||
-H "Authorization: token $RELEASE_TOKEN" \
|
||||
"${release_api}/tags/${tag}")
|
||||
|
||||
if [ "$http_code" = "200" ]; then
|
||||
release_id=$(jq -r '.id // empty' /tmp/release.json)
|
||||
if [ -n "$release_id" ]; then
|
||||
echo "已找到现有 Release: ${release_id}"
|
||||
echo "release_id=${release_id}" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
previous_tag="${{ steps.ver.outputs.previous_tag }}"
|
||||
if [ -n "$previous_tag" ]; then
|
||||
changes=$(git log --pretty=format:'- %s (%h)' "${previous_tag}..HEAD")
|
||||
else
|
||||
changes=$(git log --pretty=format:'- %s (%h)')
|
||||
fi
|
||||
[ -z "$changes" ] && changes="- 首次发布"
|
||||
|
||||
body=$(printf '## 变更日志\n\n%s' "$changes")
|
||||
|
||||
payload=$(jq -n \
|
||||
--arg tag "$tag" \
|
||||
--arg name "${{ env.BINARY_NAME }} ${version}" \
|
||||
--arg body "$body" \
|
||||
'{tag_name: $tag, name: $name, body: $body, draft: true}')
|
||||
|
||||
http_code=$(curl -sS -o /tmp/create-release.json -w '%{http_code}' \
|
||||
-H "Authorization: token $RELEASE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-X POST "$release_api" \
|
||||
-d "$payload")
|
||||
|
||||
if [ "$http_code" = "201" ] || [ "$http_code" = "200" ]; then
|
||||
release_id=$(jq -r '.id // empty' /tmp/create-release.json)
|
||||
fi
|
||||
|
||||
if [ -n "$release_id" ]; then
|
||||
echo "已创建草稿 Release: ${release_id}"
|
||||
echo "release_id=${release_id}" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "⚠ 创建 Release 失败 (HTTP ${http_code}),跳过产物上传"
|
||||
cat /tmp/create-release.json 2>/dev/null || true
|
||||
echo "release_id=" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
probe-runners:
|
||||
name: 探测 Runner
|
||||
runs-on: debian
|
||||
outputs:
|
||||
has_linux: ${{ steps.probe.outputs.has_linux }}
|
||||
has_macos: ${{ steps.probe.outputs.has_macos }}
|
||||
has_windows: ${{ steps.probe.outputs.has_windows }}
|
||||
steps:
|
||||
# ========== 环境准备 ==========
|
||||
- name: 安装依赖 (Linux)
|
||||
if: matrix.os == 'linux'
|
||||
- name: 查询可用 Runner
|
||||
id: probe
|
||||
env:
|
||||
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y jq curl git pkg-config musl-tools binutils
|
||||
|
||||
if ! command -v cargo &>/dev/null; then
|
||||
echo "安装 Rust 工具链..."
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable
|
||||
source "$HOME/.cargo/env"
|
||||
rustup component add rustfmt clippy
|
||||
fi
|
||||
|
||||
rustup target add ${{ matrix.target }}
|
||||
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: 安装依赖 (macOS)
|
||||
if: matrix.os == 'macos'
|
||||
run: |
|
||||
brew install jq
|
||||
|
||||
if ! command -v cargo &>/dev/null; then
|
||||
echo "安装 Rust 工具链..."
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable
|
||||
source "$HOME/.cargo/env"
|
||||
rustup component add rustfmt clippy
|
||||
fi
|
||||
|
||||
rustup target add ${{ matrix.target }}
|
||||
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: 安装依赖 (Windows)
|
||||
if: matrix.os == 'windows'
|
||||
shell: pwsh
|
||||
run: |
|
||||
# 检查 Rust 是否已安装
|
||||
if (-not (Get-Command cargo -ErrorAction SilentlyContinue)) {
|
||||
Write-Host "安装 Rust 工具链..."
|
||||
Invoke-WebRequest -Uri "https://win.rustup.rs/x86_64" -OutFile rustup-init.exe
|
||||
.\rustup-init.exe -y --default-toolchain stable
|
||||
Remove-Item rustup-init.exe
|
||||
fallback_all() {
|
||||
echo "has_linux=true" >> "$GITHUB_OUTPUT"
|
||||
echo "has_macos=true" >> "$GITHUB_OUTPUT"
|
||||
echo "has_windows=true" >> "$GITHUB_OUTPUT"
|
||||
}
|
||||
|
||||
if [ -z "$RELEASE_TOKEN" ]; then
|
||||
echo "未配置 RELEASE_TOKEN,默认尝试所有平台"
|
||||
fallback_all; exit 0
|
||||
fi
|
||||
|
||||
command -v jq >/dev/null 2>&1 || (sudo apt-get update -qq && sudo apt-get install -y -qq jq)
|
||||
|
||||
http_code=$(curl -sS -o /tmp/runners.json -w '%{http_code}' \
|
||||
-H "Authorization: token $RELEASE_TOKEN" \
|
||||
"${{ github.server_url }}/api/v1/repos/${{ github.repository }}/actions/runners")
|
||||
|
||||
if [ "$http_code" != "200" ]; then
|
||||
echo "Runner API 返回 HTTP ${http_code},默认尝试所有平台"
|
||||
fallback_all; exit 0
|
||||
fi
|
||||
|
||||
has_runner() {
|
||||
local label="$1"
|
||||
jq -e --arg label "$label" '
|
||||
(.runners // .data // . // [])
|
||||
| any(
|
||||
(
|
||||
(.online == true)
|
||||
or (
|
||||
((.status // "") | ascii_downcase)
|
||||
| IN("online", "idle", "busy", "active")
|
||||
)
|
||||
)
|
||||
and (
|
||||
(.labels // [])
|
||||
| map(if type == "object" then (.name // .label // "") else tostring end | ascii_downcase)
|
||||
| index($label)
|
||||
) != null
|
||||
)
|
||||
' /tmp/runners.json >/dev/null
|
||||
}
|
||||
|
||||
for pair in "debian:has_linux" "darwin-arm64:has_macos" "windows:has_windows"; do
|
||||
label="$(printf '%s' "${pair%%:*}" | tr '[:upper:]' '[:lower:]')"; key="${pair##*:}"
|
||||
if has_runner "$label"; then
|
||||
echo "${key}=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "${key}=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
done
|
||||
|
||||
check:
|
||||
name: 质量检查 (fmt / clippy / test)
|
||||
runs-on: debian
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: 安装 Rust
|
||||
run: |
|
||||
if ! command -v cargo >/dev/null 2>&1; then
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable
|
||||
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||
fi
|
||||
source "$HOME/.cargo/env" 2>/dev/null || true
|
||||
rustup component add rustfmt clippy
|
||||
rustup target add ${{ matrix.target }}
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# ========== Cargo 缓存 ==========
|
||||
- name: 缓存 Cargo 依赖
|
||||
- name: 缓存 Cargo
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
@@ -145,169 +217,265 @@ jobs:
|
||||
~/.cargo/registry/cache
|
||||
~/.cargo/git/db
|
||||
target
|
||||
key: cargo-secrets-${{ matrix.target }}-${{ hashFiles('Cargo.lock') }}
|
||||
key: cargo-check-${{ hashFiles('Cargo.lock') }}
|
||||
restore-keys: |
|
||||
cargo-secrets-${{ matrix.target }}-
|
||||
cargo-check-
|
||||
|
||||
- name: 检查代码格式
|
||||
if: matrix.os != 'windows'
|
||||
run: cargo fmt -- --check
|
||||
- run: cargo fmt -- --check
|
||||
- run: cargo clippy --locked -- -D warnings
|
||||
- run: cargo test --locked
|
||||
|
||||
- name: 检查代码格式 (Windows)
|
||||
if: matrix.os == 'windows'
|
||||
build-linux:
|
||||
name: Build (x86_64-unknown-linux-musl)
|
||||
needs: [version, probe-runners, check]
|
||||
if: needs.probe-runners.outputs.has_linux == 'true'
|
||||
runs-on: debian
|
||||
timeout-minutes: 1
|
||||
steps:
|
||||
- name: 安装依赖
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y pkg-config musl-tools binutils curl
|
||||
if ! command -v cargo >/dev/null 2>&1; then
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable
|
||||
fi
|
||||
source "$HOME/.cargo/env" 2>/dev/null || true
|
||||
rustup target add x86_64-unknown-linux-musl
|
||||
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: 缓存 Cargo
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry/index
|
||||
~/.cargo/registry/cache
|
||||
~/.cargo/git/db
|
||||
target
|
||||
key: cargo-x86_64-unknown-linux-musl-${{ hashFiles('Cargo.lock') }}
|
||||
restore-keys: |
|
||||
cargo-x86_64-unknown-linux-musl-
|
||||
|
||||
- run: cargo build --release --locked --target x86_64-unknown-linux-musl
|
||||
- run: strip target/x86_64-unknown-linux-musl/release/${{ env.BINARY_NAME }}
|
||||
|
||||
- name: 上传 Release 产物
|
||||
if: needs.version.outputs.release_id != ''
|
||||
env:
|
||||
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
run: |
|
||||
[ -z "$RELEASE_TOKEN" ] && exit 0
|
||||
tag="${{ needs.version.outputs.tag }}"
|
||||
bin="target/x86_64-unknown-linux-musl/release/${{ env.BINARY_NAME }}"
|
||||
archive="${{ env.BINARY_NAME }}-${tag}-x86_64-linux-musl.tar.gz"
|
||||
tar -czf "$archive" -C "$(dirname "$bin")" "$(basename "$bin")"
|
||||
curl -fsS -H "Authorization: token $RELEASE_TOKEN" \
|
||||
-F "attachment=@${archive}" \
|
||||
"${{ github.server_url }}/api/v1/repos/${{ github.repository }}/releases/${{ needs.version.outputs.release_id }}/assets"
|
||||
|
||||
build-macos:
|
||||
name: Build (aarch64-apple-darwin)
|
||||
needs: [version, probe-runners, check]
|
||||
if: needs.probe-runners.outputs.has_macos == 'true'
|
||||
runs-on: darwin-arm64
|
||||
timeout-minutes: 1
|
||||
steps:
|
||||
- name: 安装依赖
|
||||
run: |
|
||||
if ! command -v cargo >/dev/null 2>&1; then
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable
|
||||
fi
|
||||
source "$HOME/.cargo/env" 2>/dev/null || true
|
||||
rustup target add aarch64-apple-darwin
|
||||
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: 缓存 Cargo
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry/index
|
||||
~/.cargo/registry/cache
|
||||
~/.cargo/git/db
|
||||
target
|
||||
key: cargo-aarch64-apple-darwin-${{ hashFiles('Cargo.lock') }}
|
||||
restore-keys: |
|
||||
cargo-aarch64-apple-darwin-
|
||||
|
||||
- run: cargo build --release --locked --target aarch64-apple-darwin
|
||||
- run: strip -x target/aarch64-apple-darwin/release/${{ env.BINARY_NAME }}
|
||||
|
||||
- name: 上传 Release 产物
|
||||
if: needs.version.outputs.release_id != ''
|
||||
env:
|
||||
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
run: |
|
||||
[ -z "$RELEASE_TOKEN" ] && exit 0
|
||||
tag="${{ needs.version.outputs.tag }}"
|
||||
bin="target/aarch64-apple-darwin/release/${{ env.BINARY_NAME }}"
|
||||
archive="${{ env.BINARY_NAME }}-${tag}-aarch64-macos.tar.gz"
|
||||
tar -czf "$archive" -C "$(dirname "$bin")" "$(basename "$bin")"
|
||||
curl -fsS -H "Authorization: token $RELEASE_TOKEN" \
|
||||
-F "attachment=@${archive}" \
|
||||
"${{ github.server_url }}/api/v1/repos/${{ github.repository }}/releases/${{ needs.version.outputs.release_id }}/assets"
|
||||
|
||||
build-windows:
|
||||
name: Build (x86_64-pc-windows-msvc)
|
||||
needs: [version, probe-runners, check]
|
||||
if: needs.probe-runners.outputs.has_windows == 'true'
|
||||
runs-on: windows
|
||||
timeout-minutes: 1
|
||||
steps:
|
||||
- name: 安装依赖
|
||||
shell: pwsh
|
||||
run: cargo fmt -- --check
|
||||
run: |
|
||||
if (-not (Get-Command cargo -ErrorAction SilentlyContinue)) {
|
||||
Invoke-WebRequest -Uri "https://win.rustup.rs/x86_64" -OutFile rustup-init.exe
|
||||
.\rustup-init.exe -y --default-toolchain stable
|
||||
Remove-Item rustup-init.exe
|
||||
}
|
||||
rustup target add x86_64-pc-windows-msvc
|
||||
|
||||
- name: 运行 Clippy 检查
|
||||
if: matrix.os != 'windows'
|
||||
run: cargo clippy --release --target ${{ matrix.target }} -- -D warnings
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: 运行 Clippy 检查 (Windows)
|
||||
if: matrix.os == 'windows'
|
||||
shell: pwsh
|
||||
run: cargo clippy --release --target ${{ matrix.target }} -- -D warnings
|
||||
- name: 缓存 Cargo
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry/index
|
||||
~/.cargo/registry/cache
|
||||
~/.cargo/git/db
|
||||
target
|
||||
key: cargo-x86_64-pc-windows-msvc-${{ hashFiles('Cargo.lock') }}
|
||||
restore-keys: |
|
||||
cargo-x86_64-pc-windows-msvc-
|
||||
|
||||
- name: 构建
|
||||
if: matrix.os != 'windows'
|
||||
env:
|
||||
GIT_TAG: ${{ needs.version.outputs.version }}
|
||||
run: cargo build --release --target ${{ matrix.target }} --verbose
|
||||
|
||||
- name: 构建 (Windows)
|
||||
if: matrix.os == 'windows'
|
||||
shell: pwsh
|
||||
env:
|
||||
GIT_TAG: ${{ needs.version.outputs.version }}
|
||||
run: cargo build --release --target ${{ matrix.target }} --verbose
|
||||
run: cargo build --release --locked --target x86_64-pc-windows-msvc
|
||||
|
||||
- name: Strip 二进制 (Linux)
|
||||
if: matrix.os == 'linux'
|
||||
run: strip target/${{ matrix.target }}/release/${{ env.BINARY_NAME }}
|
||||
|
||||
- name: Strip 二进制 (macOS)
|
||||
if: matrix.os == 'macos'
|
||||
run: strip -x target/${{ matrix.target }}/release/${{ env.BINARY_NAME }}
|
||||
|
||||
# ========== 上传 Release 产物 ==========
|
||||
- name: 上传 Release 产物 (Linux/macOS)
|
||||
if: needs.version.outputs.tag_exists == 'false' && matrix.os != 'windows'
|
||||
- name: 上传 Release 产物
|
||||
if: needs.version.outputs.release_id != ''
|
||||
shell: pwsh
|
||||
env:
|
||||
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
run: |
|
||||
[ -z "$RELEASE_TOKEN" ] && echo "跳过:未配置 RELEASE_TOKEN" && exit 0
|
||||
if (-not $env:RELEASE_TOKEN) { exit 0 }
|
||||
$tag = "${{ needs.version.outputs.tag }}"
|
||||
$bin = "target\x86_64-pc-windows-msvc\release\${{ env.BINARY_NAME }}.exe"
|
||||
$archive = "${{ env.BINARY_NAME }}-${tag}-x86_64-windows.zip"
|
||||
Compress-Archive -Path $bin -DestinationPath $archive -Force
|
||||
$url = "${{ github.server_url }}/api/v1/repos/${{ github.repository }}/releases/${{ needs.version.outputs.release_id }}/assets"
|
||||
Invoke-RestMethod -Uri $url -Method Post `
|
||||
-Headers @{ "Authorization" = "token $env:RELEASE_TOKEN" } `
|
||||
-Form @{ attachment = Get-Item $archive }
|
||||
|
||||
tag="${{ needs.version.outputs.tag }}"
|
||||
binary="target/${{ matrix.target }}/release/${{ env.BINARY_NAME }}"
|
||||
archive="${{ env.BINARY_NAME }}-${tag}-${{ matrix.archive_suffix }}.tar.gz"
|
||||
publish-release:
|
||||
name: 发布草稿 Release
|
||||
needs: [version, check, build-linux, build-macos, build-windows]
|
||||
if: always() && needs.version.outputs.release_id != ''
|
||||
runs-on: debian
|
||||
timeout-minutes: 2
|
||||
steps:
|
||||
- name: 发布草稿
|
||||
env:
|
||||
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
run: |
|
||||
[ -z "$RELEASE_TOKEN" ] && exit 0
|
||||
|
||||
tar -czf "$archive" -C "$(dirname $binary)" "$(basename $binary)"
|
||||
version_r="${{ needs.version.result }}"
|
||||
check_r="${{ needs.check.result }}"
|
||||
linux_r="${{ needs.build-linux.result }}"
|
||||
macos_r="${{ needs.build-macos.result }}"
|
||||
windows_r="${{ needs.build-windows.result }}"
|
||||
|
||||
# 查找已有 Release(由首个完成的 job 创建,后续 job 直接上传)
|
||||
release_url="${{ github.server_url }}/api/v1/repos/${{ github.repository }}/releases"
|
||||
release_id=$(curl -sS -H "Authorization: token $RELEASE_TOKEN" \
|
||||
"${release_url}/tags/${tag}" | jq -r '.id // empty')
|
||||
for result in "$version_r" "$check_r" "$linux_r" "$macos_r" "$windows_r"; do
|
||||
case "$result" in
|
||||
success|skipped) ;;
|
||||
*)
|
||||
echo "存在失败或取消的 job,保留草稿 Release"
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ -z "$release_id" ]; then
|
||||
release_id=$(curl -sS -H "Authorization: token $RELEASE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-X POST "$release_url" \
|
||||
-d "{\"tag_name\": \"${tag}\", \"name\": \"${tag}\", \"body\": \"Release ${tag}\"}" \
|
||||
| jq -r '.id')
|
||||
release_api="${{ github.server_url }}/api/v1/repos/${{ github.repository }}/releases/${{ needs.version.outputs.release_id }}"
|
||||
http_code=$(curl -sS -o /tmp/publish-release.json -w '%{http_code}' \
|
||||
-H "Authorization: token $RELEASE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-X PATCH "$release_api" \
|
||||
-d '{"draft":false}')
|
||||
|
||||
if [ "$http_code" != "200" ]; then
|
||||
echo "发布草稿 Release 失败 (HTTP ${http_code})"
|
||||
cat /tmp/publish-release.json 2>/dev/null || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
upload_url="${{ github.server_url }}/api/v1/repos/${{ github.repository }}/releases/${release_id}/assets"
|
||||
curl -sS -H "Authorization: token $RELEASE_TOKEN" \
|
||||
-F "attachment=@${archive}" \
|
||||
"$upload_url"
|
||||
|
||||
echo "已上传: ${archive} → Release ${tag}"
|
||||
|
||||
- name: 上传 Release 产物 (Windows)
|
||||
if: needs.version.outputs.tag_exists == 'false' && matrix.os == 'windows'
|
||||
shell: pwsh
|
||||
env:
|
||||
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
run: |
|
||||
if (-not $env:RELEASE_TOKEN) { Write-Host "跳过:未配置 RELEASE_TOKEN"; exit 0 }
|
||||
|
||||
$tag = "${{ needs.version.outputs.tag }}"
|
||||
$binary = "target\${{ matrix.target }}\release\${{ env.BINARY_NAME }}.exe"
|
||||
$archive = "${{ env.BINARY_NAME }}-${tag}-${{ matrix.archive_suffix }}.zip"
|
||||
|
||||
Compress-Archive -Path $binary -DestinationPath $archive
|
||||
|
||||
$headers = @{ "Authorization" = "token $env:RELEASE_TOKEN"; "Content-Type" = "application/json" }
|
||||
$release_url = "${{ github.server_url }}/api/v1/repos/${{ github.repository }}/releases"
|
||||
|
||||
# 查找已有 Release
|
||||
$existing = Invoke-RestMethod -Uri "${release_url}/tags/${tag}" -Headers $headers -ErrorAction SilentlyContinue
|
||||
if ($existing.id) {
|
||||
$release_id = $existing.id
|
||||
} else {
|
||||
$body = @{ tag_name = $tag; name = $tag; body = "Release ${tag}" } | ConvertTo-Json
|
||||
$release_id = (Invoke-RestMethod -Uri $release_url -Method Post -Headers $headers -Body $body).id
|
||||
}
|
||||
|
||||
$upload_url = "${release_url}/${release_id}/assets"
|
||||
$upload_headers = @{ "Authorization" = "token $env:RELEASE_TOKEN" }
|
||||
$form = @{ attachment = Get-Item $archive }
|
||||
Invoke-RestMethod -Uri $upload_url -Method Post -Headers $upload_headers -Form $form
|
||||
|
||||
Write-Host "已上传: ${archive} → Release ${tag}"
|
||||
|
||||
# ========== 汇总通知 ==========
|
||||
notify:
|
||||
name: 发送通知
|
||||
needs: [version, build]
|
||||
name: 通知
|
||||
needs: [version, probe-runners, check, build-linux, build-macos, build-windows, publish-release]
|
||||
if: always() && github.event_name == 'push'
|
||||
runs-on: debian
|
||||
timeout-minutes: 1
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: 发送通知
|
||||
continue-on-error: true
|
||||
- name: 发送飞书通知
|
||||
env:
|
||||
WEBHOOK_URL: ${{ vars.WEBHOOK_URL }}
|
||||
run: |
|
||||
[ -z "$WEBHOOK_URL" ] && exit 0
|
||||
command -v jq >/dev/null 2>&1 || (sudo apt-get update -qq && sudo apt-get install -y -qq jq)
|
||||
|
||||
tag="${{ needs.version.outputs.tag }}"
|
||||
tag_exists="${{ needs.version.outputs.tag_exists }}"
|
||||
build_result="${{ needs.build.result }}"
|
||||
commit=$(git log -1 --pretty=format:"%s" 2>/dev/null || echo "N/A")
|
||||
url="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_number }}"
|
||||
|
||||
if [ "$build_result" = "success" ]; then
|
||||
status_text="构建成功 ✅"
|
||||
version_r="${{ needs.version.result }}"
|
||||
check_r="${{ needs.check.result }}"
|
||||
linux_r="${{ needs.build-linux.result }}"
|
||||
macos_r="${{ needs.build-macos.result }}"
|
||||
windows_r="${{ needs.build-windows.result }}"
|
||||
publish_r="${{ needs.publish-release.result }}"
|
||||
|
||||
if [ "$version_r" = "success" ] && [ "$check_r" = "success" ] \
|
||||
&& [ "$linux_r" != "failure" ] && [ "$linux_r" != "cancelled" ] \
|
||||
&& [ "$macos_r" != "failure" ] && [ "$macos_r" != "cancelled" ] \
|
||||
&& [ "$windows_r" != "failure" ] && [ "$windows_r" != "cancelled" ] \
|
||||
&& [ "$publish_r" != "failure" ] && [ "$publish_r" != "cancelled" ]; then
|
||||
status="构建成功 ✅"
|
||||
else
|
||||
status_text="构建失败 ❌"
|
||||
status="构建失败 ❌"
|
||||
fi
|
||||
|
||||
commit_title=$(git log -1 --pretty=format:"%s" 2>/dev/null || echo "N/A")
|
||||
workflow_url="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_number }}"
|
||||
icon() {
|
||||
case "$1" in
|
||||
success) echo "✅" ;;
|
||||
skipped) echo "⏭" ;;
|
||||
*) echo "❌" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
if [ "$build_result" != "success" ]; then
|
||||
payload=$(jq -n \
|
||||
--arg title "${{ env.BINARY_NAME }} ${status_text}" \
|
||||
--arg commit "$commit_title" \
|
||||
--arg version "$tag" \
|
||||
--arg author "${{ github.actor }}" \
|
||||
--arg url "$workflow_url" \
|
||||
'{msg_type: "text", content: {text: "\($title)\n提交:\($commit)\n版本:\($version)\n作者:\($author)\n详情:\($url)"}}')
|
||||
elif [ "$tag_exists" = "false" ]; then
|
||||
payload=$(jq -n \
|
||||
--arg title "${{ env.BINARY_NAME }} ${status_text}" \
|
||||
--arg commit "$commit_title" \
|
||||
--arg version "$tag" \
|
||||
--arg author "${{ github.actor }}" \
|
||||
--arg url "$workflow_url" \
|
||||
'{msg_type: "text", content: {text: "\($title)\n🆕 新版本已发布 (linux / macOS / windows)\n提交:\($commit)\n版本:\($version)\n作者:\($author)\n详情:\($url)"}}')
|
||||
msg="${{ env.BINARY_NAME }} ${status}"
|
||||
if [ "$tag_exists" = "false" ]; then
|
||||
msg="${msg}
|
||||
🆕 新版本 ${tag}"
|
||||
else
|
||||
payload=$(jq -n \
|
||||
--arg title "${{ env.BINARY_NAME }} ${status_text}" \
|
||||
--arg commit "$commit_title" \
|
||||
--arg version "$tag" \
|
||||
--arg author "${{ github.actor }}" \
|
||||
--arg url "$workflow_url" \
|
||||
'{msg_type: "text", content: {text: "\($title)\n🔄 重复构建\n提交:\($commit)\n版本:\($version)\n作者:\($author)\n详情:\($url)"}}')
|
||||
msg="${msg}
|
||||
🔄 重复构建 ${tag}"
|
||||
fi
|
||||
|
||||
msg="${msg}
|
||||
构建结果:linux$(icon "$linux_r") macOS$(icon "$macos_r") windows$(icon "$windows_r")
|
||||
Release:$(icon "$publish_r")
|
||||
提交:${commit}
|
||||
作者:${{ github.actor }}
|
||||
详情:${url}"
|
||||
|
||||
payload=$(jq -n --arg text "$msg" '{msg_type: "text", content: {text: $text}}')
|
||||
curl -sS -H "Content-Type: application/json" -X POST -d "$payload" "$WEBHOOK_URL"
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
/target
|
||||
.env
|
||||
.DS_Store
|
||||
54
AGENTS.md
54
AGENTS.md
@@ -77,6 +77,15 @@ secrets add -n <namespace> --kind <kind> --name <name> \
|
||||
secrets search [-n <namespace>] [--kind <kind>] [--tag <tag>] [-q <keyword>] [--show-secrets]
|
||||
# -q 匹配范围:name、namespace、kind、metadata 全文内容、tags
|
||||
|
||||
# 增量更新已有记录(合并语义,记录不存在则报错)
|
||||
secrets update -n <namespace> --kind <kind> --name <name> \
|
||||
[--add-tag <tag>]... # 添加标签(不影响已有标签)
|
||||
[--remove-tag <tag>]... # 移除标签
|
||||
[-m key=value]... # 新增或覆盖 metadata 字段(不影响其他字段)
|
||||
[--remove-meta <key>]... # 删除 metadata 字段
|
||||
[-s key=value]... # 新增或覆盖 encrypted 字段(不影响其他字段)
|
||||
[--remove-secret <key>]... # 删除 encrypted 字段
|
||||
|
||||
# 删除
|
||||
secrets delete -n <namespace> --kind <kind> --name <name>
|
||||
```
|
||||
@@ -104,6 +113,19 @@ secrets search -n refining --kind service --show-secrets
|
||||
|
||||
# 按 tag 筛选
|
||||
secrets search --tag hongkong
|
||||
|
||||
# 只更新一个 IP(不影响其他 metadata/secrets/tags)
|
||||
secrets update -n refining --kind server --name i-uf63f2uookgs5uxmrdyc \
|
||||
-m ip=10.0.0.1
|
||||
|
||||
# 给一条记录新增 tag 并轮换密码
|
||||
secrets update -n refining --kind service --name gitea \
|
||||
--add-tag production \
|
||||
-s token=<new-token>
|
||||
|
||||
# 移除一个废弃的 metadata 字段
|
||||
secrets update -n refining --kind service --name mqtt \
|
||||
--remove-meta old_port
|
||||
```
|
||||
|
||||
## 代码规范
|
||||
@@ -114,6 +136,38 @@ secrets search --tag hongkong
|
||||
- 新增 `kind` 类型时:只需在 `add` 调用时传入,无需改代码
|
||||
- 字段命名:CLI 短标志 `-n`=namespace,`-m`=meta,`-s`=secret,`-q`=query
|
||||
|
||||
## 提交前检查(必须全部通过)
|
||||
|
||||
每次提交代码前,请在本地依次执行以下检查,**全部通过后再 push**:
|
||||
|
||||
### 1. 版本号(按需)
|
||||
|
||||
若本次改动需要发版,请先确认 `Cargo.toml` 中的 `version` 已提升,避免 CI 打出的 Tag 与已有版本重复。可通过 git tag 判断:
|
||||
|
||||
```bash
|
||||
# 查看当前 Cargo.toml 版本
|
||||
grep '^version' Cargo.toml
|
||||
|
||||
# 查看是否已存在该版本对应的 tag(CI 使用格式 secrets-<version>)
|
||||
git tag -l 'secrets-*'
|
||||
```
|
||||
|
||||
若当前版本已被 tag(例如已有 `secrets-0.1.0` 且 `Cargo.toml` 仍为 `0.1.0`),则应在 `Cargo.toml` 中 bump 版本号后再提交,以便 CI 自动打新 Tag 并发布 Release。
|
||||
|
||||
### 2. 格式、Lint、测试
|
||||
|
||||
```bash
|
||||
cargo fmt -- --check # 格式检查(不通过则运行 cargo fmt 修复)
|
||||
cargo clippy -- -D warnings # Lint 检查(消除所有 warning)
|
||||
cargo test # 单元/集成测试
|
||||
```
|
||||
|
||||
或一次性执行:
|
||||
|
||||
```bash
|
||||
cargo fmt -- --check && cargo clippy -- -D warnings && cargo test
|
||||
```
|
||||
|
||||
## CI/CD
|
||||
|
||||
- Gitea Actions(runner: debian)
|
||||
|
||||
312
Cargo.lock
generated
312
Cargo.lock
generated
@@ -227,16 +227,6 @@ version = "0.9.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.7"
|
||||
@@ -379,12 +369,6 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
@@ -408,21 +392,6 @@ version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
|
||||
dependencies = [
|
||||
"foreign-types-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types-shared"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.2.2"
|
||||
@@ -817,12 +786,6 @@ dependencies = [
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||
|
||||
[[package]]
|
||||
name = "litemap"
|
||||
version = "0.8.1"
|
||||
@@ -871,23 +834,6 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "native-tls"
|
||||
version = "0.2.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"openssl",
|
||||
"openssl-probe",
|
||||
"openssl-sys",
|
||||
"schannel",
|
||||
"security-framework",
|
||||
"security-framework-sys",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint-dig"
|
||||
version = "0.8.6"
|
||||
@@ -946,50 +892,6 @@ version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.76"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if",
|
||||
"foreign-types",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"openssl-macros",
|
||||
"openssl-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-macros"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-probe"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.112"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking"
|
||||
version = "2.2.1"
|
||||
@@ -1173,6 +1075,20 @@ dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"getrandom 0.2.17",
|
||||
"libc",
|
||||
"untrusted",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rsa"
|
||||
version = "0.9.10"
|
||||
@@ -1194,16 +1110,37 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.1.4"
|
||||
name = "rustls"
|
||||
version = "0.23.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
|
||||
checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.61.2",
|
||||
"once_cell",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"rustls-webpki",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pki-types"
|
||||
version = "1.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd"
|
||||
dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.103.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1218,15 +1155,6 @@ version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||
|
||||
[[package]]
|
||||
name = "schannel"
|
||||
version = "0.1.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
@@ -1235,7 +1163,7 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "secrets"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
@@ -1249,29 +1177,6 @@ dependencies = [
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "3.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"core-foundation",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
"security-framework-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework-sys"
|
||||
version = "2.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.27"
|
||||
@@ -1469,9 +1374,9 @@ dependencies = [
|
||||
"indexmap",
|
||||
"log",
|
||||
"memchr",
|
||||
"native-tls",
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"rustls",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
@@ -1482,6 +1387,7 @@ dependencies = [
|
||||
"tracing",
|
||||
"url",
|
||||
"uuid",
|
||||
"webpki-roots 0.26.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1682,19 +1588,6 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom 0.4.2",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.18"
|
||||
@@ -1889,6 +1782,12 @@ version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.5.8"
|
||||
@@ -2046,6 +1945,24 @@ dependencies = [
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "0.26.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
|
||||
dependencies = [
|
||||
"webpki-roots 1.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "whoami"
|
||||
version = "1.6.1"
|
||||
@@ -2121,7 +2038,16 @@ version = "0.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2139,13 +2065,29 @@ version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
"windows_aarch64_gnullvm 0.48.5",
|
||||
"windows_aarch64_msvc 0.48.5",
|
||||
"windows_i686_gnu 0.48.5",
|
||||
"windows_i686_msvc 0.48.5",
|
||||
"windows_x86_64_gnu 0.48.5",
|
||||
"windows_x86_64_gnullvm 0.48.5",
|
||||
"windows_x86_64_msvc 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm 0.52.6",
|
||||
"windows_aarch64_msvc 0.52.6",
|
||||
"windows_i686_gnu 0.52.6",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc 0.52.6",
|
||||
"windows_x86_64_gnu 0.52.6",
|
||||
"windows_x86_64_gnullvm 0.52.6",
|
||||
"windows_x86_64_msvc 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2154,42 +2096,90 @@ version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "1.0.0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "secrets"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
@@ -10,7 +10,7 @@ clap = { version = "4.6.0", features = ["derive", "env"] }
|
||||
dotenvy = "0.15.7"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_json = "1.0.149"
|
||||
sqlx = { version = "0.8.6", features = ["runtime-tokio", "tls-native-tls", "postgres", "uuid", "json", "chrono"] }
|
||||
sqlx = { version = "0.8.6", features = ["runtime-tokio", "tls-rustls", "postgres", "uuid", "json", "chrono"] }
|
||||
tokio = { version = "1.50.0", features = ["full"] }
|
||||
toml = "1.0.7"
|
||||
uuid = { version = "1.22.0", features = ["serde", "v4"] }
|
||||
|
||||
@@ -33,6 +33,7 @@ secrets -h
|
||||
secrets help add
|
||||
secrets help search
|
||||
secrets help delete
|
||||
secrets help update
|
||||
|
||||
# 添加服务器
|
||||
secrets add -n refining --kind server --name my-server \
|
||||
@@ -53,6 +54,11 @@ secrets search --tag hongkong
|
||||
secrets search -q mqtt # 关键词匹配 name / metadata / tags
|
||||
secrets search -n refining --kind service --name gitea --show-secrets
|
||||
|
||||
# 增量更新已有记录(合并语义,记录不存在则报错)
|
||||
secrets update -n refining --kind server --name my-server -m ip=10.0.0.1
|
||||
secrets update -n refining --kind service --name gitea --add-tag production -s token=<new-token>
|
||||
secrets update -n refining --kind service --name mqtt --remove-meta old_port --remove-secret old_key
|
||||
|
||||
# 删除
|
||||
secrets delete -n refining --kind server --name my-server
|
||||
```
|
||||
@@ -83,6 +89,7 @@ src/
|
||||
add.rs # upsert
|
||||
search.rs # 多条件查询
|
||||
delete.rs # 删除
|
||||
update.rs # 增量更新(合并 tags/metadata/encrypted)
|
||||
scripts/
|
||||
seed-data.sh # 导入 refining / ricnsmart 全量数据
|
||||
```
|
||||
|
||||
@@ -109,11 +109,10 @@ echo ""
|
||||
|
||||
# 1. 创建 Secret: RELEASE_TOKEN
|
||||
echo "1. 创建 Secret: RELEASE_TOKEN"
|
||||
encoded=$(echo -n "$GITEA_TOKEN" | base64)
|
||||
resp=$(curl -s -w "\n%{http_code}" -X PUT \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"data\":\"${encoded}\"}" \
|
||||
-d "{\"data\":\"${GITEA_TOKEN}\"}" \
|
||||
"${API_BASE}/repos/${OWNER}/${REPO}/actions/secrets/RELEASE_TOKEN")
|
||||
http_code=$(echo "$resp" | tail -n1)
|
||||
body=$(echo "$resp" | sed '$d')
|
||||
|
||||
@@ -4,7 +4,7 @@ use sqlx::PgPool;
|
||||
use std::fs;
|
||||
|
||||
/// Parse "key=value" entries. Value starting with '@' reads from file.
|
||||
fn parse_kv(entry: &str) -> Result<(String, String)> {
|
||||
pub(crate) fn parse_kv(entry: &str) -> Result<(String, String)> {
|
||||
let (key, raw_val) = entry.split_once('=').ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"Invalid format '{}'. Expected: key=value or key=@file",
|
||||
|
||||
@@ -2,14 +2,13 @@ use anyhow::Result;
|
||||
use sqlx::PgPool;
|
||||
|
||||
pub async fn run(pool: &PgPool, namespace: &str, kind: &str, name: &str) -> Result<()> {
|
||||
let result = sqlx::query(
|
||||
"DELETE FROM secrets WHERE namespace = $1 AND kind = $2 AND name = $3",
|
||||
)
|
||||
.bind(namespace)
|
||||
.bind(kind)
|
||||
.bind(name)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
let result =
|
||||
sqlx::query("DELETE FROM secrets WHERE namespace = $1 AND kind = $2 AND name = $3")
|
||||
.bind(namespace)
|
||||
.bind(kind)
|
||||
.bind(name)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
if result.rows_affected() == 0 {
|
||||
println!("Not found: [{}/{}] {}", namespace, kind, name);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod add;
|
||||
pub mod delete;
|
||||
pub mod search;
|
||||
pub mod update;
|
||||
|
||||
@@ -66,25 +66,25 @@ pub async fn run(
|
||||
}
|
||||
|
||||
for row in &rows {
|
||||
println!(
|
||||
"[{}/{}] {}",
|
||||
row.namespace, row.kind, row.name,
|
||||
);
|
||||
println!("[{}/{}] {}", row.namespace, row.kind, row.name,);
|
||||
println!(" id: {}", row.id);
|
||||
|
||||
if !row.tags.is_empty() {
|
||||
println!(" tags: [{}]", row.tags.join(", "));
|
||||
}
|
||||
|
||||
let meta_obj = row.metadata.as_object();
|
||||
if let Some(m) = meta_obj {
|
||||
if !m.is_empty() {
|
||||
println!(" metadata: {}", serde_json::to_string_pretty(&row.metadata)?);
|
||||
}
|
||||
if row.metadata.as_object().is_some_and(|m| !m.is_empty()) {
|
||||
println!(
|
||||
" metadata: {}",
|
||||
serde_json::to_string_pretty(&row.metadata)?
|
||||
);
|
||||
}
|
||||
|
||||
if show_secrets {
|
||||
println!(" secrets: {}", serde_json::to_string_pretty(&row.encrypted)?);
|
||||
println!(
|
||||
" secrets: {}",
|
||||
serde_json::to_string_pretty(&row.encrypted)?
|
||||
);
|
||||
} else {
|
||||
let keys: Vec<String> = row
|
||||
.encrypted
|
||||
@@ -92,11 +92,17 @@ pub async fn run(
|
||||
.map(|m| m.keys().cloned().collect())
|
||||
.unwrap_or_default();
|
||||
if !keys.is_empty() {
|
||||
println!(" secrets: [{}] (--show-secrets to reveal)", keys.join(", "));
|
||||
println!(
|
||||
" secrets: [{}] (--show-secrets to reveal)",
|
||||
keys.join(", ")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
println!(" created: {}", row.created_at.format("%Y-%m-%d %H:%M:%S UTC"));
|
||||
println!(
|
||||
" created: {}",
|
||||
row.created_at.format("%Y-%m-%d %H:%M:%S UTC")
|
||||
);
|
||||
println!();
|
||||
}
|
||||
println!("{} record(s) found.", rows.len());
|
||||
|
||||
125
src/commands/update.rs
Normal file
125
src/commands/update.rs
Normal file
@@ -0,0 +1,125 @@
|
||||
use anyhow::Result;
|
||||
use serde_json::{Map, Value};
|
||||
use sqlx::PgPool;
|
||||
|
||||
use super::add::parse_kv;
|
||||
|
||||
pub struct UpdateArgs<'a> {
|
||||
pub namespace: &'a str,
|
||||
pub kind: &'a str,
|
||||
pub name: &'a str,
|
||||
pub add_tags: &'a [String],
|
||||
pub remove_tags: &'a [String],
|
||||
pub meta_entries: &'a [String],
|
||||
pub remove_meta: &'a [String],
|
||||
pub secret_entries: &'a [String],
|
||||
pub remove_secrets: &'a [String],
|
||||
}
|
||||
|
||||
pub async fn run(pool: &PgPool, args: UpdateArgs<'_>) -> Result<()> {
|
||||
let row = sqlx::query!(
|
||||
r#"
|
||||
SELECT id, tags, metadata, encrypted
|
||||
FROM secrets
|
||||
WHERE namespace = $1 AND kind = $2 AND name = $3
|
||||
"#,
|
||||
args.namespace,
|
||||
args.kind,
|
||||
args.name,
|
||||
)
|
||||
.fetch_optional(pool)
|
||||
.await?;
|
||||
|
||||
let row = row.ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"Not found: [{}/{}] {}. Use `add` to create it first.",
|
||||
args.namespace,
|
||||
args.kind,
|
||||
args.name
|
||||
)
|
||||
})?;
|
||||
|
||||
// Merge tags
|
||||
let mut tags: Vec<String> = row.tags;
|
||||
for t in args.add_tags {
|
||||
if !tags.contains(t) {
|
||||
tags.push(t.clone());
|
||||
}
|
||||
}
|
||||
tags.retain(|t| !args.remove_tags.contains(t));
|
||||
|
||||
// Merge metadata
|
||||
let mut meta_map: Map<String, Value> = match row.metadata {
|
||||
Value::Object(m) => m,
|
||||
_ => Map::new(),
|
||||
};
|
||||
for entry in args.meta_entries {
|
||||
let (key, value) = parse_kv(entry)?;
|
||||
meta_map.insert(key, Value::String(value));
|
||||
}
|
||||
for key in args.remove_meta {
|
||||
meta_map.remove(key);
|
||||
}
|
||||
let metadata = Value::Object(meta_map);
|
||||
|
||||
// Merge encrypted
|
||||
let mut enc_map: Map<String, Value> = match row.encrypted {
|
||||
Value::Object(m) => m,
|
||||
_ => Map::new(),
|
||||
};
|
||||
for entry in args.secret_entries {
|
||||
let (key, value) = parse_kv(entry)?;
|
||||
enc_map.insert(key, Value::String(value));
|
||||
}
|
||||
for key in args.remove_secrets {
|
||||
enc_map.remove(key);
|
||||
}
|
||||
let encrypted = Value::Object(enc_map);
|
||||
|
||||
sqlx::query!(
|
||||
r#"
|
||||
UPDATE secrets
|
||||
SET tags = $1, metadata = $2, encrypted = $3, updated_at = NOW()
|
||||
WHERE id = $4
|
||||
"#,
|
||||
&tags,
|
||||
metadata,
|
||||
encrypted,
|
||||
row.id,
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
println!("Updated: [{}/{}] {}", args.namespace, args.kind, args.name);
|
||||
|
||||
if !args.add_tags.is_empty() {
|
||||
println!(" +tags: {}", args.add_tags.join(", "));
|
||||
}
|
||||
if !args.remove_tags.is_empty() {
|
||||
println!(" -tags: {}", args.remove_tags.join(", "));
|
||||
}
|
||||
if !args.meta_entries.is_empty() {
|
||||
let keys: Vec<&str> = args
|
||||
.meta_entries
|
||||
.iter()
|
||||
.filter_map(|s| s.split_once('=').map(|(k, _)| k))
|
||||
.collect();
|
||||
println!(" +metadata: {}", keys.join(", "));
|
||||
}
|
||||
if !args.remove_meta.is_empty() {
|
||||
println!(" -metadata: {}", args.remove_meta.join(", "));
|
||||
}
|
||||
if !args.secret_entries.is_empty() {
|
||||
let keys: Vec<&str> = args
|
||||
.secret_entries
|
||||
.iter()
|
||||
.filter_map(|s| s.split_once('=').map(|(k, _)| k))
|
||||
.collect();
|
||||
println!(" +secrets: {}", keys.join(", "));
|
||||
}
|
||||
if !args.remove_secrets.is_empty() {
|
||||
println!(" -secrets: {}", args.remove_secrets.join(", "));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
use anyhow::Result;
|
||||
use sqlx::postgres::PgPoolOptions;
|
||||
use sqlx::PgPool;
|
||||
use sqlx::postgres::PgPoolOptions;
|
||||
|
||||
pub async fn create_pool(database_url: &str) -> Result<PgPool> {
|
||||
let pool = PgPoolOptions::new()
|
||||
|
||||
64
src/main.rs
64
src/main.rs
@@ -7,7 +7,11 @@ use clap::{Parser, Subcommand};
|
||||
use dotenvy::dotenv;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "secrets", version, about = "Secrets & config manager backed by PostgreSQL")]
|
||||
#[command(
|
||||
name = "secrets",
|
||||
version,
|
||||
about = "Secrets & config manager backed by PostgreSQL"
|
||||
)]
|
||||
struct Cli {
|
||||
/// Database URL (or set DATABASE_URL env var)
|
||||
#[arg(long, env = "DATABASE_URL", global = true, default_value = "")]
|
||||
@@ -72,6 +76,37 @@ enum Commands {
|
||||
#[arg(long)]
|
||||
name: String,
|
||||
},
|
||||
|
||||
/// Incrementally update an existing record (merge semantics)
|
||||
Update {
|
||||
/// Namespace (e.g. refining, ricnsmart)
|
||||
#[arg(short, long)]
|
||||
namespace: String,
|
||||
/// Kind of record (server, service, key, ...)
|
||||
#[arg(long)]
|
||||
kind: String,
|
||||
/// Human-readable name
|
||||
#[arg(long)]
|
||||
name: String,
|
||||
/// Add a tag (repeatable)
|
||||
#[arg(long = "add-tag")]
|
||||
add_tags: Vec<String>,
|
||||
/// Remove a tag (repeatable)
|
||||
#[arg(long = "remove-tag")]
|
||||
remove_tags: Vec<String>,
|
||||
/// Set or overwrite a metadata field: key=value (repeatable, @file supported)
|
||||
#[arg(long = "meta", short = 'm')]
|
||||
meta: Vec<String>,
|
||||
/// Remove a metadata field by key (repeatable)
|
||||
#[arg(long = "remove-meta")]
|
||||
remove_meta: Vec<String>,
|
||||
/// Set or overwrite a secret field: key=value (repeatable, @file supported)
|
||||
#[arg(long = "secret", short = 's')]
|
||||
secrets: Vec<String>,
|
||||
/// Remove a secret field by key (repeatable)
|
||||
#[arg(long = "remove-secret")]
|
||||
remove_secrets: Vec<String>,
|
||||
},
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
@@ -126,6 +161,33 @@ async fn main() -> Result<()> {
|
||||
} => {
|
||||
commands::delete::run(&pool, namespace, kind, name).await?;
|
||||
}
|
||||
Commands::Update {
|
||||
namespace,
|
||||
kind,
|
||||
name,
|
||||
add_tags,
|
||||
remove_tags,
|
||||
meta,
|
||||
remove_meta,
|
||||
secrets,
|
||||
remove_secrets,
|
||||
} => {
|
||||
commands::update::run(
|
||||
&pool,
|
||||
commands::update::UpdateArgs {
|
||||
namespace,
|
||||
kind,
|
||||
name,
|
||||
add_tags,
|
||||
remove_tags,
|
||||
meta_entries: meta,
|
||||
remove_meta,
|
||||
secret_entries: secrets,
|
||||
remove_secrets,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
Reference in New Issue
Block a user