Compare commits
8 Commits
rewrite
...
31d9e388f8
| Author | SHA1 | Date | |
|---|---|---|---|
| 31d9e388f8 | |||
|
|
9e313f5b86 | ||
|
|
c735d7318f | ||
|
|
9c94aa1df5 | ||
|
|
e4b44c7b5e | ||
|
|
1c27b68965 | ||
|
|
083d7dd662 | ||
| bc796f2aa4 |
99
.gitea/workflows/README.md
Normal file
99
.gitea/workflows/README.md
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
# Gitea Actions Workflows
|
||||||
|
|
||||||
|
## Workflows
|
||||||
|
|
||||||
|
### 1. CI (`ci.yml`)
|
||||||
|
**Triggers:** Push/PR to `main`, `rewrite`, `feat` branches
|
||||||
|
|
||||||
|
**Jobs:**
|
||||||
|
- Install dependencies
|
||||||
|
- Lint code
|
||||||
|
- Build application
|
||||||
|
- Run unit tests (Vitest)
|
||||||
|
- Run E2E tests (Playwright)
|
||||||
|
- Upload test results as artifacts
|
||||||
|
|
||||||
|
### 2. Docker Build (`docker-build.yml`)
|
||||||
|
**Triggers:**
|
||||||
|
- Tags matching `v*` (e.g., `v1.0.0`)
|
||||||
|
- Push to `main` branch
|
||||||
|
|
||||||
|
**Jobs:**
|
||||||
|
- Build React application
|
||||||
|
- Build Docker image locally
|
||||||
|
- Save image as tarball artifact
|
||||||
|
- Upload artifact for download (30 days retention)
|
||||||
|
|
||||||
|
**Output:**
|
||||||
|
- Docker image artifact: `configucci.tar`
|
||||||
|
- Can be downloaded and loaded with: `docker load -i configucci.tar`
|
||||||
|
|
||||||
|
### 3. Deploy (`deploy.yml`)
|
||||||
|
**Triggers:** Push to `main` branch
|
||||||
|
|
||||||
|
**Jobs:**
|
||||||
|
- Build React application
|
||||||
|
- Build Docker image locally
|
||||||
|
- Create docker-compose.yml configuration
|
||||||
|
- Deploy container on Gitea runner (port 11088)
|
||||||
|
- Health check to verify application is running
|
||||||
|
- Cleanup old Docker images
|
||||||
|
|
||||||
|
**No SSH required** - Everything runs natively on the Gitea Actions runner!
|
||||||
|
|
||||||
|
**Output:**
|
||||||
|
- Application available at: `http://<gitea-server>:11088`
|
||||||
|
- Container auto-restarts on failure
|
||||||
|
- Health check ensures successful deployment
|
||||||
|
|
||||||
|
## Setup Instructions
|
||||||
|
|
||||||
|
### 1. Enable Gitea Actions
|
||||||
|
Make sure Actions is enabled in your Gitea instance:
|
||||||
|
```ini
|
||||||
|
[actions]
|
||||||
|
ENABLED = true
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Configure Runner
|
||||||
|
Ensure your Gitea runner has Docker and docker-compose installed:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install Docker
|
||||||
|
curl -fsSL https://get.docker.com | sh
|
||||||
|
|
||||||
|
# Install docker-compose
|
||||||
|
sudo apt-get install docker-compose-plugin
|
||||||
|
```
|
||||||
|
|
||||||
|
**No secrets required** - Everything runs on the runner!
|
||||||
|
|
||||||
|
## Workflow Files Location
|
||||||
|
`.gitea/workflows/`
|
||||||
|
|
||||||
|
## Testing Workflows Locally
|
||||||
|
|
||||||
|
You can test workflows locally using [act](https://github.com/nektos/act):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install act
|
||||||
|
brew install act
|
||||||
|
|
||||||
|
# Run CI workflow locally
|
||||||
|
act push
|
||||||
|
|
||||||
|
# Run with specific job
|
||||||
|
act -j build-and-test
|
||||||
|
|
||||||
|
# Run with verbose output
|
||||||
|
act -v
|
||||||
|
```
|
||||||
|
|
||||||
|
## Badge Examples
|
||||||
|
|
||||||
|
Add these to your README.md:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
[](https://git.six83.ru/ssa/configucci/actions/workflows/ci.yml)
|
||||||
|
[](https://git.six83.ru/ssa/configucci/actions/workflows/deploy.yml)
|
||||||
|
```
|
||||||
65
.gitea/workflows/ci.yml
Normal file
65
.gitea/workflows/ci.yml
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main, rewrite, feat]
|
||||||
|
pull_request:
|
||||||
|
branches: [main, rewrite, feat]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Lint
|
||||||
|
run: npm run lint
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Run unit tests
|
||||||
|
run: npm run test
|
||||||
|
|
||||||
|
- name: Install Playwright browsers
|
||||||
|
run: npx playwright install --with-deps chromium
|
||||||
|
|
||||||
|
- name: Start dev server
|
||||||
|
run: npm run dev &
|
||||||
|
env:
|
||||||
|
HOST: 0.0.0.0
|
||||||
|
PORT: 5173
|
||||||
|
|
||||||
|
- name: Wait for dev server
|
||||||
|
run: |
|
||||||
|
echo "Waiting for dev server to start..."
|
||||||
|
for i in {1..30}; do
|
||||||
|
if curl -s http://localhost:5173 > /dev/null 2>&1; then
|
||||||
|
echo "Dev server is ready!"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
echo "Dev server failed to start"
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
- name: Run E2E tests
|
||||||
|
run: npm run test:e2e
|
||||||
|
|
||||||
|
- name: Upload test results
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: test-results
|
||||||
|
path: test-results/
|
||||||
76
.gitea/workflows/deploy.yml
Normal file
76
.gitea/workflows/deploy.yml
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
name: Deploy to Server
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Build application
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Build Docker image
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: false
|
||||||
|
load: true
|
||||||
|
tags: configucci:latest
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
|
||||||
|
- name: Create docker-compose.yml
|
||||||
|
run: |
|
||||||
|
cat > docker-compose.yml << 'EOF'
|
||||||
|
version: '3.8'
|
||||||
|
services:
|
||||||
|
configucci:
|
||||||
|
image: configucci:latest
|
||||||
|
container_name: configucci
|
||||||
|
ports:
|
||||||
|
- "11088:80"
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:80"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
EOF
|
||||||
|
|
||||||
|
- name: Stop existing containers
|
||||||
|
run: docker-compose down || true
|
||||||
|
|
||||||
|
- name: Start new container
|
||||||
|
run: docker-compose up -d
|
||||||
|
|
||||||
|
- name: Wait for application health
|
||||||
|
run: |
|
||||||
|
echo "Waiting for application to be healthy..."
|
||||||
|
for i in {1..30}; do
|
||||||
|
if curl -s http://localhost:11088 > /dev/null 2>&1; then
|
||||||
|
echo "Application is ready!"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
echo "Application failed to start"
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
- name: Cleanup old images
|
||||||
|
run: docker system prune -f
|
||||||
51
.gitea/workflows/docker-build.yml
Normal file
51
.gitea/workflows/docker-build.yml
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
name: Build Docker Image
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Build application
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Build Docker image
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: false
|
||||||
|
load: true
|
||||||
|
tags: configucci:latest
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
|
||||||
|
- name: Save Docker image as tarball
|
||||||
|
run: docker save configucci:latest -o configucci.tar
|
||||||
|
|
||||||
|
- name: Upload Docker image artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: docker-image
|
||||||
|
path: configucci.tar
|
||||||
|
retention-days: 30
|
||||||
@@ -179,6 +179,19 @@ test.describe('Environment Management', () => {
|
|||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
await page.click('button:has-text("New Config")');
|
await page.click('button:has-text("New Config")');
|
||||||
await page.waitForTimeout(500);
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
// Add a parameter to make the config non-empty (Download button requires non-empty config)
|
||||||
|
await page.click('button[title="Add parameter"]');
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
const nameInput = page.locator('input[placeholder="Parameter name"]').last();
|
||||||
|
const valueInput = page.locator('input[placeholder="Parameter value"]').last();
|
||||||
|
const addButton = page.locator('button[title="Add parameter"]').last();
|
||||||
|
await nameInput.fill('host');
|
||||||
|
await valueInput.fill('localhost');
|
||||||
|
await addButton.click();
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
// Now download should work
|
||||||
const [download] = await Promise.all([
|
const [download] = await Promise.all([
|
||||||
page.waitForEvent('download'),
|
page.waitForEvent('download'),
|
||||||
page.click('button:has-text("Download")')
|
page.click('button:has-text("Download")')
|
||||||
|
|||||||
@@ -3,21 +3,31 @@ import globals from 'globals'
|
|||||||
import reactHooks from 'eslint-plugin-react-hooks'
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||||
import tseslint from 'typescript-eslint'
|
import tseslint from 'typescript-eslint'
|
||||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
|
||||||
|
|
||||||
export default defineConfig([
|
export default tseslint.config(
|
||||||
globalIgnores(['dist']),
|
{
|
||||||
|
ignores: ['dist'],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
files: ['**/*.{ts,tsx}'],
|
|
||||||
extends: [
|
extends: [
|
||||||
js.configs.recommended,
|
js.configs.recommended,
|
||||||
tseslint.configs.recommended,
|
tseslint.configs.recommended,
|
||||||
reactHooks.configs.flat.recommended,
|
|
||||||
reactRefresh.configs.vite,
|
|
||||||
],
|
],
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
ecmaVersion: 2020,
|
ecmaVersion: 2020,
|
||||||
globals: globals.browser,
|
globals: globals.browser,
|
||||||
},
|
},
|
||||||
|
plugins: {
|
||||||
|
'react-hooks': reactHooks,
|
||||||
|
'react-refresh': reactRefresh,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...reactHooks.configs.recommended.rules,
|
||||||
|
'react-refresh/only-export-components': [
|
||||||
|
'warn',
|
||||||
|
{ allowConstantExport: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
])
|
)
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export class EnvBuilder implements IBuilder<Env> {
|
|||||||
|
|
||||||
private params(): this {
|
private params(): this {
|
||||||
const tag = `<parameter name="{name}" value="{val}" />`;
|
const tag = `<parameter name="{name}" value="{val}" />`;
|
||||||
for (let p of this.src.params) {
|
for (const p of this.src.params) {
|
||||||
this.stack.push(this.ident);
|
this.stack.push(this.ident);
|
||||||
this.stack.push(tag
|
this.stack.push(tag
|
||||||
.replace("{name}", p.name ?? "!ERR!")
|
.replace("{name}", p.name ?? "!ERR!")
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export interface IBuilder<T> {
|
|||||||
|
|
||||||
export class Builder {
|
export class Builder {
|
||||||
public static getEnv(env: Env): IBuilder<Env> {
|
public static getEnv(env: Env): IBuilder<Env> {
|
||||||
let b = new EnvBuilder();
|
const b = new EnvBuilder();
|
||||||
b.src = env;
|
b.src = env;
|
||||||
return b;
|
return b;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|||||||
variant?: ButtonVariant;
|
variant?: ButtonVariant;
|
||||||
size?: ButtonSize;
|
size?: ButtonSize;
|
||||||
icon?: LucideIcon;
|
icon?: LucideIcon;
|
||||||
iconPosition?: 'left' | 'right';
|
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,7 +32,6 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
|||||||
variant = 'primary',
|
variant = 'primary',
|
||||||
size = 'md',
|
size = 'md',
|
||||||
icon: Icon,
|
icon: Icon,
|
||||||
iconPosition = 'left',
|
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
disabled,
|
disabled,
|
||||||
children,
|
children,
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export const Card = forwardRef<HTMLDivElement, CardProps>(
|
|||||||
|
|
||||||
Card.displayName = 'Card';
|
Card.displayName = 'Card';
|
||||||
|
|
||||||
interface CardHeaderProps extends HTMLAttributes<HTMLDivElement> {}
|
type CardHeaderProps = HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
export const CardHeader = forwardRef<HTMLDivElement, CardHeaderProps>(
|
export const CardHeader = forwardRef<HTMLDivElement, CardHeaderProps>(
|
||||||
({ className = '', children, ...props }, ref) => {
|
({ className = '', children, ...props }, ref) => {
|
||||||
@@ -52,7 +52,7 @@ export const CardHeader = forwardRef<HTMLDivElement, CardHeaderProps>(
|
|||||||
|
|
||||||
CardHeader.displayName = 'CardHeader';
|
CardHeader.displayName = 'CardHeader';
|
||||||
|
|
||||||
interface CardBodyProps extends HTMLAttributes<HTMLDivElement> {}
|
type CardBodyProps = HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
export const CardBody = forwardRef<HTMLDivElement, CardBodyProps>(
|
export const CardBody = forwardRef<HTMLDivElement, CardBodyProps>(
|
||||||
({ className = '', children, ...props }, ref) => {
|
({ className = '', children, ...props }, ref) => {
|
||||||
@@ -66,7 +66,7 @@ export const CardBody = forwardRef<HTMLDivElement, CardBodyProps>(
|
|||||||
|
|
||||||
CardBody.displayName = 'CardBody';
|
CardBody.displayName = 'CardBody';
|
||||||
|
|
||||||
interface CardFooterProps extends HTMLAttributes<HTMLDivElement> {}
|
type CardFooterProps = HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
export const CardFooter = forwardRef<HTMLDivElement, CardFooterProps>(
|
export const CardFooter = forwardRef<HTMLDivElement, CardFooterProps>(
|
||||||
({ className = '', children, ...props }, ref) => {
|
({ className = '', children, ...props }, ref) => {
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ export class Config {
|
|||||||
* Updates template by adding placeholders for environment params
|
* Updates template by adding placeholders for environment params
|
||||||
*/
|
*/
|
||||||
public updateTemplateFromEnv(env: Env): void {
|
public updateTemplateFromEnv(env: Env): void {
|
||||||
let templateObj: Record<string, any> = {};
|
let templateObj: Record<string, string> = {};
|
||||||
|
|
||||||
// Try to parse existing template
|
// Try to parse existing template
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -36,8 +36,8 @@ const cfgTemplate = `
|
|||||||
|
|
||||||
|
|
||||||
test("read from a file", async ({expect})=>{
|
test("read from a file", async ({expect})=>{
|
||||||
let sut = new ConfigReader();
|
const sut = new ConfigReader();
|
||||||
let file = new File([cfgTemplate],'cfg.json.xml',{type:'application/xml'});
|
const file = new File([cfgTemplate],'cfg.json.xml',{type:'application/xml'});
|
||||||
|
|
||||||
// define a missing jsdom text() function,
|
// define a missing jsdom text() function,
|
||||||
// that presents in the real DOM
|
// that presents in the real DOM
|
||||||
@@ -46,15 +46,15 @@ test("read from a file", async ({expect})=>{
|
|||||||
writable: true
|
writable: true
|
||||||
});
|
});
|
||||||
|
|
||||||
let cfg = await sut.parseFromFile(file);
|
const cfg = await sut.parseFromFile(file);
|
||||||
|
|
||||||
expect(cfg).not.toBeUndefined();
|
expect(cfg).not.toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("load environments and params", ({expect})=>{
|
test("load environments and params", ({expect})=>{
|
||||||
let sut = new ConfigReader();
|
const sut = new ConfigReader();
|
||||||
|
|
||||||
let cfg = sut.parseFromString(cfgTemplate);
|
const cfg = sut.parseFromString(cfgTemplate);
|
||||||
|
|
||||||
expect(cfg?.envs).toHaveLength(3);
|
expect(cfg?.envs).toHaveLength(3);
|
||||||
expect(cfg?.envs.map(x=>x.name))
|
expect(cfg?.envs.map(x=>x.name))
|
||||||
@@ -64,9 +64,9 @@ test("load environments and params", ({expect})=>{
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("load template", ({expect})=>{
|
test("load template", ({expect})=>{
|
||||||
let sut = new ConfigReader();
|
const sut = new ConfigReader();
|
||||||
|
|
||||||
let cfg = sut.parseFromString(cfgTemplate);
|
const cfg = sut.parseFromString(cfgTemplate);
|
||||||
|
|
||||||
expect(cfg?.template).toBeDefined();
|
expect(cfg?.template).toBeDefined();
|
||||||
expect(cfg?.template.content.length).toBeGreaterThan(20);
|
expect(cfg?.template.content.length).toBeGreaterThan(20);
|
||||||
|
|||||||
@@ -1,13 +1,4 @@
|
|||||||
{
|
{
|
||||||
"status": "failed",
|
"status": "passed",
|
||||||
"failedTests": [
|
"failedTests": []
|
||||||
"2a84b67ca6571daf27b7-7e9801b0b833c50af541",
|
|
||||||
"2a84b67ca6571daf27b7-17ce5649ea02e0d09692",
|
|
||||||
"2a84b67ca6571daf27b7-46f1d47e52a9de8bb54e",
|
|
||||||
"2a84b67ca6571daf27b7-5ac3b387f0f8104531f1",
|
|
||||||
"2a84b67ca6571daf27b7-61b44892e9e54397b980",
|
|
||||||
"2a84b67ca6571daf27b7-e38411b679b6dd8a5f2c",
|
|
||||||
"2a84b67ca6571daf27b7-ca92b197b6f7ecc9f092",
|
|
||||||
"2a84b67ca6571daf27b7-0c2a356d8582be760d5f"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user