Compare commits
5 Commits
2b9c5d0248
...
0ca694ca99
| Author | SHA1 | Date | |
|---|---|---|---|
| 0ca694ca99 | |||
| d8f90c7d6c | |||
| b86f02699e | |||
| d1800b4888 | |||
| 2defb7c02f |
133
CICD_SETUP.md
Normal file
133
CICD_SETUP.md
Normal file
@@ -0,0 +1,133 @@
|
||||
# CI/CD Setup — Gitea Actions (Auto Deploy)
|
||||
|
||||
This doc covers how to set up automatic deployment to your production server whenever you push to `main`. The deploy runs `deploy.sh` (`git pull && docker-compose down && docker-compose up -d --build`).
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### On Gitea
|
||||
- Gitea Actions must be enabled (check Site Administration → Configuration)
|
||||
- At least one Actions runner must be registered and online
|
||||
|
||||
### On the Production Server
|
||||
- Docker and docker-compose installed
|
||||
- The repo already cloned at a known path
|
||||
- SSH access configured
|
||||
|
||||
---
|
||||
|
||||
## Step 1 — Install a Gitea Actions Runner
|
||||
|
||||
If you don't already have a runner:
|
||||
|
||||
1. Download the runner binary from your Gitea instance or from the Gitea releases page
|
||||
2. Register it:
|
||||
```
|
||||
./act_runner register --instance https://your-gitea-url --token <runner-token>
|
||||
```
|
||||
Get the token from: Gitea → Site Administration → Runners → Create Runner
|
||||
3. Start the runner:
|
||||
```
|
||||
./act_runner daemon
|
||||
```
|
||||
Consider running it as a systemd service so it survives reboots.
|
||||
|
||||
---
|
||||
|
||||
## Step 2 — Set Up SSH Key for Deployment
|
||||
|
||||
On your local machine or CI machine, generate a dedicated deploy key:
|
||||
|
||||
```bash
|
||||
ssh-keygen -t ed25519 -C "gitea-deploy" -f ~/.ssh/gitea_deploy
|
||||
```
|
||||
|
||||
Copy the **public key** (`gitea_deploy.pub`) to your production server:
|
||||
|
||||
```bash
|
||||
ssh-copy-id -i ~/.ssh/gitea_deploy.pub user@your-server
|
||||
```
|
||||
|
||||
Or manually append it to `~/.ssh/authorized_keys` on the server.
|
||||
|
||||
---
|
||||
|
||||
## Step 3 — Add Secrets in Gitea
|
||||
|
||||
Go to: your repo → Settings → Secrets → Add Secret
|
||||
|
||||
| Secret Name | Value |
|
||||
|------------------|--------------------------------------------|
|
||||
| `DEPLOY_HOST` | IP address or hostname of your server |
|
||||
| `DEPLOY_USER` | SSH username (e.g. `ubuntu`, `root`) |
|
||||
| `DEPLOY_SSH_KEY` | Full contents of the **private** key file |
|
||||
| `DEPLOY_PORT` | SSH port (default: `22`) |
|
||||
|
||||
---
|
||||
|
||||
## Step 4 — Create the Workflow File
|
||||
|
||||
Create `.gitea/workflows/deploy.yml` in the repo root:
|
||||
|
||||
```yaml
|
||||
name: Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Deploy via SSH
|
||||
uses: appleboy/ssh-action@v1.0.3
|
||||
with:
|
||||
host: ${{ secrets.DEPLOY_HOST }}
|
||||
username: ${{ secrets.DEPLOY_USER }}
|
||||
key: ${{ secrets.DEPLOY_SSH_KEY }}
|
||||
port: ${{ secrets.DEPLOY_PORT }}
|
||||
script: |
|
||||
cd /path/to/grateful-journal # <-- update this path
|
||||
bash deploy.sh
|
||||
```
|
||||
|
||||
Update the `cd` path to wherever the repo lives on your server.
|
||||
|
||||
---
|
||||
|
||||
## Alternative — Runner Running Directly on the Server
|
||||
|
||||
If your Gitea Actions runner is already installed on the production server itself, you can skip SSH entirely and simplify the workflow:
|
||||
|
||||
```yaml
|
||||
name: Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Run deploy script
|
||||
run: |
|
||||
cd /path/to/grateful-journal # <-- update this path
|
||||
bash deploy.sh
|
||||
```
|
||||
|
||||
This is simpler and avoids managing SSH keys.
|
||||
|
||||
---
|
||||
|
||||
## Verifying It Works
|
||||
|
||||
1. Push a commit to `main`
|
||||
2. Go to your repo → Actions tab in Gitea
|
||||
3. You should see the workflow run and each step's log output
|
||||
|
||||
If the runner isn't picking up jobs, check that the runner is online in Site Administration → Runners.
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 26 KiB |
12
src/App.css
12
src/App.css
@@ -860,12 +860,11 @@
|
||||
}
|
||||
|
||||
.bottom-nav-avatar {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
.bottom-nav-avatar-placeholder {
|
||||
@@ -879,7 +878,8 @@
|
||||
}
|
||||
|
||||
.bottom-nav-btn-active .bottom-nav-avatar {
|
||||
border-color: currentColor;
|
||||
outline: 2px solid currentColor;
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
.bottom-nav-btn span {
|
||||
@@ -2167,8 +2167,8 @@
|
||||
}
|
||||
|
||||
.bottom-nav-avatar {
|
||||
width: 31px;
|
||||
height: 31px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
/* Active pill keeps green but full-width */
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useNavigate, useLocation } from 'react-router-dom'
|
||||
import { useState } from 'react'
|
||||
import { useAuth } from '../contexts/AuthContext'
|
||||
|
||||
export default function BottomNav() {
|
||||
@@ -6,7 +7,9 @@ export default function BottomNav() {
|
||||
const location = useLocation()
|
||||
const { user, mongoUser } = useAuth()
|
||||
const displayName = mongoUser?.displayName || user?.displayName || 'U'
|
||||
const photoURL = (mongoUser && 'photoURL' in mongoUser) ? (mongoUser.photoURL || null) : (user?.photoURL || null)
|
||||
const mongoPhoto = mongoUser && 'photoURL' in mongoUser ? mongoUser.photoURL : null
|
||||
const photoURL = (mongoPhoto?.startsWith('data:')) ? mongoPhoto : (user?.photoURL || null)
|
||||
const [imgError, setImgError] = useState(false)
|
||||
|
||||
const isActive = (path: string) => location.pathname === path
|
||||
|
||||
@@ -54,8 +57,8 @@ export default function BottomNav() {
|
||||
onClick={() => navigate('/settings')}
|
||||
aria-label="Settings"
|
||||
>
|
||||
{photoURL ? (
|
||||
<img src={photoURL} alt={displayName} className="bottom-nav-avatar" />
|
||||
{photoURL && !imgError ? (
|
||||
<img src={photoURL} alt={displayName} className="bottom-nav-avatar" onError={() => setImgError(true)} />
|
||||
) : (
|
||||
<div className="bottom-nav-avatar bottom-nav-avatar-placeholder">
|
||||
{displayName.charAt(0).toUpperCase()}
|
||||
|
||||
@@ -270,9 +270,7 @@ export default function HistoryPage() {
|
||||
</h3>
|
||||
|
||||
{loadingEntries ? (
|
||||
<p style={{ color: 'var(--color-text-muted)', fontSize: '0.875rem', textAlign: 'center', padding: '1.5rem 0', fontFamily: '"Sniglet", system-ui' }}>
|
||||
Loading entries…
|
||||
</p>
|
||||
<PageLoader />
|
||||
) : (
|
||||
<div className="entries-list">
|
||||
{selectedDateEntries.length === 0 ? (
|
||||
|
||||
@@ -77,8 +77,9 @@ export default function SettingsPage() {
|
||||
}
|
||||
|
||||
const displayName = mongoUser?.displayName || user?.displayName || 'User'
|
||||
// Prefer mongo photo; only fall back to Google photo if mongo has no photo set
|
||||
const photoURL = (mongoUser && 'photoURL' in mongoUser) ? (mongoUser.photoURL || null) : (user?.photoURL || null)
|
||||
// Use custom uploaded photo (base64) if set, otherwise always use Firebase's fresh Google URL
|
||||
const mongoPhoto = mongoUser && 'photoURL' in mongoUser ? mongoUser.photoURL : null
|
||||
const photoURL = (mongoPhoto?.startsWith('data:')) ? mongoPhoto : (user?.photoURL || null)
|
||||
|
||||
const openEditModal = () => {
|
||||
setEditName(displayName)
|
||||
@@ -206,9 +207,11 @@ export default function SettingsPage() {
|
||||
<main className="settings-container">
|
||||
{/* Profile Section */}
|
||||
<div id="tour-edit-profile" className="settings-profile">
|
||||
<div className="settings-avatar">
|
||||
<div className="settings-avatar" onClick={openEditModal} style={{ cursor: 'pointer' }}>
|
||||
{photoURL ? (
|
||||
<img src={photoURL} alt={displayName} className="settings-avatar-img" />
|
||||
<img src={photoURL} alt={displayName} className="settings-avatar-img"
|
||||
onError={(e) => { (e.target as HTMLImageElement).style.display = 'none'; }}
|
||||
/>
|
||||
) : (
|
||||
<div className="settings-avatar-placeholder" style={{ fontSize: '1.75rem' }}>
|
||||
{displayName.charAt(0).toUpperCase()}
|
||||
@@ -465,9 +468,18 @@ export default function SettingsPage() {
|
||||
<div className="confirm-modal" onClick={(e) => e.stopPropagation()}>
|
||||
<h3 className="edit-modal-title">Edit Profile</h3>
|
||||
|
||||
<div className="edit-modal-avatar" onClick={() => fileInputRef.current?.click()}>
|
||||
<label className="edit-modal-avatar" style={{ cursor: 'pointer' }}>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
style={{ display: 'none' }}
|
||||
onChange={handlePhotoSelect}
|
||||
/>
|
||||
{editPhotoPreview ? (
|
||||
<img src={editPhotoPreview} alt="Preview" className="edit-modal-avatar-img" />
|
||||
<img src={editPhotoPreview} alt="Preview" className="edit-modal-avatar-img"
|
||||
onError={(e) => { (e.target as HTMLImageElement).style.display = 'none' }}
|
||||
/>
|
||||
) : (
|
||||
<div className="edit-modal-avatar-placeholder">
|
||||
{editName.charAt(0).toUpperCase() || 'U'}
|
||||
@@ -479,14 +491,7 @@ export default function SettingsPage() {
|
||||
<circle cx="12" cy="13" r="4" />
|
||||
</svg>
|
||||
</div>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
style={{ display: 'none' }}
|
||||
onChange={handlePhotoSelect}
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
{editPhotoPreview && (
|
||||
<button
|
||||
type="button"
|
||||
|
||||
Reference in New Issue
Block a user