Version 1.1
This commit is contained in:
16
.env.example
Normal file
16
.env.example
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# Copy to .env and adjust as needed.
|
||||||
|
|
||||||
|
# Port exposed on the host
|
||||||
|
WEB_PORT=5000
|
||||||
|
|
||||||
|
# Flask environment (development/production)
|
||||||
|
FLASK_ENV=production
|
||||||
|
|
||||||
|
# These are read by the app (see app.py) and are also used by docker-compose.yml.
|
||||||
|
SECRET_KEY=change-me
|
||||||
|
# Store the SQLite DB in the Flask instance folder
|
||||||
|
SQLALCHEMY_DATABASE_URI=sqlite:////instance/liturgie.db
|
||||||
|
|
||||||
|
# Default admin bootstrap (created only if the user does not exist yet)
|
||||||
|
ADMIN_USERNAME=admin
|
||||||
|
ADMIN_PASSWORD=admin
|
||||||
33
.gitignore
vendored
Normal file
33
.gitignore
vendored
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
### Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
.Python
|
||||||
|
*.egg-info/
|
||||||
|
.eggs/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
|
||||||
|
### Virtual environments
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
|
||||||
|
### IDEs
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
### OS
|
||||||
|
Thumbs.db
|
||||||
|
Desktop.ini
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
### Secrets / local env
|
||||||
|
.env
|
||||||
|
|
||||||
|
### App data (don’t commit runtime db/uploads)
|
||||||
|
instance/*.db
|
||||||
|
static/uploads/*
|
||||||
|
!static/uploads/readme.txt
|
||||||
82
README.md
82
README.md
@@ -0,0 +1,82 @@
|
|||||||
|
# Psalmbord Online
|
||||||
|
|
||||||
|
## Run with Docker Compose
|
||||||
|
|
||||||
|
This project includes a `Dockerfile` + `docker-compose.yml` to run the Flask app behind gunicorn.
|
||||||
|
|
||||||
|
### Configure environment (optional)
|
||||||
|
|
||||||
|
Copy the example env file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
### Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
Open:
|
||||||
|
|
||||||
|
- http://localhost:5000 (or `WEB_PORT`)
|
||||||
|
|
||||||
|
### Persisted data
|
||||||
|
|
||||||
|
Docker compose bind-mounts the following so data survives container rebuilds/recreates:
|
||||||
|
|
||||||
|
- `./instance/` -> SQLite database file (stored at `instance/liturgie.db`)
|
||||||
|
- `./static/uploads/` -> uploaded backgrounds/logos
|
||||||
|
|
||||||
|
### Default admin user
|
||||||
|
|
||||||
|
On startup, `init_db.py` ensures DB/tables exist and creates an admin user **only if it does not already exist**.
|
||||||
|
|
||||||
|
Defaults:
|
||||||
|
|
||||||
|
- Username: `admin`
|
||||||
|
- Password: `admin`
|
||||||
|
|
||||||
|
Override via `.env`:
|
||||||
|
|
||||||
|
```env
|
||||||
|
ADMIN_USERNAME=admin
|
||||||
|
ADMIN_PASSWORD=change-me
|
||||||
|
```
|
||||||
|
|
||||||
|
## Release / publish (git + docker push)
|
||||||
|
|
||||||
|
This repo includes helper scripts that:
|
||||||
|
|
||||||
|
1. Prompt for a version (e.g. `1.2.3`)
|
||||||
|
2. Create a git commit with message `Version <version>`
|
||||||
|
3. Push to: `https://git.alphen.cloud/bramval/PsalmbordOnlineCE`
|
||||||
|
4. Build + push Docker image to:
|
||||||
|
- `git.alphen.cloud/bramval/psalmbordonlinece:<version>`
|
||||||
|
- `git.alphen.cloud/bramval/psalmbordonlinece:latest`
|
||||||
|
|
||||||
|
### Recommended (cross-platform Python)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python release.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Windows (PowerShell)
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
./release.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Linux / macOS (bash)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x ./release.sh
|
||||||
|
./release.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
If Docker push fails due to authentication, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker login git.alphen.cloud
|
||||||
|
```
|
||||||
|
|||||||
20
app.py
20
app.py
@@ -12,8 +12,18 @@ UPLOAD_FOLDER = 'static/uploads'
|
|||||||
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}
|
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.config['SECRET_KEY'] = 'your_secret_key_here'
|
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'your_secret_key_here')
|
||||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///liturgie.db'
|
|
||||||
|
# Prefer an env override, otherwise store the SQLite DB in Flask's `instance/` folder.
|
||||||
|
# This keeps the DB out of the repo root and makes Docker persistence easier.
|
||||||
|
default_db_path = os.path.join(app.instance_path, 'liturgie.db')
|
||||||
|
app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get(
|
||||||
|
'SQLALCHEMY_DATABASE_URI',
|
||||||
|
f"sqlite:///{default_db_path}",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ensure the instance folder exists so SQLite can create/open the DB file.
|
||||||
|
os.makedirs(app.instance_path, exist_ok=True)
|
||||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||||
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
|
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
|
||||||
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
|
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
|
||||||
@@ -672,7 +682,11 @@ def change_password():
|
|||||||
return render_template('change_password.html', msg=msg)
|
return render_template('change_password.html', msg=msg)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
if not os.path.exists('liturgie.db'):
|
# Ensure instance folder exists when running without Docker.
|
||||||
|
os.makedirs(app.instance_path, exist_ok=True)
|
||||||
|
|
||||||
|
# Create DB on first run when using the default instance DB path.
|
||||||
|
if app.config['SQLALCHEMY_DATABASE_URI'].startswith('sqlite:///') and not os.path.exists(default_db_path):
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
db.create_all()
|
db.create_all()
|
||||||
# Create initial admin user if not exists
|
# Create initial admin user if not exists
|
||||||
|
|||||||
35
docker-compose.yml
Normal file
35
docker-compose.yml
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
services:
|
||||||
|
web:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
container_name: psalmbordonline-web
|
||||||
|
ports:
|
||||||
|
# Host:Container
|
||||||
|
- "${WEB_PORT:-5000}:5000"
|
||||||
|
environment:
|
||||||
|
# Flask
|
||||||
|
- FLASK_ENV=${FLASK_ENV:-production}
|
||||||
|
# The app reads these from env (with sensible defaults in app.py)
|
||||||
|
- SECRET_KEY=${SECRET_KEY:-change-me}
|
||||||
|
# Use an absolute path inside the container. Note the 4 slashes for sqlite.
|
||||||
|
- SQLALCHEMY_DATABASE_URI=${SQLALCHEMY_DATABASE_URI:-sqlite:////instance/liturgie.db}
|
||||||
|
# Default admin bootstrap (only created if not existing)
|
||||||
|
- ADMIN_USERNAME=${ADMIN_USERNAME:-admin}
|
||||||
|
- ADMIN_PASSWORD=${ADMIN_PASSWORD:-admin}
|
||||||
|
command:
|
||||||
|
# Ensure the SQLite DB + tables exist when running under gunicorn (the __main__
|
||||||
|
# block in app.py does not run under gunicorn).
|
||||||
|
- sh
|
||||||
|
- -c
|
||||||
|
- "python init_db.py && gunicorn wsgi:app -b 0.0.0.0:5000"
|
||||||
|
volumes:
|
||||||
|
# Persist SQLite DB (stored under Flask's instance folder)
|
||||||
|
- ./instance:/instance
|
||||||
|
# Persist uploaded images/backgrounds
|
||||||
|
- ./static/uploads:/static/uploads
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
42
init_db.py
Normal file
42
init_db.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
"""Container startup helper.
|
||||||
|
|
||||||
|
When the app is started via gunicorn, the `if __name__ == '__main__'` block in
|
||||||
|
app.py is not executed, so the SQLite DB/tables/admin user may not be created.
|
||||||
|
|
||||||
|
This script makes startup idempotent by ensuring tables exist and creating a
|
||||||
|
default admin user if missing.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from werkzeug.security import generate_password_hash
|
||||||
|
|
||||||
|
from app import app, db, User, Church
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
admin_username = os.environ.get("ADMIN_USERNAME", "admin")
|
||||||
|
admin_password = os.environ.get("ADMIN_PASSWORD", "admin")
|
||||||
|
|
||||||
|
with app.app_context():
|
||||||
|
db.create_all()
|
||||||
|
|
||||||
|
if not User.query.filter_by(username=admin_username).first():
|
||||||
|
admin_church = Church.query.filter_by(name="Admin").first()
|
||||||
|
if not admin_church:
|
||||||
|
admin_church = Church(name="Admin")
|
||||||
|
db.session.add(admin_church)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
admin_user = User(
|
||||||
|
username=admin_username,
|
||||||
|
password=generate_password_hash(admin_password),
|
||||||
|
church_id=admin_church.id,
|
||||||
|
is_admin=True,
|
||||||
|
)
|
||||||
|
db.session.add(admin_user)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
87
release.ps1
Normal file
87
release.ps1
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
function Ensure-Success($exitCode, $what) {
|
||||||
|
if ($exitCode -ne 0) {
|
||||||
|
throw "Failed: $what (exit code $exitCode)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-VersionFromUser {
|
||||||
|
$version = Read-Host "Enter version (e.g. 1.2.3)"
|
||||||
|
$version = $version.Trim()
|
||||||
|
if ([string]::IsNullOrWhiteSpace($version)) {
|
||||||
|
throw "Version cannot be empty"
|
||||||
|
}
|
||||||
|
# Basic docker tag safety: allow only common characters
|
||||||
|
if ($version -notmatch '^[0-9A-Za-z][0-9A-Za-z._-]{0,127}$') {
|
||||||
|
throw "Invalid version '$version'. Use only letters/numbers and . _ - (max 128 chars)"
|
||||||
|
}
|
||||||
|
return $version
|
||||||
|
}
|
||||||
|
|
||||||
|
$TargetGitUrl = "https://git.alphen.cloud/bramval/PsalmbordOnlineCE.git"
|
||||||
|
$DockerImage = "git.alphen.cloud/bramval/psalmbordonlinece"
|
||||||
|
|
||||||
|
$gitCmd = Get-Command git -ErrorAction SilentlyContinue
|
||||||
|
if (-not $gitCmd) { throw "git is not available in PATH" }
|
||||||
|
$dockerCmd = Get-Command docker -ErrorAction SilentlyContinue
|
||||||
|
if (-not $dockerCmd) { throw "docker is not available in PATH" }
|
||||||
|
|
||||||
|
$version = Get-VersionFromUser
|
||||||
|
$commitMessage = "Version $version"
|
||||||
|
|
||||||
|
$branch = (git branch --show-current).Trim()
|
||||||
|
if ($branch -ne 'main') {
|
||||||
|
Write-Host "You are on branch '$branch' (expected 'main')." -ForegroundColor Yellow
|
||||||
|
$ans = Read-Host "Continue and push 'main' anyway? (y/N)"
|
||||||
|
if ($ans.ToLower() -ne 'y') { throw "Aborted by user" }
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "\n== Git: ensuring remote 'origin' points to $TargetGitUrl ==" -ForegroundColor Cyan
|
||||||
|
$originUrl = (git remote get-url origin 2>$null)
|
||||||
|
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($originUrl)) {
|
||||||
|
Write-Host "Remote 'origin' not found; adding it." -ForegroundColor Yellow
|
||||||
|
git remote add origin $TargetGitUrl
|
||||||
|
Ensure-Success $LASTEXITCODE "git remote add"
|
||||||
|
} elseif ($originUrl -ne $TargetGitUrl) {
|
||||||
|
Write-Host "Remote 'origin' is '$originUrl' -> updating to '$TargetGitUrl'" -ForegroundColor Yellow
|
||||||
|
git remote set-url origin $TargetGitUrl
|
||||||
|
Ensure-Success $LASTEXITCODE "git remote set-url"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "\n== Git: status ==" -ForegroundColor Cyan
|
||||||
|
git status -sb
|
||||||
|
|
||||||
|
$confirm = Read-Host "Proceed with commit+push and docker push? (y/N)"
|
||||||
|
if ($confirm.ToLower() -ne 'y') { throw "Aborted by user" }
|
||||||
|
|
||||||
|
Write-Host "\n== Git: add/commit/push ==" -ForegroundColor Cyan
|
||||||
|
git add -A
|
||||||
|
Ensure-Success $LASTEXITCODE "git add"
|
||||||
|
|
||||||
|
# Commit might fail if nothing to commit; handle gracefully
|
||||||
|
git commit -m $commitMessage
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Write-Host "No changes to commit (or commit failed). Continuing to push anyway..." -ForegroundColor Yellow
|
||||||
|
} else {
|
||||||
|
Write-Host "Committed: $commitMessage" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
|
||||||
|
git push -u origin main
|
||||||
|
Ensure-Success $LASTEXITCODE "git push"
|
||||||
|
|
||||||
|
Write-Host "\n== Docker: build/tag/push ==" -ForegroundColor Cyan
|
||||||
|
Write-Host "Docker image: $DockerImage" -ForegroundColor Gray
|
||||||
|
Write-Host "Tags: $version, latest" -ForegroundColor Gray
|
||||||
|
|
||||||
|
docker build -t "${DockerImage}:${version}" -t "${DockerImage}:latest" .
|
||||||
|
Ensure-Success $LASTEXITCODE "docker build"
|
||||||
|
|
||||||
|
docker push "${DockerImage}:${version}"
|
||||||
|
Ensure-Success $LASTEXITCODE "docker push (version)"
|
||||||
|
|
||||||
|
docker push "${DockerImage}:latest"
|
||||||
|
Ensure-Success $LASTEXITCODE "docker push (latest)"
|
||||||
|
|
||||||
|
Write-Host "\nDone." -ForegroundColor Green
|
||||||
|
Write-Host "If docker push failed due to auth, run: docker login git.alphen.cloud" -ForegroundColor Yellow
|
||||||
153
release.py
Normal file
153
release.py
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Release helper: git commit/push + docker build/push.
|
||||||
|
|
||||||
|
Flow:
|
||||||
|
1) Prompt for version
|
||||||
|
2) git add -A
|
||||||
|
3) git commit -m "Version <version>" (skips if nothing to commit)
|
||||||
|
4) git push -u origin main (ensures origin points to target repo)
|
||||||
|
5) docker build + push tags: <version> and latest
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Config:
|
||||||
|
target_git_url: str = "https://git.alphen.cloud/bramval/PsalmbordOnlineCE.git"
|
||||||
|
docker_image: str = "git.alphen.cloud/bramval/psalmbordonlinece"
|
||||||
|
branch: str = "main"
|
||||||
|
|
||||||
|
|
||||||
|
VERSION_RE = re.compile(r"^[0-9A-Za-z][0-9A-Za-z._-]{0,127}$")
|
||||||
|
|
||||||
|
|
||||||
|
def run(cmd: list[str], *, check: bool = True) -> subprocess.CompletedProcess[str]:
|
||||||
|
"""Run command and stream output; return CompletedProcess."""
|
||||||
|
print(f"\n$ {' '.join(cmd)}")
|
||||||
|
return subprocess.run(cmd, text=True, check=check)
|
||||||
|
|
||||||
|
|
||||||
|
def capture(cmd: list[str]) -> str:
|
||||||
|
"""Run command and capture stdout."""
|
||||||
|
return subprocess.check_output(cmd, text=True).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_tools() -> None:
|
||||||
|
for tool in ("git", "docker"):
|
||||||
|
if shutil.which(tool) is None:
|
||||||
|
raise SystemExit(f"Error: '{tool}' is not available in PATH")
|
||||||
|
|
||||||
|
|
||||||
|
def prompt_version() -> str:
|
||||||
|
version = input("Enter version (e.g. 1.2.3): ").strip()
|
||||||
|
if not version:
|
||||||
|
raise SystemExit("Error: version cannot be empty")
|
||||||
|
if not VERSION_RE.match(version):
|
||||||
|
raise SystemExit(
|
||||||
|
"Error: invalid version. Use only letters/numbers and . _ - (max 128 chars)"
|
||||||
|
)
|
||||||
|
return version
|
||||||
|
|
||||||
|
|
||||||
|
def prompt_yes_no(question: str, default_no: bool = True) -> bool:
|
||||||
|
prompt = "(y/N)" if default_no else "(Y/n)"
|
||||||
|
ans = input(f"{question} {prompt}: ").strip().lower()
|
||||||
|
if not ans:
|
||||||
|
return not default_no
|
||||||
|
return ans in ("y", "yes")
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_origin(config: Config) -> None:
|
||||||
|
try:
|
||||||
|
origin_url = capture(["git", "remote", "get-url", "origin"])
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
origin_url = ""
|
||||||
|
|
||||||
|
if not origin_url:
|
||||||
|
print(f"Remote 'origin' not found; adding {config.target_git_url}")
|
||||||
|
run(["git", "remote", "add", "origin", config.target_git_url])
|
||||||
|
return
|
||||||
|
|
||||||
|
if origin_url != config.target_git_url:
|
||||||
|
print(
|
||||||
|
f"Remote 'origin' is '{origin_url}' -> updating to '{config.target_git_url}'"
|
||||||
|
)
|
||||||
|
run(["git", "remote", "set-url", "origin", config.target_git_url])
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
config = Config()
|
||||||
|
|
||||||
|
ensure_tools()
|
||||||
|
version = prompt_version()
|
||||||
|
commit_message = f"Version {version}"
|
||||||
|
|
||||||
|
# Branch warning
|
||||||
|
try:
|
||||||
|
current_branch = capture(["git", "branch", "--show-current"])
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
current_branch = ""
|
||||||
|
if current_branch and current_branch != config.branch:
|
||||||
|
print(
|
||||||
|
f"Warning: you are on branch '{current_branch}' (expected '{config.branch}')."
|
||||||
|
)
|
||||||
|
if not prompt_yes_no(f"Continue and push '{config.branch}' anyway?", default_no=True):
|
||||||
|
return 1
|
||||||
|
|
||||||
|
ensure_origin(config)
|
||||||
|
|
||||||
|
print("\n== Git: status ==")
|
||||||
|
run(["git", "status", "-sb"], check=False)
|
||||||
|
|
||||||
|
if not prompt_yes_no("Proceed with commit+push and docker push?", default_no=True):
|
||||||
|
print("Aborted.")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
print("\n== Git: add/commit/push ==")
|
||||||
|
run(["git", "add", "-A"])
|
||||||
|
|
||||||
|
# commit may fail if nothing to commit
|
||||||
|
try:
|
||||||
|
run(["git", "commit", "-m", commit_message])
|
||||||
|
print(f"Committed: {commit_message}")
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
print("No changes to commit (or commit failed). Continuing...")
|
||||||
|
|
||||||
|
run(["git", "push", "-u", "origin", config.branch])
|
||||||
|
|
||||||
|
print("\n== Docker: build/tag/push ==")
|
||||||
|
print(f"Docker image: {config.docker_image}")
|
||||||
|
print(f"Tags: {version}, latest")
|
||||||
|
|
||||||
|
run(
|
||||||
|
[
|
||||||
|
"docker",
|
||||||
|
"build",
|
||||||
|
"-t",
|
||||||
|
f"{config.docker_image}:{version}",
|
||||||
|
"-t",
|
||||||
|
f"{config.docker_image}:latest",
|
||||||
|
".",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
run(["docker", "push", f"{config.docker_image}:{version}"])
|
||||||
|
run(["docker", "push", f"{config.docker_image}:latest"])
|
||||||
|
|
||||||
|
print("\nDone.")
|
||||||
|
print("If docker push failed due to auth, run: docker login git.alphen.cloud")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Make Ctrl+C exit nicely
|
||||||
|
try:
|
||||||
|
raise SystemExit(main())
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\nAborted (Ctrl+C).")
|
||||||
|
raise SystemExit(130)
|
||||||
86
release.sh
Normal file
86
release.sh
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
TARGET_GIT_URL="https://git.alphen.cloud/bramval/PsalmbordOnlineCE.git"
|
||||||
|
DOCKER_IMAGE="git.alphen.cloud/bramval/psalmbordonlinece"
|
||||||
|
BRANCH="main"
|
||||||
|
|
||||||
|
command -v git >/dev/null 2>&1 || { echo "git is not available in PATH" >&2; exit 1; }
|
||||||
|
command -v docker >/dev/null 2>&1 || { echo "docker is not available in PATH" >&2; exit 1; }
|
||||||
|
|
||||||
|
read -r -p "Enter version (e.g. 1.2.3): " version
|
||||||
|
version="${version//[[:space:]]/}"
|
||||||
|
if [[ -z "$version" ]]; then
|
||||||
|
echo "Version cannot be empty" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Basic docker tag safety
|
||||||
|
if [[ ! "$version" =~ ^[0-9A-Za-z][0-9A-Za-z._-]{0,127}$ ]]; then
|
||||||
|
echo "Invalid version '$version'. Use only letters/numbers and . _ - (max 128 chars)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
commit_message="Version ${version}"
|
||||||
|
|
||||||
|
current_branch="$(git branch --show-current)"
|
||||||
|
if [[ "$current_branch" != "$BRANCH" ]]; then
|
||||||
|
echo "You are on branch '$current_branch' (expected '$BRANCH')." >&2
|
||||||
|
read -r -p "Continue and push '$BRANCH' anyway? (y/N): " ans
|
||||||
|
if [[ "${ans,,}" != "y" ]]; then
|
||||||
|
echo "Aborted by user" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "== Git: ensuring remote 'origin' points to ${TARGET_GIT_URL} =="
|
||||||
|
if git remote get-url origin >/dev/null 2>&1; then
|
||||||
|
origin_url="$(git remote get-url origin)"
|
||||||
|
if [[ "$origin_url" != "$TARGET_GIT_URL" ]]; then
|
||||||
|
echo "Remote 'origin' is '$origin_url' -> updating to '$TARGET_GIT_URL'"
|
||||||
|
git remote set-url origin "$TARGET_GIT_URL"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Remote 'origin' not found; adding it."
|
||||||
|
git remote add origin "$TARGET_GIT_URL"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "== Git: status =="
|
||||||
|
git status -sb
|
||||||
|
|
||||||
|
read -r -p "Proceed with commit+push and docker push? (y/N): " confirm
|
||||||
|
if [[ "${confirm,,}" != "y" ]]; then
|
||||||
|
echo "Aborted by user" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "== Git: add/commit/push =="
|
||||||
|
git add -A
|
||||||
|
|
||||||
|
set +e
|
||||||
|
git commit -m "$commit_message"
|
||||||
|
commit_ec=$?
|
||||||
|
set -e
|
||||||
|
if [[ $commit_ec -ne 0 ]]; then
|
||||||
|
echo "No changes to commit (or commit failed). Continuing to push anyway..." >&2
|
||||||
|
else
|
||||||
|
echo "Committed: $commit_message"
|
||||||
|
fi
|
||||||
|
|
||||||
|
git push -u origin "$BRANCH"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "== Docker: build/tag/push =="
|
||||||
|
echo "Docker image: $DOCKER_IMAGE"
|
||||||
|
echo "Tags: $version, latest"
|
||||||
|
|
||||||
|
docker build -t "${DOCKER_IMAGE}:${version}" -t "${DOCKER_IMAGE}:latest" .
|
||||||
|
docker push "${DOCKER_IMAGE}:${version}"
|
||||||
|
docker push "${DOCKER_IMAGE}:latest"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Done."
|
||||||
|
echo "If docker push failed due to auth, run: docker login git.alphen.cloud" >&2
|
||||||
@@ -263,7 +263,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Contact form at BOTTOM -->
|
<!-- Contact form at BOTTOM
|
||||||
<div class="contact-section" style="max-width:none; margin:3.8rem 0 0 0; width:100%;">
|
<div class="contact-section" style="max-width:none; margin:3.8rem 0 0 0; width:100%;">
|
||||||
<h2>Contacteer ons</h2>
|
<h2>Contacteer ons</h2>
|
||||||
<form method="POST" action="/contact" autocomplete="off">
|
<form method="POST" action="/contact" autocomplete="off">
|
||||||
@@ -334,4 +334,5 @@
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</div>
|
</div>
|
||||||
|
-->
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user