mirror of
https://github.com/ashim-hq/ashim
synced 2026-04-21 13:37:52 +00:00
feat: add semantic-release for automated versioning and help dialog
- Set up semantic-release with zero-touch CI pipeline on push to main - Add version sync script to keep all package.json files and APP_VERSION constant in sync automatically - Consolidate Docker publishing into single tag-triggered workflow that pushes to both Docker Hub and ghcr.io with semver tags - Add help dialog with keyboard shortcuts, getting started guide, and resource links - Sync all versions to 0.2.1 to match Docker Hub latest
This commit is contained in:
parent
fb1d366d5f
commit
4807bd2726
22 changed files with 2359 additions and 82 deletions
42
.github/workflows/ci.yml
vendored
42
.github/workflows/ci.yml
vendored
|
|
@ -45,7 +45,7 @@ jobs:
|
|||
- run: pnpm build
|
||||
|
||||
docker:
|
||||
name: Docker Build
|
||||
name: Docker Build Test
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
steps:
|
||||
|
|
@ -61,43 +61,3 @@ jobs:
|
|||
tags: stirling-image:ci
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
docker-publish:
|
||||
name: Publish Docker Image
|
||||
runs-on: ubuntu-latest
|
||||
needs: docker
|
||||
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: docker/setup-qemu-action@v3
|
||||
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
|
||||
- uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: docker/metadata-action@v5
|
||||
id: meta
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository }}
|
||||
tags: |
|
||||
type=sha
|
||||
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: docker/Dockerfile
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
|
|
|||
23
.github/workflows/docker-publish.yml
vendored
23
.github/workflows/docker-publish.yml
vendored
|
|
@ -2,7 +2,6 @@ name: Build and Push Docker Image
|
|||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
tags: ["v*"]
|
||||
workflow_dispatch:
|
||||
|
||||
|
|
@ -30,22 +29,36 @@ jobs:
|
|||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Extract metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.IMAGE_NAME }}
|
||||
images: |
|
||||
${{ env.IMAGE_NAME }}
|
||||
ghcr.io/${{ github.repository }}
|
||||
tags: |
|
||||
type=raw,value=latest,enable={{is_default_branch}}
|
||||
type=semver,pattern={{version}}
|
||||
type=sha,prefix=
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
type=raw,value=latest
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: docker/Dockerfile
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
|
|
|
|||
40
.github/workflows/release.yml
vendored
Normal file
40
.github/workflows/release.yml
vendored
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Semantic Release
|
||||
runs-on: ubuntu-latest
|
||||
# Skip release commits to avoid infinite loop
|
||||
if: "!contains(github.event.head_commit.message, '[skip ci]')"
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: pnpm
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Run semantic-release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: npx semantic-release
|
||||
44
.releaserc.json
Normal file
44
.releaserc.json
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
{
|
||||
"branches": ["main"],
|
||||
"plugins": [
|
||||
"@semantic-release/commit-analyzer",
|
||||
"@semantic-release/release-notes-generator",
|
||||
[
|
||||
"@semantic-release/changelog",
|
||||
{
|
||||
"changelogFile": "CHANGELOG.md"
|
||||
}
|
||||
],
|
||||
[
|
||||
"@semantic-release/npm",
|
||||
{
|
||||
"npmPublish": false
|
||||
}
|
||||
],
|
||||
[
|
||||
"@semantic-release/exec",
|
||||
{
|
||||
"prepareCmd": "./scripts/sync-version.sh ${nextRelease.version}"
|
||||
}
|
||||
],
|
||||
[
|
||||
"@semantic-release/git",
|
||||
{
|
||||
"assets": [
|
||||
"package.json",
|
||||
"pnpm-lock.yaml",
|
||||
"CHANGELOG.md",
|
||||
"apps/web/package.json",
|
||||
"apps/api/package.json",
|
||||
"apps/docs/package.json",
|
||||
"packages/shared/package.json",
|
||||
"packages/shared/src/constants.ts",
|
||||
"packages/image-engine/package.json",
|
||||
"packages/ai/package.json"
|
||||
],
|
||||
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
|
||||
}
|
||||
],
|
||||
"@semantic-release/github"
|
||||
]
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@stirling-image/api",
|
||||
"version": "0.0.1",
|
||||
"version": "0.2.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -401,8 +401,22 @@ export async function authMiddleware(app: FastifyInstance): Promise<void> {
|
|||
app.addHook(
|
||||
"preHandler",
|
||||
async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
// Skip if auth is disabled
|
||||
if (!env.AUTH_ENABLED) return;
|
||||
// When auth is disabled, attach the first admin user so requireAuth/requireAdmin pass
|
||||
if (!env.AUTH_ENABLED) {
|
||||
const adminUser = db
|
||||
.select()
|
||||
.from(schema.users)
|
||||
.where(eq(schema.users.role, "admin"))
|
||||
.get();
|
||||
if (adminUser) {
|
||||
(request as FastifyRequest & { user?: AuthUser }).user = {
|
||||
id: adminUser.id,
|
||||
username: adminUser.username,
|
||||
role: "admin",
|
||||
};
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const isPublic = isPublicRoute(request.url);
|
||||
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ export function registerRemoveBackground(app: FastifyInstance) {
|
|||
const resultBuffer = await removeBackground(
|
||||
fileBuffer,
|
||||
join(workspacePath, "output"),
|
||||
{ model: settings.model },
|
||||
{ model: settings.model, backgroundColor: settings.backgroundColor },
|
||||
);
|
||||
|
||||
// Save output
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@stirling-image/docs",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"docs:dev": "vitepress dev .",
|
||||
|
|
@ -11,4 +11,4 @@
|
|||
"tsx": "^4.19.0",
|
||||
"vitepress": "^1.1.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@stirling-image/web",
|
||||
"version": "0.0.1",
|
||||
"version": "0.2.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { ToolPanel } from "./tool-panel";
|
|||
import { Footer } from "./footer";
|
||||
import { Dropzone } from "../common/dropzone";
|
||||
import { SettingsDialog } from "../settings/settings-dialog";
|
||||
import { HelpDialog } from "../help/help-dialog";
|
||||
import { useMobile } from "@/hooks/use-mobile";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
|
|
@ -24,6 +25,7 @@ interface AppLayoutProps {
|
|||
|
||||
export function AppLayout({ children, showToolPanel = true, onFiles }: AppLayoutProps) {
|
||||
const [settingsOpen, setSettingsOpen] = useState(false);
|
||||
const [helpOpen, setHelpOpen] = useState(false);
|
||||
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
|
||||
const isMobile = useMobile();
|
||||
|
||||
|
|
@ -31,7 +33,7 @@ export function AppLayout({ children, showToolPanel = true, onFiles }: AppLayout
|
|||
<div className="flex h-screen bg-background text-foreground overflow-hidden">
|
||||
{/* Desktop sidebar */}
|
||||
{!isMobile && (
|
||||
<Sidebar onSettingsClick={() => setSettingsOpen(true)} />
|
||||
<Sidebar onSettingsClick={() => setSettingsOpen(true)} onHelpClick={() => setHelpOpen(true)} />
|
||||
)}
|
||||
|
||||
{/* Mobile sidebar overlay */}
|
||||
|
|
@ -59,6 +61,10 @@ export function AppLayout({ children, showToolPanel = true, onFiles }: AppLayout
|
|||
setMobileSidebarOpen(false);
|
||||
setSettingsOpen(true);
|
||||
}}
|
||||
onHelpClick={() => {
|
||||
setMobileSidebarOpen(false);
|
||||
setHelpOpen(true);
|
||||
}}
|
||||
expanded
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -122,6 +128,12 @@ export function AppLayout({ children, showToolPanel = true, onFiles }: AppLayout
|
|||
open={settingsOpen}
|
||||
onClose={() => setSettingsOpen(false)}
|
||||
/>
|
||||
|
||||
{/* Help dialog */}
|
||||
<HelpDialog
|
||||
open={helpOpen}
|
||||
onClose={() => setHelpOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,17 +25,18 @@ const topItems: SidebarItem[] = [
|
|||
];
|
||||
|
||||
const bottomItems: SidebarItem[] = [
|
||||
{ icon: HelpCircle, label: "Help", href: "/help" },
|
||||
{ icon: HelpCircle, label: "Help" },
|
||||
{ icon: Settings, label: "Settings" },
|
||||
];
|
||||
|
||||
interface SidebarProps {
|
||||
onSettingsClick: () => void;
|
||||
onHelpClick: () => void;
|
||||
/** When true, renders in expanded mode (for mobile overlay). */
|
||||
expanded?: boolean;
|
||||
}
|
||||
|
||||
export function Sidebar({ onSettingsClick, expanded = false }: SidebarProps) {
|
||||
export function Sidebar({ onSettingsClick, onHelpClick, expanded = false }: SidebarProps) {
|
||||
const location = useLocation();
|
||||
|
||||
const renderItem = (item: SidebarItem, isActive: boolean) => {
|
||||
|
|
@ -72,6 +73,13 @@ export function Sidebar({ onSettingsClick, expanded = false }: SidebarProps) {
|
|||
</button>
|
||||
);
|
||||
}
|
||||
if (item.label === "Help") {
|
||||
return (
|
||||
<button key={item.label} onClick={onHelpClick} className="w-full">
|
||||
{content}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Link key={item.label} to={item.href || "/"}>
|
||||
{content}
|
||||
|
|
|
|||
15
package.json
15
package.json
|
|
@ -1,18 +1,31 @@
|
|||
{
|
||||
"name": "stirling-image",
|
||||
"version": "0.2.1",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@9.15.4",
|
||||
"scripts": {
|
||||
"dev": "turbo dev",
|
||||
"docs:dev": "pnpm --filter @stirling-image/docs docs:dev",
|
||||
"build": "turbo build",
|
||||
"lint": "turbo lint",
|
||||
"clean": "turbo clean",
|
||||
"typecheck": "turbo typecheck",
|
||||
"test:e2e": "playwright test",
|
||||
"test:e2e:ui": "playwright test --ui"
|
||||
"test:e2e:ui": "playwright test --ui",
|
||||
"version:sync": "./scripts/sync-version.sh",
|
||||
"release": "semantic-release",
|
||||
"release:dry": "semantic-release --dry-run"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.58.2",
|
||||
"@semantic-release/changelog": "^6.0.3",
|
||||
"@semantic-release/commit-analyzer": "^13.0.1",
|
||||
"@semantic-release/exec": "^7.1.0",
|
||||
"@semantic-release/git": "^10.0.1",
|
||||
"@semantic-release/github": "^12.0.6",
|
||||
"@semantic-release/npm": "^13.1.5",
|
||||
"@semantic-release/release-notes-generator": "^14.1.0",
|
||||
"semantic-release": "^25.0.3",
|
||||
"turbo": "^2.4.0",
|
||||
"typescript": "^5.7.0"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@stirling-image/ai",
|
||||
"version": "0.0.1",
|
||||
"version": "0.2.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
|
|
|
|||
|
|
@ -68,10 +68,8 @@ def main():
|
|||
}
|
||||
)
|
||||
)
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(json.dumps({"success": False, "error": str(e)}))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { runPythonScript } from "./bridge.js";
|
||||
import { writeFile, readFile } from "node:fs/promises";
|
||||
import { writeFile, readFile, unlink } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { randomUUID } from "node:crypto";
|
||||
|
||||
export interface RemoveBackgroundOptions {
|
||||
model?: string;
|
||||
|
|
@ -12,22 +14,30 @@ export async function removeBackground(
|
|||
outputDir: string,
|
||||
options: RemoveBackgroundOptions = {},
|
||||
): Promise<Buffer> {
|
||||
const inputPath = join(outputDir, "input_bg.png");
|
||||
const outputPath = join(outputDir, "output_bg.png");
|
||||
const id = randomUUID();
|
||||
const inputPath = join(tmpdir(), `rembg_in_${id}.png`);
|
||||
const outputPath = join(outputDir, `rembg_out_${id}.png`);
|
||||
|
||||
await writeFile(inputPath, inputBuffer);
|
||||
// BiRefNet models need longer timeout (up to 10 min for first load)
|
||||
const timeout = options.model?.startsWith("birefnet") ? 600000 : 300000;
|
||||
const { stdout } = await runPythonScript("remove_bg.py", [
|
||||
inputPath,
|
||||
outputPath,
|
||||
JSON.stringify(options),
|
||||
], timeout);
|
||||
try {
|
||||
// BiRefNet models need longer timeout (up to 10 min for first load)
|
||||
const timeout = options.model?.startsWith("birefnet") ? 600000 : 300000;
|
||||
const { stdout } = await runPythonScript("remove_bg.py", [
|
||||
inputPath,
|
||||
outputPath,
|
||||
JSON.stringify(options),
|
||||
], timeout);
|
||||
|
||||
const result = JSON.parse(stdout);
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || "Background removal failed");
|
||||
const result = JSON.parse(stdout);
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || "Background removal failed");
|
||||
}
|
||||
|
||||
const outputBuffer = await readFile(outputPath);
|
||||
return outputBuffer;
|
||||
} finally {
|
||||
// Clean up temp files
|
||||
await unlink(inputPath).catch(() => {});
|
||||
await unlink(outputPath).catch(() => {});
|
||||
}
|
||||
|
||||
return readFile(outputPath);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,8 +25,8 @@ function extractPythonError(error: unknown): string {
|
|||
stdout?: string;
|
||||
message?: string;
|
||||
};
|
||||
// Try to parse JSON error from stderr or stdout
|
||||
for (const output of [execError.stderr, execError.stdout]) {
|
||||
// Try stdout first (Python scripts write JSON errors there), then stderr
|
||||
for (const output of [execError.stdout, execError.stderr]) {
|
||||
if (output) {
|
||||
try {
|
||||
const parsed = JSON.parse(output.trim());
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
export const AI_VERSION = "0.0.1";
|
||||
|
||||
export { runPythonScript } from "./bridge.js";
|
||||
export { removeBackground } from "./background-removal.js";
|
||||
export type { RemoveBackgroundOptions } from "./background-removal.js";
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@stirling-image/image-engine",
|
||||
"version": "0.0.1",
|
||||
"version": "0.2.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@stirling-image/shared",
|
||||
"version": "0.0.1",
|
||||
"version": "0.2.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
|
|
|
|||
|
|
@ -100,4 +100,4 @@ export const SUPPORTED_OUTPUT_FORMATS = [
|
|||
|
||||
export const DEFAULT_OUTPUT_FORMAT = "jpg" as const;
|
||||
|
||||
export const APP_VERSION = "0.1.0";
|
||||
export const APP_VERSION = "0.2.1";
|
||||
|
|
|
|||
2120
pnpm-lock.yaml
2120
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
47
scripts/sync-version.sh
Executable file
47
scripts/sync-version.sh
Executable file
|
|
@ -0,0 +1,47 @@
|
|||
#!/usr/bin/env bash
|
||||
# Syncs the version from semantic-release to all workspace package.json files
|
||||
# and the APP_VERSION constant in shared/constants.ts.
|
||||
#
|
||||
# Usage: ./scripts/sync-version.sh <version>
|
||||
# Example: ./scripts/sync-version.sh 1.2.3
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
VERSION="${1:?Usage: sync-version.sh <version>}"
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
|
||||
# All workspace package.json files to sync
|
||||
PACKAGES=(
|
||||
"apps/web/package.json"
|
||||
"apps/api/package.json"
|
||||
"apps/docs/package.json"
|
||||
"packages/shared/package.json"
|
||||
"packages/image-engine/package.json"
|
||||
"packages/ai/package.json"
|
||||
)
|
||||
|
||||
for pkg in "${PACKAGES[@]}"; do
|
||||
FILE="$ROOT/$pkg"
|
||||
if [ -f "$FILE" ]; then
|
||||
# Use node to update JSON cleanly (preserves formatting better than sed)
|
||||
node -e "
|
||||
const fs = require('fs');
|
||||
const path = '$FILE';
|
||||
const raw = fs.readFileSync(path, 'utf8');
|
||||
const json = JSON.parse(raw);
|
||||
json.version = '$VERSION';
|
||||
fs.writeFileSync(path, JSON.stringify(json, null, 2) + '\n');
|
||||
"
|
||||
echo " Updated $pkg -> $VERSION"
|
||||
fi
|
||||
done
|
||||
|
||||
# Update APP_VERSION in shared constants
|
||||
CONSTANTS="$ROOT/packages/shared/src/constants.ts"
|
||||
if [ -f "$CONSTANTS" ]; then
|
||||
sed -i.bak "s/export const APP_VERSION = \".*\"/export const APP_VERSION = \"$VERSION\"/" "$CONSTANTS"
|
||||
rm -f "$CONSTANTS.bak"
|
||||
echo " Updated APP_VERSION -> $VERSION"
|
||||
fi
|
||||
|
||||
echo "All versions synced to $VERSION"
|
||||
Loading…
Reference in a new issue