From 3225b4e652915094588b03d1db1a6d50d7936bbb Mon Sep 17 00:00:00 2001 From: Alois Date: Mon, 11 May 2026 14:36:12 +0200 Subject: [PATCH 1/6] (feat): add custom ttp urls (qol): update todo --- .../web/src/components/screens/login/form.tsx | 59 +++++++++++++++---- apps/web/todo.md | 1 + packages/shared/src/data.ts | 2 + packages/ttp/src/context.tsx | 40 ++++++++++--- packages/ttp/src/values.ts | 1 - 5 files changed, 82 insertions(+), 21 deletions(-) diff --git a/apps/web/src/components/screens/login/form.tsx b/apps/web/src/components/screens/login/form.tsx index da5726c..0f297d6 100644 --- a/apps/web/src/components/screens/login/form.tsx +++ b/apps/web/src/components/screens/login/form.tsx @@ -23,7 +23,7 @@ const fetchedUser = z.object({ }); const formSchema = z.object({ - username: z.string().min(1).max(15), + username: z.string().min(1).max(255), private_key: z.string().min(1).max(92), }); @@ -35,6 +35,7 @@ const formSchema = z.object({ function parseTuFileContent(rawFileContent: string): { userId: number; privateKey: string; + domain: string | null; } { if (rawFileContent.trim().length === 0) { throw new Error("File is empty"); @@ -42,21 +43,40 @@ function parseTuFileContent(rawFileContent: string): { throw new Error("Invalid file"); } else if (rawFileContent.split("::").length !== 2) { throw new Error("Invalid file"); - } else if (rawFileContent.split("::")[0].length === 0) { + } + + const left = rawFileContent.split("::")[0]; + const right = rawFileContent.split("::")[1]; + + if (left.length === 0) { throw new Error("Invalid file"); - } else if (rawFileContent.split("::")[1].length === 0) { + } else if (right.length === 0) { throw new Error("Invalid file"); - } else if (isNaN(Number(rawFileContent.split("::")[0]))) { + } else if (isNaN(Number(left)) && !left.includes("@")) { throw new Error("Invalid file"); } const [userIdString, privateKey] = rawFileContent.split("::"); - const userId = Number(userIdString); + const userId = isNaN(Number(userIdString)) + ? Number(userIdString.split("@")[0]) + : Number(userIdString); + + const rawDomain = userIdString.includes("@") + ? userIdString.split("@")[1] + : null; + const domain = rawDomain ? (rawDomain.includes(":") ? rawDomain : rawDomain + ":1984") : null; + if (!userId || !privateKey) { throw new Error("Invalid file"); } - return { userId, privateKey }; + console.log({ + domain, + rawDomain, + userId, + }); + + return { userId, privateKey, domain }; } /** @@ -86,6 +106,9 @@ export default function Form() { await save("session_id", Date.now()); await save("user_id", parsed.userId); await save("private_key", parsed.privateKey); + if (parsed.domain) { + await save("ttp_url", `https://${parsed.domain}/`); + } location.href = "/"; } catch (error) { @@ -114,9 +137,14 @@ export default function Form() { return; } + const inputUsername = inputParse.data.username; + const actualUsername = inputUsername.includes("@") ? inputUsername.split("@")[0] : inputUsername; + const rawDomain = inputUsername.includes("@") ? inputUsername.split("@")[1] : null; + const domain = rawDomain ? (rawDomain.includes(":") ? rawDomain : rawDomain + ":1984") : null; + try { const response = await fetch( - `https://omega.tensamin.net/api/get/id/${inputParse.data.username}`, + `https://omega.tensamin.net/api/get/id/${actualUsername}`, ); const rawData = await response.arrayBuffer(); @@ -137,6 +165,9 @@ export default function Form() { await save("session_id", Date.now()); await save("user_id", user.user_id); await save("private_key", inputParse.data.private_key); + if (domain) { + await save("ttp_url", `https://${domain}/`); + } location.href = "/"; } catch (error) { @@ -154,7 +185,7 @@ export default function Form() { {isTauriEnv ? ( <> { + onData={async (data) => { if (!data.startsWith("tensamin://tu::")) { toast("error", "Invalid QR code"); return; @@ -162,10 +193,14 @@ export default function Form() { const decoded = data.replace("tensamin://tu::", ""); try { - const { userId, privateKey } = parseTuFileContent(decoded); - save("session_id", Date.now()); - save("user_id", userId); - save("private_key", privateKey); + const { userId, privateKey, domain } = + parseTuFileContent(decoded); + await save("session_id", Date.now()); + await save("user_id", userId); + await save("private_key", privateKey); + if (domain) { + await save("ttp_url", `https://${domain}/`); + } location.href = "/"; } catch (error) { log(0, "login", "red", error); diff --git a/apps/web/todo.md b/apps/web/todo.md index 790b489..dd80800 100644 --- a/apps/web/todo.md +++ b/apps/web/todo.md @@ -1 +1,2 @@ - Make the sidebar use to make it feel less slow on mobile +- Add logout button diff --git a/packages/shared/src/data.ts b/packages/shared/src/data.ts index a059e9c..e03ca3d 100644 --- a/packages/shared/src/data.ts +++ b/packages/shared/src/data.ts @@ -244,6 +244,7 @@ export interface Storage extends SettingsStorageDefaults { legal_docs: z.infer; cached_contacts: Contacts; cached_communities: Communities; + ttp_url: string; } export const storageDefaults: Storage = { @@ -276,4 +277,5 @@ export const storageDefaults: Storage = { }, cached_contacts: [], cached_communities: [], + ttp_url: "https://tensamin.net:959", }; diff --git a/packages/ttp/src/context.tsx b/packages/ttp/src/context.tsx index f1c30aa..0a2ea2c 100644 --- a/packages/ttp/src/context.tsx +++ b/packages/ttp/src/context.tsx @@ -24,7 +24,6 @@ import { RECONNECT_RESET, RECONNECT_TRIES, RETRY_INTERVAL, - TRANSPORT_URL, } from "./values"; import { type Calls, @@ -193,6 +192,14 @@ export function Provider(props: { > | null>(null); const identificationStartedRef = useRef(false); + // Load ttp url + const [ttpUrl, setTtpUrl] = useState(null); + useEffect(() => { + load("ttp_url").then((url) => { + setTtpUrl(url); + }); + }, [load]); + /** * Sends typed protocol messages through the active transport client. * @param type Protocol message type. @@ -266,6 +273,10 @@ export function Provider(props: { }, [connected, identified, send]); useEffect(() => { + if (!ttpUrl) { + return; + } + let attempts = 0; let reconnectTimer: ReturnType | null = null; let reconnectResetTimer: ReturnType | null = null; @@ -343,7 +354,7 @@ export function Provider(props: { const transportClient = !props.blockConnection ? createTransportClient(schemas, { - url: TRANSPORT_URL, + url: ttpUrl, onReadyStateChange: (state) => { currentReadyState = state; setReadyState(state); @@ -392,8 +403,12 @@ export function Provider(props: { return; } + if (!ttpUrl) { + return; + } + try { - await transportClient?.connect(TRANSPORT_URL); + await transportClient?.connect(ttpUrl); } catch (connectError) { if (disposed) { return; @@ -460,7 +475,7 @@ export function Provider(props: { setIdentifying(false); identificationStartedRef.current = false; }; - }, [props.blockConnection]); + }, [props.blockConnection, ttpUrl]); useEffect(() => { if (!connected) { @@ -603,14 +618,19 @@ export function Provider(props: { }, [connected, decrypt, getSharedSecret, load, send]); const progress = useMemo(() => { + if (!ttpUrl) return 10; if (readyState === READY_STATE.CONNECTING) return 30; if (!connected) return 45; if (identifying) return 75; if (!identified) return 90; return 100; - }, [connected, identified, identifying, readyState]); + }, [connected, identified, identifying, readyState, ttpUrl]); const loadingTitle = useMemo(() => { + if (!ttpUrl) { + return "Looking up configuration"; + } + if (readyState === READY_STATE.CONNECTING || !connected) { return "Connecting to Tensamin"; } @@ -620,9 +640,13 @@ export function Provider(props: { } return "Loading"; - }, [connected, identified, identifying, readyState]); + }, [connected, identified, identifying, readyState, ttpUrl]); const loadingDescription = useMemo(() => { + if (!ttpUrl) { + return "Loading connection details"; + } + if (readyState === READY_STATE.CONNECTING || !connected) { return "Establishing transport channel"; } @@ -632,13 +656,13 @@ export function Provider(props: { } return undefined; - }, [connected, identified, identifying, readyState]); + }, [connected, identified, identifying, readyState, ttpUrl]); if (error !== "" && errorDescription !== "") { return ; } - if (!connected || !identified) { + if (!connected || !identified || !ttpUrl) { return ( Date: Wed, 13 May 2026 18:20:01 +0200 Subject: [PATCH 2/6] (feat): add logout button (qol): update todo --- .../web/src/components/screens/login/form.tsx | 20 +++++++++++++++---- apps/web/src/features/settings/layout.tsx | 5 ++++- apps/web/todo.md | 1 - bun.lock | 4 ++-- package.json | 2 +- 5 files changed, 23 insertions(+), 9 deletions(-) diff --git a/apps/web/src/components/screens/login/form.tsx b/apps/web/src/components/screens/login/form.tsx index 0f297d6..ff76307 100644 --- a/apps/web/src/components/screens/login/form.tsx +++ b/apps/web/src/components/screens/login/form.tsx @@ -64,7 +64,11 @@ function parseTuFileContent(rawFileContent: string): { const rawDomain = userIdString.includes("@") ? userIdString.split("@")[1] : null; - const domain = rawDomain ? (rawDomain.includes(":") ? rawDomain : rawDomain + ":1984") : null; + const domain = rawDomain + ? rawDomain.includes(":") + ? rawDomain + : rawDomain + ":1984" + : null; if (!userId || !privateKey) { throw new Error("Invalid file"); @@ -138,9 +142,17 @@ export default function Form() { } const inputUsername = inputParse.data.username; - const actualUsername = inputUsername.includes("@") ? inputUsername.split("@")[0] : inputUsername; - const rawDomain = inputUsername.includes("@") ? inputUsername.split("@")[1] : null; - const domain = rawDomain ? (rawDomain.includes(":") ? rawDomain : rawDomain + ":1984") : null; + const actualUsername = inputUsername.includes("@") + ? inputUsername.split("@")[0] + : inputUsername; + const rawDomain = inputUsername.includes("@") + ? inputUsername.split("@")[1] + : null; + const domain = rawDomain + ? rawDomain.includes(":") + ? rawDomain + : rawDomain + ":1984" + : null; try { const response = await fetch( diff --git a/apps/web/src/features/settings/layout.tsx b/apps/web/src/features/settings/layout.tsx index fa27444..36d366a 100644 --- a/apps/web/src/features/settings/layout.tsx +++ b/apps/web/src/features/settings/layout.tsx @@ -1,4 +1,4 @@ -import { Button } from "@tensamin/ui"; +import { Button, ClearStorageButton } from "@tensamin/ui"; import options from "@tensamin/shared/settings"; import { cn, useIsMobile } from "@tensamin/ui"; @@ -69,6 +69,9 @@ export function SettingsSidebar() { ))} ))} +
+ +
); } diff --git a/apps/web/todo.md b/apps/web/todo.md index dd80800..790b489 100644 --- a/apps/web/todo.md +++ b/apps/web/todo.md @@ -1,2 +1 @@ - Make the sidebar use to make it feel less slow on mobile -- Add logout button diff --git a/bun.lock b/bun.lock index 5b4b4cd..325be20 100644 --- a/bun.lock +++ b/bun.lock @@ -259,7 +259,7 @@ }, "overrides": { "@tensamin/ttp-core": "https://git.methanium.net/tensamin/ttp/archive/0.0.17.tar.gz", - "@tensamin/ui": "https://git.methanium.net/tensamin/ui/archive/0.0.31.tar.gz", + "@tensamin/ui": "https://git.methanium.net/tensamin/ui/archive/0.0.34.tar.gz", }, "packages": { "@antfu/ni": ["@antfu/ni@25.0.0", "", { "dependencies": { "ansis": "^4.0.0", "fzf": "^0.5.2", "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" }, "bin": { "na": "bin/na.mjs", "ni": "bin/ni.mjs", "nr": "bin/nr.mjs", "nci": "bin/nci.mjs", "nlx": "bin/nlx.mjs", "nun": "bin/nun.mjs", "nup": "bin/nup.mjs" } }, "sha512-9q/yCljni37pkMr4sPrI3G4jqdIk074+iukc5aFJl7kmDCCsiJrbZ6zKxnES1Gwg+i9RcDZwvktl23puGslmvA=="], @@ -700,7 +700,7 @@ "@tensamin/ttp-core": ["@tensamin/ttp-core@https://git.methanium.net/tensamin/ttp/archive/0.0.17.tar.gz", { "dependencies": { "@eslint/js": "^10.0.1", "@typescript-eslint/parser": "^8.59.1", "@webtransport-bun/webtransport": "^0.3.0", "globals": "^17.5.0", "typescript": "^6.0.3", "typescript-eslint": "^8.59.1", "zod": "^4.4.1" } }, "sha512-smyx+04hSnWM1oyWJfJrRWmxu7OInwyHeIlaD1h3tUrkrSxiKWHl1qCmxVO1bC+cDwyUXhJ5HUnNCbx1aWx7Dg=="], - "@tensamin/ui": ["@tensamin/ui@https://git.methanium.net/tensamin/ui/archive/0.0.31.tar.gz", { "dependencies": { "@base-ui/react": "^1.3.0", "@fontsource-variable/inter": "^5.2.6", "@tauri-apps/api": "^2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", "date-fns": "^4.1.0", "embla-carousel-react": "^8.6.0", "input-otp": "^1.4.2", "lucide-react": "^1.8.0", "next-themes": "^0.4.6", "react-day-picker": "^9.14.0", "react-resizable-panels": "^4.10.0", "recharts": "3.8.0", "shadcn": "^3.5.0", "sonner": "^2.0.7", "tailwind-merge": "^3.5.0", "tw-animate-css": "^1.3.0", "vaul": "^1.1.2" }, "peerDependencies": { "react": "^19.2.0", "react-dom": "^19.2.0" } }, "sha512-I2FhsDtR5ElHwCasf6CfFCiyr5T2Cp9MQ2uAhcMnoNZ0tsK0mJhs+Rz9nUDmysydNyqOj8j/uC0CYXnnG1pCxQ=="], + "@tensamin/ui": ["@tensamin/ui@https://git.methanium.net/tensamin/ui/archive/0.0.34.tar.gz", { "dependencies": { "@base-ui/react": "^1.3.0", "@fontsource-variable/inter": "^5.2.6", "@tauri-apps/api": "^2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", "date-fns": "^4.1.0", "embla-carousel-react": "^8.6.0", "input-otp": "^1.4.2", "lucide-react": "^1.8.0", "next-themes": "^0.4.6", "react-day-picker": "^9.14.0", "react-resizable-panels": "^4.10.0", "recharts": "3.8.0", "shadcn": "^3.5.0", "sonner": "^2.0.7", "tailwind-merge": "^3.5.0", "tw-animate-css": "^1.3.0", "vaul": "^1.1.2" }, "peerDependencies": { "react": "^19.2.0", "react-dom": "^19.2.0" } }, "sha512-hp7rV0a0gfD/rNw9et+PqM1PPkgFC6/Z7eYfza6NIp/m+a8/r5Um+S/8tzBDCgZmQC9Y1sJsjesH7mXZz6Jmuw=="], "@tensamin/user": ["@tensamin/user@workspace:packages/user"], diff --git a/package.json b/package.json index 40e19a8..7f969e8 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "jsonc-parser": "^3.3.1" }, "overrides": { - "@tensamin/ui": "https://git.methanium.net/tensamin/ui/archive/0.0.31.tar.gz", + "@tensamin/ui": "https://git.methanium.net/tensamin/ui/archive/0.0.34.tar.gz", "@tensamin/ttp-core": "https://git.methanium.net/tensamin/ttp/archive/0.0.17.tar.gz" }, "dependencies": { From 075964a449a31fc77d2333a597ba47d9b5909d0c Mon Sep 17 00:00:00 2001 From: Alois Date: Thu, 14 May 2026 11:35:51 +0200 Subject: [PATCH 3/6] (feat): improved mobile sidebar UX (feat): update workflows for better releases --- .forgejo/workflows/deploy-dev.yml | 84 ++++++++++++++++++++++++++++- .forgejo/workflows/deploy-prod.yml | 5 +- apps/web/src/components/sidebar.tsx | 41 ++++++++++++-- 3 files changed, 122 insertions(+), 8 deletions(-) diff --git a/.forgejo/workflows/deploy-dev.yml b/.forgejo/workflows/deploy-dev.yml index 26053ff..19d5a78 100644 --- a/.forgejo/workflows/deploy-dev.yml +++ b/.forgejo/workflows/deploy-dev.yml @@ -10,18 +10,98 @@ jobs: steps: - name: Check out repo uses: https://data.forgejo.org/actions/checkout@v4 + with: + fetch-depth: 0 - - name: Setup Bun + - name: Install Packages + run: apt-get update && apt-get install -y sudo curl jq + + - name: Install Nix + uses: https://github.com/cachix/install-nix-action@v30 + + - name: Install Bun uses: oven-sh/setup-bun@v2 - name: Install dependencies run: bun install --frozen-lockfile + - name: Setup Android Keystore + env: + KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} + KEYSTORE_PROPERTIES: ${{ secrets.ANDROID_KEYSTORE_PROPERTIES }} + run: | + bun -e "require('fs').writeFileSync('keystore.jks', Buffer.from(process.env.KEYSTORE_BASE64.replace(/\s+/g, ''), 'base64'))" + bun -e "const content = process.env.KEYSTORE_PROPERTIES.replace(/\\\\n/g, '\n').replace(/\\r/g, '').split('\n').map(l => l.trim()).filter(l => l).join('\n'); require('fs').writeFileSync('keystore.properties', content)" + - name: Build - run: bun run build:web + run: bun run build:apps - name: Install rsync run: apt-get update && apt-get install -y rsync - name: Deploy run: rsync -a --delete apps/web/dist/ /var/lib/www/tensamin-web-dev/ + + - name: Read version and hash + id: version + run: | + VERSION="$(node -p "require('./package.json').version")" + SHORT_SHA="$(git rev-parse --short HEAD)" + echo "version=$VERSION" >> "$FORGEJO_OUTPUT" + echo "short_sha=$SHORT_SHA" >> "$FORGEJO_OUTPUT" + echo "tag=dev-${VERSION}-${SHORT_SHA}" >> "$FORGEJO_OUTPUT" + echo "title=${VERSION}-dev-${SHORT_SHA}" >> "$FORGEJO_OUTPUT" + + - name: Create pre-release and upload files + env: + TOKEN: ${{ forgejo.token }} + API: ${{ forgejo.api_url }} + REPO: ${{ forgejo.repository }} + SHA: ${{ forgejo.sha }} + TAG: ${{ steps.version.outputs.tag }} + TITLE: ${{ steps.version.outputs.title }} + run: | + set -eu + + test -d releases + find releases -type f | grep -q . + + LATEST_PROD_TAG="$(git tag --list 'v*' --sort=-v:refname | head -n 1 || true)" + if [ -n "$LATEST_PROD_TAG" ]; then + BODY="$(git log "$LATEST_PROD_TAG"..HEAD --pretty=format:'- %s')" + else + BODY="$(git log --pretty=format:'- %s')" + fi + + HTTP_STATUS=$(curl -s -w "%{http_code}" -o release_out.json -H "Authorization: token $TOKEN" "$API/repos/$REPO/releases/tags/$TAG") + + if [ "$HTTP_STATUS" = "200" ]; then + echo "Release $TAG already exists." + RELEASE_ID="$(jq -r .id release_out.json)" + else + echo "Creating new pre-release for $TAG" + RELEASE_JSON="$(curl -f -sS -X POST "$API/repos/$REPO/releases" \ + -H "Authorization: token $TOKEN" \ + -H "Content-Type: application/json" \ + -d "$(jq -n \ + --arg tag "$TAG" \ + --arg name "$TITLE" \ + --arg body "$BODY" \ + --arg target "$SHA" \ + '{ + tag_name: $tag, + name: $name, + body: $body, + target_commitish: $target, + draft: false, + prerelease: true + }')")" + RELEASE_ID="$(echo "$RELEASE_JSON" | jq -r .id)" + fi + + find releases -type f -print0 | while IFS= read -r -d '' file; do + name="$(basename "$file")" + curl -fsS -X POST "$API/repos/$REPO/releases/$RELEASE_ID/assets?name=$name" \ + -H "Authorization: token $TOKEN" \ + -F "attachment=@$file" + done diff --git a/.forgejo/workflows/deploy-prod.yml b/.forgejo/workflows/deploy-prod.yml index 1df5b60..c2f5c62 100644 --- a/.forgejo/workflows/deploy-prod.yml +++ b/.forgejo/workflows/deploy-prod.yml @@ -54,13 +54,14 @@ jobs: REPO: ${{ forgejo.repository }} SHA: ${{ forgejo.sha }} TAG: ${{ steps.version.outputs.tag }} - VERSION: ${{ steps.version.outputs.version }} run: | set -eu test -d releases find releases -type f | grep -q . + COMMIT_MSG="$(git log -1 --pretty=%B)" + HTTP_STATUS=$(curl -s -w "%{http_code}" -o release_out.json -H "Authorization: token $TOKEN" "$API/repos/$REPO/releases/tags/$TAG") if [ "$HTTP_STATUS" = "200" ]; then @@ -74,7 +75,7 @@ jobs: -d "$(jq -n \ --arg tag "$TAG" \ --arg name "$TAG" \ - --arg body "Release $VERSION" \ + --arg body "$COMMIT_MSG" \ --arg target "$SHA" \ '{ tag_name: $tag, diff --git a/apps/web/src/components/sidebar.tsx b/apps/web/src/components/sidebar.tsx index ce9bb6b..6b4b6cd 100644 --- a/apps/web/src/components/sidebar.tsx +++ b/apps/web/src/components/sidebar.tsx @@ -6,6 +6,7 @@ import { Sidebar as SidebarRoot, SidebarContent, SidebarFooter, + useSidebar, } from "@tensamin/ui"; import { isTauri } from "@tauri-apps/api/core"; import { useIsMobile } from "@tensamin/ui"; @@ -13,17 +14,21 @@ import { MobileNavbar } from "./navbar"; import SidebarBox from "@tensamin/call/sidebarBox"; import { useShowMobileNavbar } from "@/routes/app/layout"; - /** * Renders the conversation sidebar with account summary and conversation list. + * On mobile the sidebar is always kept in the DOM and hidden via CSS + * (opacity + translateX) instead of being unmounted. This keeps the DOM and + * React state alive while the drawer is closed, allowing the sidebar to open + * instantly on subsequent toggles. * @returns Sidebar JSX. */ export default function Sidebar() { const isMobile = useIsMobile(); const showMobileNavbar = useShowMobileNavbar(); + const { openMobile, setOpenMobile } = useSidebar(); - return ( - + const content = ( + <> )} - + ); + + if (isMobile) { + return ( + <> + {openMobile && ( +
setOpenMobile(false)} + /> + )} +
+
{content}
+
+ + ); + } + + return {content}; } From 9f83f1e1b7c3c62116f513596bf940d2720148ac Mon Sep 17 00:00:00 2001 From: Alois Date: Thu, 14 May 2026 12:18:18 +0200 Subject: [PATCH 4/6] (fix): release description formatting --- .forgejo/workflows/deploy-dev.yml | 4 +-- .forgejo/workflows/deploy-prod.yml | 42 +++++++++++++++--------------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/.forgejo/workflows/deploy-dev.yml b/.forgejo/workflows/deploy-dev.yml index 19d5a78..191af8a 100644 --- a/.forgejo/workflows/deploy-dev.yml +++ b/.forgejo/workflows/deploy-dev.yml @@ -68,9 +68,9 @@ jobs: LATEST_PROD_TAG="$(git tag --list 'v*' --sort=-v:refname | head -n 1 || true)" if [ -n "$LATEST_PROD_TAG" ]; then - BODY="$(git log "$LATEST_PROD_TAG"..HEAD --pretty=format:'- %s')" + BODY="$(git log "$LATEST_PROD_TAG"..HEAD --pretty=format:'- %s' | sed 's/$/
/')" else - BODY="$(git log --pretty=format:'- %s')" + BODY="$(git log --pretty=format:'- %s' | sed 's/$/
/')" fi HTTP_STATUS=$(curl -s -w "%{http_code}" -o release_out.json -H "Authorization: token $TOKEN" "$API/repos/$REPO/releases/tags/$TAG") diff --git a/.forgejo/workflows/deploy-prod.yml b/.forgejo/workflows/deploy-prod.yml index c2f5c62..782d096 100644 --- a/.forgejo/workflows/deploy-prod.yml +++ b/.forgejo/workflows/deploy-prod.yml @@ -60,34 +60,34 @@ jobs: test -d releases find releases -type f | grep -q . - COMMIT_MSG="$(git log -1 --pretty=%B)" + COMMIT_MSG="$(git log -1 --pretty=%B | sed 's/$/
/')" HTTP_STATUS=$(curl -s -w "%{http_code}" -o release_out.json -H "Authorization: token $TOKEN" "$API/repos/$REPO/releases/tags/$TAG") if [ "$HTTP_STATUS" = "200" ]; then echo "Release $TAG already exists." - RELEASE_ID="$(jq -r .id release_out.json)" - else - echo "Creating new release for $TAG" - RELEASE_JSON="$(curl -f -sS -X POST "$API/repos/$REPO/releases" \ - -H "Authorization: token $TOKEN" \ - -H "Content-Type: application/json" \ - -d "$(jq -n \ - --arg tag "$TAG" \ - --arg name "$TAG" \ - --arg body "$COMMIT_MSG" \ - --arg target "$SHA" \ - '{ - tag_name: $tag, - name: $name, - body: $body, - target_commitish: $target, - draft: false, - prerelease: false - }')")" - RELEASE_ID="$(echo "$RELEASE_JSON" | jq -r .id)" + exit 0 fi + echo "Creating new release for $TAG" + RELEASE_JSON="$(curl -f -sS -X POST "$API/repos/$REPO/releases" \ + -H "Authorization: token $TOKEN" \ + -H "Content-Type: application/json" \ + -d "$(jq -n \ + --arg tag "$TAG" \ + --arg name "$TAG" \ + --arg body "$COMMIT_MSG" \ + --arg target "$SHA" \ + '{ + tag_name: $tag, + name: $name, + body: $body, + target_commitish: $target, + draft: false, + prerelease: false + }')")" + RELEASE_ID="$(echo "$RELEASE_JSON" | jq -r .id)" + find releases -type f -print0 | while IFS= read -r -d '' file; do name="$(basename "$file")" curl -fsS -X POST "$API/repos/$REPO/releases/$RELEASE_ID/assets?name=$name" \ From a1af7ce25f1feec1136d052861f31553ffc5498c Mon Sep 17 00:00:00 2001 From: Alois Date: Thu, 14 May 2026 13:15:59 +0200 Subject: [PATCH 5/6] (fix): workflow formatting --- .forgejo/workflows/deploy-dev.yml | 11 ++++++++--- .forgejo/workflows/deploy-prod.yml | 4 ++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/.forgejo/workflows/deploy-dev.yml b/.forgejo/workflows/deploy-dev.yml index 191af8a..000dcd6 100644 --- a/.forgejo/workflows/deploy-dev.yml +++ b/.forgejo/workflows/deploy-dev.yml @@ -49,7 +49,7 @@ jobs: SHORT_SHA="$(git rev-parse --short HEAD)" echo "version=$VERSION" >> "$FORGEJO_OUTPUT" echo "short_sha=$SHORT_SHA" >> "$FORGEJO_OUTPUT" - echo "tag=dev-${VERSION}-${SHORT_SHA}" >> "$FORGEJO_OUTPUT" + echo "tag=${VERSION}-dev-${SHORT_SHA}" >> "$FORGEJO_OUTPUT" echo "title=${VERSION}-dev-${SHORT_SHA}" >> "$FORGEJO_OUTPUT" - name: Create pre-release and upload files @@ -68,9 +68,14 @@ jobs: LATEST_PROD_TAG="$(git tag --list 'v*' --sort=-v:refname | head -n 1 || true)" if [ -n "$LATEST_PROD_TAG" ]; then - BODY="$(git log "$LATEST_PROD_TAG"..HEAD --pretty=format:'- %s' | sed 's/$/
/')" + LOG="$(git log "$LATEST_PROD_TAG"..HEAD --pretty=format:'- %s')" + SERVER_URL="$(echo "$API" | sed 's|/api/v1.*||')" + COMPARE_URL="${SERVER_URL}/${REPO}/compare/${LATEST_PROD_TAG}...${TAG}" + BODY="${LOG} + +[View changes](${COMPARE_URL})" else - BODY="$(git log --pretty=format:'- %s' | sed 's/$/
/')" + BODY="$(git log --pretty=format:'- %s')" fi HTTP_STATUS=$(curl -s -w "%{http_code}" -o release_out.json -H "Authorization: token $TOKEN" "$API/repos/$REPO/releases/tags/$TAG") diff --git a/.forgejo/workflows/deploy-prod.yml b/.forgejo/workflows/deploy-prod.yml index 782d096..8207e6a 100644 --- a/.forgejo/workflows/deploy-prod.yml +++ b/.forgejo/workflows/deploy-prod.yml @@ -45,7 +45,7 @@ jobs: run: | VERSION="$(node -p "require('./package.json').version")" echo "version=$VERSION" >> "$FORGEJO_OUTPUT" - echo "tag=v$VERSION" >> "$FORGEJO_OUTPUT" + echo "tag=${VERSION}-prod" >> "$FORGEJO_OUTPUT" - name: Create release and upload files env: @@ -60,7 +60,7 @@ jobs: test -d releases find releases -type f | grep -q . - COMMIT_MSG="$(git log -1 --pretty=%B | sed 's/$/
/')" + COMMIT_MSG="$(git log -1 --pretty=%B | sed 's/$/ /')" HTTP_STATUS=$(curl -s -w "%{http_code}" -o release_out.json -H "Authorization: token $TOKEN" "$API/repos/$REPO/releases/tags/$TAG") From 52918265ea452ea30fe32429898ac2663badf05f Mon Sep 17 00:00:00 2001 From: Alois Date: Thu, 14 May 2026 13:19:15 +0200 Subject: [PATCH 6/6] (fix): workflow --- .forgejo/workflows/deploy-dev.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.forgejo/workflows/deploy-dev.yml b/.forgejo/workflows/deploy-dev.yml index 000dcd6..1a022c2 100644 --- a/.forgejo/workflows/deploy-dev.yml +++ b/.forgejo/workflows/deploy-dev.yml @@ -71,9 +71,7 @@ jobs: LOG="$(git log "$LATEST_PROD_TAG"..HEAD --pretty=format:'- %s')" SERVER_URL="$(echo "$API" | sed 's|/api/v1.*||')" COMPARE_URL="${SERVER_URL}/${REPO}/compare/${LATEST_PROD_TAG}...${TAG}" - BODY="${LOG} - -[View changes](${COMPARE_URL})" + BODY="$(printf '%s\n\n[View changes](%s)' "$LOG" "$COMPARE_URL")" else BODY="$(git log --pretty=format:'- %s')" fi