feat: Gitea Actions CI/CD, Docker registry images, drive drag-and-drop
Build & Release / Docker / backend (push) Failing after 1m31s
Build & Release / Docker / frontend (push) Failing after 1m42s
Build & Release / Deploy to server (push) Has been cancelled
Build & Release / Create release (push) Has been cancelled
Build & Release / Desktop / Linux & Windows (push) Has been cancelled
Build & Release / Docker / backend (push) Failing after 1m31s
Build & Release / Docker / frontend (push) Failing after 1m42s
Build & Release / Deploy to server (push) Has been cancelled
Build & Release / Create release (push) Has been cancelled
Build & Release / Desktop / Linux & Windows (push) Has been cancelled
- Add .gitea/workflows/docker-build.yml: builds backend and frontend
Docker images on every push, pushes to Gitea Container Registry
(git.necloud.us/nate/cloudlink/{backend,frontend}:latest + SHA tag)
- Add desktop build job: Linux AppImage and Windows NSIS installer via
Wine cross-compilation, uploaded as workflow artifacts
- Add release job: creates a Gitea release with installers attached when
a v* tag is pushed
- Add deploy job: SSH pull + restart on pushes to master
- docker-compose.yml: add image: fields alongside build: so the server
can pull pre-built images with docker compose pull
- DrivePage: add drag-and-drop upload with full-screen drop overlay;
refactor upload logic into a shared uploadFile callback
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
9b57dd9b94
commit
2823b8e10e
3 files changed
+213
-7
No files matched your search
@@ -0,0 +1,179 @@
|
|||||||
|
name: Build & Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [master, main]
|
||||||
|
tags: ['v*']
|
||||||
|
pull_request:
|
||||||
|
branches: [master, main]
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: git.necloud.us
|
||||||
|
OWNER: nate
|
||||||
|
REPO: cloudlink
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# ─── Docker: Backend ────────────────────────────────────────────────────────
|
||||||
|
build-backend:
|
||||||
|
name: Docker / backend
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Log in to registry
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ secrets.REGISTRY_USER }}
|
||||||
|
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||||
|
|
||||||
|
- uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: ./backend
|
||||||
|
file: ./backend/Dockerfile
|
||||||
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
|
tags: |
|
||||||
|
${{ env.REGISTRY }}/${{ env.OWNER }}/${{ env.REPO }}/backend:latest
|
||||||
|
${{ env.REGISTRY }}/${{ env.OWNER }}/${{ env.REPO }}/backend:${{ github.sha }}
|
||||||
|
cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.OWNER }}/${{ env.REPO }}/backend:cache
|
||||||
|
cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.OWNER }}/${{ env.REPO }}/backend:cache,mode=max
|
||||||
|
|
||||||
|
# ─── Docker: Frontend ───────────────────────────────────────────────────────
|
||||||
|
build-frontend:
|
||||||
|
name: Docker / frontend
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Log in to registry
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ secrets.REGISTRY_USER }}
|
||||||
|
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||||
|
|
||||||
|
- uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: ./frontend
|
||||||
|
file: ./frontend/Dockerfile
|
||||||
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
|
build-args: |
|
||||||
|
VITE_API_URL=/api
|
||||||
|
VITE_WS_URL=/ws
|
||||||
|
tags: |
|
||||||
|
${{ env.REGISTRY }}/${{ env.OWNER }}/${{ env.REPO }}/frontend:latest
|
||||||
|
${{ env.REGISTRY }}/${{ env.OWNER }}/${{ env.REPO }}/frontend:${{ github.sha }}
|
||||||
|
cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.OWNER }}/${{ env.REPO }}/frontend:cache
|
||||||
|
cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.OWNER }}/${{ env.REPO }}/frontend:cache,mode=max
|
||||||
|
|
||||||
|
# ─── Desktop installers (Linux + Windows, built on Linux runner) ─────────────
|
||||||
|
build-desktop:
|
||||||
|
name: Desktop / Linux & Windows
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
cache: 'npm'
|
||||||
|
cache-dependency-path: desktop/package-lock.json
|
||||||
|
|
||||||
|
- name: Install desktop dependencies
|
||||||
|
working-directory: desktop
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Install Wine (Windows cross-compile)
|
||||||
|
run: |
|
||||||
|
sudo dpkg --add-architecture i386
|
||||||
|
sudo apt-get update -q
|
||||||
|
sudo apt-get install -y --no-install-recommends wine wine32 wine64
|
||||||
|
|
||||||
|
- name: Build Linux AppImage
|
||||||
|
working-directory: desktop
|
||||||
|
env:
|
||||||
|
# electron-builder reads this to avoid interactive prompts
|
||||||
|
GH_TOKEN: unused
|
||||||
|
run: npx electron-builder --linux AppImage --x64
|
||||||
|
|
||||||
|
- name: Build Windows NSIS installer
|
||||||
|
working-directory: desktop
|
||||||
|
env:
|
||||||
|
GH_TOKEN: unused
|
||||||
|
run: npx electron-builder --win nsis --x64
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: desktop-linux-windows-${{ github.sha }}
|
||||||
|
path: |
|
||||||
|
desktop/dist/*.AppImage
|
||||||
|
desktop/dist/*.exe
|
||||||
|
if-no-files-found: warn
|
||||||
|
retention-days: 30
|
||||||
|
|
||||||
|
# ─── Deploy (on push to master only, not PRs or tags) ───────────────────────
|
||||||
|
deploy:
|
||||||
|
name: Deploy to server
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [build-backend, build-frontend]
|
||||||
|
if: github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main')
|
||||||
|
steps:
|
||||||
|
- name: SSH deploy
|
||||||
|
uses: appleboy/ssh-action@v1
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.DEPLOY_HOST }}
|
||||||
|
username: ${{ secrets.DEPLOY_USER }}
|
||||||
|
key: ${{ secrets.DEPLOY_SSH_KEY }}
|
||||||
|
script: |
|
||||||
|
cd ${{ secrets.DEPLOY_PATH }}
|
||||||
|
echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.necloud.us -u "${{ secrets.REGISTRY_USER }}" --password-stdin
|
||||||
|
docker compose pull backend frontend
|
||||||
|
docker compose up -d --no-build backend frontend
|
||||||
|
docker logout git.necloud.us
|
||||||
|
docker image prune -f
|
||||||
|
|
||||||
|
# ─── Release (only on v* tags) ───────────────────────────────────────────────
|
||||||
|
release:
|
||||||
|
name: Create release
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [build-desktop, build-backend, build-frontend]
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
|
steps:
|
||||||
|
- name: Download desktop artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: desktop-linux-windows-${{ github.sha }}
|
||||||
|
path: ./release-assets
|
||||||
|
|
||||||
|
- name: Publish Gitea release
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||||
|
TAG: ${{ github.ref_name }}
|
||||||
|
SHA: ${{ github.sha }}
|
||||||
|
run: |
|
||||||
|
# Create the release
|
||||||
|
RELEASE=$(curl -sf -X POST \
|
||||||
|
-H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"https://git.necloud.us/api/v1/repos/nate/cloudlink/releases" \
|
||||||
|
-d "{\"tag_name\":\"$TAG\",\"name\":\"CloudLink $TAG\",\"body\":\"Docker images pushed to registry. Desktop installers attached below.\"}")
|
||||||
|
|
||||||
|
RELEASE_ID=$(echo "$RELEASE" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
|
||||||
|
echo "Release ID: $RELEASE_ID"
|
||||||
|
|
||||||
|
# Upload each installer as a release asset
|
||||||
|
for file in ./release-assets/*; do
|
||||||
|
NAME=$(basename "$file")
|
||||||
|
echo "Uploading $NAME..."
|
||||||
|
curl -sf -X POST \
|
||||||
|
-H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
-H "Content-Type: application/octet-stream" \
|
||||||
|
"https://git.necloud.us/api/v1/repos/nate/cloudlink/releases/$RELEASE_ID/assets?name=$NAME" \
|
||||||
|
--data-binary @"$file"
|
||||||
|
done
|
||||||
@@ -20,6 +20,7 @@ services:
|
|||||||
retries: 5
|
retries: 5
|
||||||
|
|
||||||
backend:
|
backend:
|
||||||
|
image: git.necloud.us/nate/cloudlink/backend:latest
|
||||||
build:
|
build:
|
||||||
context: ./backend
|
context: ./backend
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
@@ -39,6 +40,7 @@ services:
|
|||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
|
image: git.necloud.us/nate/cloudlink/frontend:latest
|
||||||
build:
|
build:
|
||||||
context: ./frontend
|
context: ./frontend
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useRef, useState, useEffect } from 'react';
|
import { useRef, useState, useEffect, useCallback } from 'react';
|
||||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import { Upload, File, Trash2, Download, Lock } from 'lucide-react';
|
import { Upload, File, Trash2, Download, Lock } from 'lucide-react';
|
||||||
import { formatDistanceToNow } from 'date-fns';
|
import { formatDistanceToNow } from 'date-fns';
|
||||||
@@ -34,6 +34,7 @@ export default function DrivePage() {
|
|||||||
const qc = useQueryClient();
|
const qc = useQueryClient();
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
const [uploading, setUploading] = useState(false);
|
const [uploading, setUploading] = useState(false);
|
||||||
|
const [dragOver, setDragOver] = useState(false);
|
||||||
const [decryptedNames, setDecryptedNames] = useState<Record<string, string>>({});
|
const [decryptedNames, setDecryptedNames] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
const { data: files = [] } = useQuery<DriveFile[]>({
|
const { data: files = [] } = useQuery<DriveFile[]>({
|
||||||
@@ -60,11 +61,9 @@ export default function DrivePage() {
|
|||||||
})();
|
})();
|
||||||
}, [files, masterKey]);
|
}, [files, masterKey]);
|
||||||
|
|
||||||
async function handleUpload(e: React.ChangeEvent<HTMLInputElement>) {
|
const uploadFile = useCallback(async (file: File) => {
|
||||||
const file = e.target.files?.[0];
|
if (!masterKey) return;
|
||||||
if (!file || !masterKey) return;
|
|
||||||
setUploading(true);
|
setUploading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const fileKey = await generateFileKey();
|
const fileKey = await generateFileKey();
|
||||||
const encryptedBlob = await encryptFile(file, fileKey);
|
const encryptedBlob = await encryptFile(file, fileKey);
|
||||||
@@ -83,6 +82,18 @@ export default function DrivePage() {
|
|||||||
setUploading(false);
|
setUploading(false);
|
||||||
if (inputRef.current) inputRef.current.value = '';
|
if (inputRef.current) inputRef.current.value = '';
|
||||||
}
|
}
|
||||||
|
}, [masterKey, qc]);
|
||||||
|
|
||||||
|
function handleUpload(e: React.ChangeEvent<HTMLInputElement>) {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (file) uploadFile(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDrop(e: React.DragEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
setDragOver(false);
|
||||||
|
const file = e.dataTransfer.files[0];
|
||||||
|
if (file) uploadFile(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleDownload(f: DriveFile) {
|
async function handleDownload(f: DriveFile) {
|
||||||
@@ -125,11 +136,25 @@ export default function DrivePage() {
|
|||||||
<input ref={inputRef} type="file" className="hidden" onChange={handleUpload} />
|
<input ref={inputRef} type="file" className="hidden" onChange={handleUpload} />
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto p-6">
|
<div
|
||||||
|
className="flex-1 overflow-y-auto p-6"
|
||||||
|
onDragOver={(e) => { e.preventDefault(); setDragOver(true); }}
|
||||||
|
onDragLeave={() => setDragOver(false)}
|
||||||
|
onDrop={handleDrop}
|
||||||
|
>
|
||||||
|
{dragOver && (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 pointer-events-none">
|
||||||
|
<div className="flex flex-col items-center gap-3 bg-surface-1 border-2 border-dashed border-accent rounded-2xl px-16 py-12">
|
||||||
|
<Upload className="w-12 h-12 text-accent" />
|
||||||
|
<p className="text-lg font-semibold">Drop to encrypt & upload</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{files.length === 0 ? (
|
{files.length === 0 ? (
|
||||||
<div className="flex flex-col items-center justify-center h-48 text-slate-500 gap-3">
|
<div className="flex flex-col items-center justify-center h-48 text-slate-500 gap-3">
|
||||||
<File className="w-10 h-10" />
|
<File className="w-10 h-10" />
|
||||||
<p>No files yet — upload your first encrypted file</p>
|
<p>No files yet — drag & drop or click Upload</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-1 gap-2">
|
<div className="grid grid-cols-1 gap-2">
|
||||||
|
|||||||
Reference in new issue
Block a user