Compare commits
4 Commits
@@ -0,0 +1,236 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
|
||||
env:
|
||||
GITEA_SERVER_URL: https://git.pchuan.top
|
||||
RUST_IMAGE: docker.m.daocloud.io/library/rust:1-bookworm
|
||||
CARGO_REGISTRY: sparse+https://rsproxy.cn/index/
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
prepare-release:
|
||||
name: Prepare release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Create or update Gitea release
|
||||
shell: bash
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
current_tag="$(git describe --tags --exact-match)"
|
||||
version="${current_tag#v}"
|
||||
previous_tag="$(git describe --tags --abbrev=0 "${current_tag}^{}^" 2>/dev/null || true)"
|
||||
api_base="${GITEA_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}"
|
||||
|
||||
{
|
||||
echo "## Changes"
|
||||
echo
|
||||
if [[ -n "${previous_tag}" ]]; then
|
||||
echo "Changes since \`${previous_tag}\`:"
|
||||
echo
|
||||
git log --no-merges --pretty=format:'- %s (%h)' "${previous_tag}..${current_tag}"
|
||||
else
|
||||
echo "Initial release changes:"
|
||||
echo
|
||||
git log --no-merges --pretty=format:'- %s (%h)' "${current_tag}^{}"
|
||||
fi
|
||||
echo
|
||||
echo
|
||||
echo "## Artifacts"
|
||||
echo
|
||||
echo "- \`cdxs-${version}-windows-x64.exe\`"
|
||||
echo "- \`cdxs-${version}-linux-x64\`"
|
||||
echo "- \`cdxs-${version}-linux-arm64\`"
|
||||
} > RELEASE_NOTES.md
|
||||
|
||||
body="$(python3 -c 'import json, pathlib; print(json.dumps(pathlib.Path("RELEASE_NOTES.md").read_text()))')"
|
||||
payload="$(printf '{"tag_name":"%s","target_commitish":"%s","name":"%s","body":%s,"draft":false,"prerelease":false}' \
|
||||
"${current_tag}" "${GITHUB_SHA}" "${current_tag}" "${body}")"
|
||||
|
||||
status="$(curl -sS -o release.json -w '%{http_code}' \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${api_base}/releases/tags/${current_tag}")"
|
||||
|
||||
if [[ "${status}" == "200" ]]; then
|
||||
release_id="$(python3 -c 'import json; print(json.load(open("release.json"))["id"])')"
|
||||
curl -sS -X PATCH \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "${payload}" \
|
||||
"${api_base}/releases/${release_id}" >/dev/null
|
||||
elif [[ "${status}" == "404" ]]; then
|
||||
curl -sS -X POST \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "${payload}" \
|
||||
"${api_base}/releases" >/dev/null
|
||||
else
|
||||
cat release.json
|
||||
echo "Unexpected Gitea release lookup status: ${status}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
build-windows-x64:
|
||||
name: Build windows-x64
|
||||
runs-on: windows-latest
|
||||
needs: prepare-release
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust target
|
||||
shell: pwsh
|
||||
run: |
|
||||
New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.cargo" | Out-Null
|
||||
@"
|
||||
[source.crates-io]
|
||||
replace-with = "mirror"
|
||||
|
||||
[source.mirror]
|
||||
registry = "$env:CARGO_REGISTRY"
|
||||
"@ | Set-Content -Path "$env:USERPROFILE\.cargo\config.toml" -Encoding utf8
|
||||
rustup target add x86_64-pc-windows-msvc
|
||||
|
||||
- name: Build release binary
|
||||
shell: pwsh
|
||||
run: |
|
||||
$tag = git describe --tags --exact-match
|
||||
$version = $tag -replace '^v', ''
|
||||
./scripts/publish-binaries.ps1 -Version $version -Target win -Clean
|
||||
|
||||
- name: Upload release asset
|
||||
shell: pwsh
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
run: |
|
||||
$tag = git describe --tags --exact-match
|
||||
$release = Invoke-RestMethod `
|
||||
-Headers @{ Authorization = "token $env:GITEA_TOKEN" } `
|
||||
-Uri "$env:GITEA_SERVER_URL/api/v1/repos/$env:GITHUB_REPOSITORY/releases/tags/$tag"
|
||||
$file = Get-ChildItem -Path dist -File | Select-Object -First 1
|
||||
curl.exe -fsS -X POST `
|
||||
-H "Authorization: token $env:GITEA_TOKEN" `
|
||||
-F "attachment=@$($file.FullName)" `
|
||||
"$env:GITEA_SERVER_URL/api/v1/repos/$env:GITHUB_REPOSITORY/releases/$($release.id)/assets?name=$($file.Name)"
|
||||
|
||||
build-linux-x64:
|
||||
name: Build linux-x64
|
||||
runs-on: ubuntu-latest
|
||||
needs: prepare-release
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build release binary
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
tag="$(git describe --tags --exact-match)"
|
||||
version="${tag#v}"
|
||||
target_name="linux-x64"
|
||||
rust_target="x86_64-unknown-linux-gnu"
|
||||
platform="linux/amd64"
|
||||
image_tag="cdxs-build:${target_name}-${GITHUB_RUN_ID:-manual}"
|
||||
container_name="cdxs-build-${target_name}-${GITHUB_RUN_ID:-manual}"
|
||||
|
||||
mkdir -p dist
|
||||
docker buildx build \
|
||||
--platform "${platform}" \
|
||||
--target builder \
|
||||
--build-arg "RUST_TARGET=${rust_target}" \
|
||||
--build-arg "RUST_IMAGE=${RUST_IMAGE}" \
|
||||
--build-arg "CARGO_REGISTRY=${CARGO_REGISTRY}" \
|
||||
--load \
|
||||
-t "${image_tag}" \
|
||||
-f scripts/docker/Dockerfile.release \
|
||||
.
|
||||
|
||||
container_id="$(docker create --name "${container_name}" "${image_tag}")"
|
||||
trap 'docker rm -f "${container_name}" >/dev/null 2>&1 || true' EXIT
|
||||
docker cp "${container_id}:/out/cdxs" "dist/cdxs-${version}-${target_name}"
|
||||
|
||||
- name: Upload release asset
|
||||
shell: bash
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
tag="$(git describe --tags --exact-match)"
|
||||
api_base="${GITEA_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}"
|
||||
curl -sS -H "Authorization: token ${GITEA_TOKEN}" "${api_base}/releases/tags/${tag}" > release.json
|
||||
release_id="$(python3 -c 'import json; print(json.load(open("release.json"))["id"])')"
|
||||
file="$(find dist -maxdepth 1 -type f | head -n 1)"
|
||||
name="$(basename "${file}")"
|
||||
curl -fsS -X POST \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-F "attachment=@${file}" \
|
||||
"${api_base}/releases/${release_id}/assets?name=${name}"
|
||||
|
||||
build-linux-arm64:
|
||||
name: Build linux-arm64
|
||||
runs-on: ubuntu-latest
|
||||
needs: prepare-release
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build release binary
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
tag="$(git describe --tags --exact-match)"
|
||||
version="${tag#v}"
|
||||
target_name="linux-arm64"
|
||||
rust_target="aarch64-unknown-linux-gnu"
|
||||
platform="linux/arm64"
|
||||
image_tag="cdxs-build:${target_name}-${GITHUB_RUN_ID:-manual}"
|
||||
container_name="cdxs-build-${target_name}-${GITHUB_RUN_ID:-manual}"
|
||||
|
||||
mkdir -p dist
|
||||
docker buildx build \
|
||||
--platform "${platform}" \
|
||||
--target builder \
|
||||
--build-arg "RUST_TARGET=${rust_target}" \
|
||||
--build-arg "RUST_IMAGE=${RUST_IMAGE}" \
|
||||
--build-arg "CARGO_REGISTRY=${CARGO_REGISTRY}" \
|
||||
--load \
|
||||
-t "${image_tag}" \
|
||||
-f scripts/docker/Dockerfile.release \
|
||||
.
|
||||
|
||||
container_id="$(docker create --name "${container_name}" "${image_tag}")"
|
||||
trap 'docker rm -f "${container_name}" >/dev/null 2>&1 || true' EXIT
|
||||
docker cp "${container_id}:/out/cdxs" "dist/cdxs-${version}-${target_name}"
|
||||
|
||||
- name: Upload release asset
|
||||
shell: bash
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
tag="$(git describe --tags --exact-match)"
|
||||
api_base="${GITEA_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}"
|
||||
curl -sS -H "Authorization: token ${GITEA_TOKEN}" "${api_base}/releases/tags/${tag}" > release.json
|
||||
release_id="$(python3 -c 'import json; print(json.load(open("release.json"))["id"])')"
|
||||
file="$(find dist -maxdepth 1 -type f | head -n 1)"
|
||||
name="$(basename "${file}")"
|
||||
curl -fsS -X POST \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-F "attachment=@${file}" \
|
||||
"${api_base}/releases/${release_id}/assets?name=${name}"
|
||||
Generated
+1
-1
@@ -232,7 +232,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cdxs"
|
||||
version = "0.1.1"
|
||||
version = "0.1.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "cdxs"
|
||||
version = "0.1.1"
|
||||
version = "0.1.2"
|
||||
edition = "2021"
|
||||
description = "Codex account switcher CLI"
|
||||
|
||||
|
||||
@@ -2,9 +2,13 @@ ARG RUST_IMAGE=rust:1-bookworm
|
||||
FROM --platform=$BUILDPLATFORM ${RUST_IMAGE} AS builder
|
||||
|
||||
ARG RUST_TARGET=x86_64-unknown-linux-gnu
|
||||
ARG CARGO_REGISTRY=sparse+https://rsproxy.cn/index/
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN mkdir -p /usr/local/cargo \
|
||||
&& printf '[source.crates-io]\nreplace-with = "mirror"\n\n[source.mirror]\nregistry = "%s"\n' "${CARGO_REGISTRY}" > /usr/local/cargo/config.toml
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
|
||||
+117
-20
@@ -97,36 +97,36 @@ fn print_accounts(store: &Store, json: bool) -> Result<()> {
|
||||
println!("没有保存的账号。可使用 `cdxs import auth` 导入。");
|
||||
return Ok(());
|
||||
}
|
||||
println!(
|
||||
"{:<3} {:<22} {:<34} {:<10} {:<12} {}",
|
||||
"", "ID", "Email", "Mode", "Plan", "Quota"
|
||||
);
|
||||
for account in &store.accounts {
|
||||
print_account_table_border();
|
||||
print_account_table_row("", "ID", "Email", "Mode", "Plan", "5h", "Week");
|
||||
print_account_table_border();
|
||||
let current_account_id = store.meta.current_account_id.as_deref();
|
||||
let mut accounts = store.accounts.iter().collect::<Vec<_>>();
|
||||
accounts.sort_by_key(|account| {
|
||||
if Some(account.id.as_str()) == current_account_id {
|
||||
0
|
||||
} else {
|
||||
1
|
||||
}
|
||||
});
|
||||
for account in accounts {
|
||||
let current = if store.meta.current_account_id.as_deref() == Some(account.id.as_str()) {
|
||||
"*"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let quota = account
|
||||
.quota
|
||||
.as_ref()
|
||||
.map(|q| {
|
||||
format!(
|
||||
"5h={}%, weekly={}%",
|
||||
q.primary_remaining_percent, q.secondary_remaining_percent
|
||||
)
|
||||
})
|
||||
.unwrap_or_else(|| "-".to_string());
|
||||
println!(
|
||||
"{:<3} {:<22} {:<34} {:<10} {:<12} {}",
|
||||
let (primary_quota, secondary_quota) = format_quota_cells(account);
|
||||
print_account_table_row(
|
||||
current,
|
||||
shorten(&account.id, 22),
|
||||
shorten(&account.email, 34),
|
||||
&account.id,
|
||||
&account.email,
|
||||
auth_file::account_auth_mode_name(account),
|
||||
account.plan_type.as_deref().unwrap_or("-"),
|
||||
quota
|
||||
&primary_quota,
|
||||
&secondary_quota,
|
||||
);
|
||||
}
|
||||
print_account_table_border();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -633,6 +633,103 @@ fn format_quota(account: &Account) -> String {
|
||||
.unwrap_or_else(|| "-".to_string())
|
||||
}
|
||||
|
||||
fn format_quota_cells(account: &Account) -> (String, String) {
|
||||
let Some(quota) = account.quota.as_ref() else {
|
||||
return ("-".to_string(), "-".to_string());
|
||||
};
|
||||
(
|
||||
format_quota_cell(
|
||||
quota.primary_remaining_percent,
|
||||
quota.primary_reset_time,
|
||||
format_reset_time,
|
||||
),
|
||||
format_quota_cell(
|
||||
quota.secondary_remaining_percent,
|
||||
quota.secondary_reset_time,
|
||||
format_week_reset_time,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn format_quota_cell(
|
||||
remaining_percent: i32,
|
||||
reset_time: Option<i64>,
|
||||
format_reset: fn(i64) -> String,
|
||||
) -> String {
|
||||
format!(
|
||||
"{:>3}% / {}",
|
||||
remaining_percent,
|
||||
reset_time
|
||||
.map(format_reset)
|
||||
.unwrap_or_else(|| "-".to_string())
|
||||
)
|
||||
}
|
||||
|
||||
fn format_reset_time(reset_time: i64) -> String {
|
||||
let now = Utc::now().timestamp();
|
||||
let seconds = reset_time.saturating_sub(now);
|
||||
if seconds <= 0 {
|
||||
return "now".to_string();
|
||||
}
|
||||
|
||||
let days = seconds / 86_400;
|
||||
let hours = (seconds % 86_400) / 3_600;
|
||||
let minutes = (seconds % 3_600) / 60;
|
||||
if days > 0 {
|
||||
format!("{days}d{hours}h")
|
||||
} else if hours > 0 {
|
||||
format!("{hours}h{minutes:02}m")
|
||||
} else {
|
||||
format!("{minutes}m")
|
||||
}
|
||||
}
|
||||
|
||||
fn format_week_reset_time(reset_time: i64) -> String {
|
||||
let now = Utc::now().timestamp();
|
||||
let seconds = reset_time.saturating_sub(now);
|
||||
if seconds <= 0 {
|
||||
return "now".to_string();
|
||||
}
|
||||
|
||||
let days = seconds / 86_400;
|
||||
let hours = (seconds % 86_400) / 3_600;
|
||||
format!("{days}d{hours:02}h")
|
||||
}
|
||||
|
||||
fn print_account_table_border() {
|
||||
println!(
|
||||
"+{}+{}+{}+{}+{}+{}+{}+",
|
||||
"-".repeat(3),
|
||||
"-".repeat(24),
|
||||
"-".repeat(30),
|
||||
"-".repeat(8),
|
||||
"-".repeat(8),
|
||||
"-".repeat(14),
|
||||
"-".repeat(14)
|
||||
);
|
||||
}
|
||||
|
||||
fn print_account_table_row(
|
||||
marker: &str,
|
||||
id: &str,
|
||||
email: &str,
|
||||
mode: &str,
|
||||
plan: &str,
|
||||
primary_quota: &str,
|
||||
secondary_quota: &str,
|
||||
) {
|
||||
println!(
|
||||
"| {:<1} | {:<22} | {:<28} | {:<6} | {:<6} | {:<12} | {:<12} |",
|
||||
shorten(marker, 1),
|
||||
shorten(id, 22),
|
||||
shorten(email, 28),
|
||||
shorten(mode, 6),
|
||||
shorten(plan, 6),
|
||||
shorten(primary_quota, 12),
|
||||
shorten(secondary_quota, 12)
|
||||
);
|
||||
}
|
||||
|
||||
fn shorten(value: &str, width: usize) -> String {
|
||||
if value.chars().count() <= width {
|
||||
return value.to_string();
|
||||
|
||||
Reference in New Issue
Block a user