feat: add per-component deploy steps and component templates endpoint
Add deploy-{name} CI steps to all component templates (app-astro,
app-react, service, worker) so each component deploys independently
via kubectl set image on merge to main. Replace the skeleton's
generic deploy step with a verify step that confirms deployments.
Add GET /templates/components endpoint for listing available component
templates with optional type filter. Simplify component API by merging
type+template into a single type field (e.g., "app-react" instead of
type="app" template="app-react").
Include ESLint configs and pnpm-workspace.yaml in app templates.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
f6ced22e06
commit
c2b0447d80
@ -155,9 +155,8 @@ curl -X POST "$RDEV_API_URL/projects/taskapp/components" \
|
||||
-H "X-API-Key: $RDEV_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"type": "app",
|
||||
"name": "dashboard",
|
||||
"template": "app-react"
|
||||
"type": "app-react",
|
||||
"name": "dashboard"
|
||||
}'
|
||||
```
|
||||
|
||||
@ -165,11 +164,10 @@ curl -X POST "$RDEV_API_URL/projects/taskapp/components" \
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "app",
|
||||
"type": "app-react",
|
||||
"name": "dashboard",
|
||||
"path": "apps/dashboard",
|
||||
"port": 3001,
|
||||
"template": "app-react"
|
||||
"port": 3001
|
||||
}
|
||||
}
|
||||
```
|
||||
@ -213,22 +211,17 @@ curl -s "$RDEV_API_URL/builds/{task_id}" \
|
||||
|
||||
---
|
||||
|
||||
## Step 5: Deploy All Components
|
||||
## Step 5: Deployment (CI-Driven)
|
||||
|
||||
Each component's CI step includes a `deploy-{name}` phase that runs `kubectl set image` on merge to `main`. Pushing code triggers the full build-and-deploy pipeline automatically.
|
||||
|
||||
To manually deploy a single component outside of CI:
|
||||
|
||||
```bash
|
||||
curl -X POST "$RDEV_API_URL/projects/taskapp/deploy" \
|
||||
-H "X-API-Key: $RDEV_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
This deploys all components to K8s, or deploy a single component:
|
||||
|
||||
```bash
|
||||
curl -X POST "$RDEV_API_URL/projects/taskapp/deploy" \
|
||||
-H "X-API-Key: $RDEV_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"component": "services/api"}'
|
||||
# Example: deploy the api service with a specific image tag
|
||||
kubectl set image deployment/taskapp-api \
|
||||
api=registry.threesix.ai/taskapp/api:<tag> \
|
||||
-n projects
|
||||
```
|
||||
|
||||
---
|
||||
@ -298,12 +291,13 @@ curl -s "$RDEV_API_URL/projects/taskapp/components" \
|
||||
|
||||
## Component Types
|
||||
|
||||
| Type | Directory | Templates | Default Port |
|
||||
|------|-----------|-----------|--------------|
|
||||
| service | `services/` | service | 8001+ |
|
||||
| worker | `workers/` | worker | N/A |
|
||||
| app | `apps/` | app-astro, app-react | 3001+ |
|
||||
| cli | `cli/` | cli | N/A |
|
||||
| Type | Directory | Stack | Default Port |
|
||||
|------|-----------|-------|--------------|
|
||||
| service | `services/` | go | 8001+ |
|
||||
| worker | `workers/` | go | N/A |
|
||||
| app-astro | `apps/` | astro | 3001+ |
|
||||
| app-react | `apps/` | react | 3001+ |
|
||||
| cli | `cli/` | go | N/A |
|
||||
|
||||
Ports auto-increment as you add components of the same type.
|
||||
|
||||
@ -365,8 +359,10 @@ Cleanup:
|
||||
│ │
|
||||
│ Git push → Woodpecker CI: │
|
||||
│ ├──► build-api (Kaniko → registry) │
|
||||
│ ├──► deploy-api (kubectl set image) │
|
||||
│ ├──► build-dashboard (Kaniko → registry) │
|
||||
│ └──► deploy (kubectl set image) │
|
||||
│ ├──► deploy-dashboard (kubectl set image) │
|
||||
│ └──► verify (confirm deployments) │
|
||||
│ │
|
||||
│ Components live at https://{slug}.threesix.ai │
|
||||
│ ├──► /api/* → services/api │
|
||||
@ -384,9 +380,9 @@ Cleanup:
|
||||
curl -s "$RDEV_API_URL/projects/taskapp" \
|
||||
-H "X-API-Key: $RDEV_API_KEY" | jq '.data'
|
||||
|
||||
# Check available templates
|
||||
# Check available component templates
|
||||
curl -s "$RDEV_API_URL/templates/components" \
|
||||
-H "X-API-Key: $RDEV_API_KEY" | jq '.data'
|
||||
-H "X-API-Key: $RDEV_API_KEY" | jq '.data.components'
|
||||
```
|
||||
|
||||
### Build stuck in pending
|
||||
|
||||
@ -94,7 +94,7 @@ run_flow() {
|
||||
|
||||
# Step 3: Add frontend app
|
||||
print_header "Step 3: Adding frontend app"
|
||||
if ! add_component "app" "web" "app-react"; then
|
||||
if ! add_component "app-react" "web"; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@ -105,7 +105,7 @@ run_flow() {
|
||||
echo "$components" | jq '.data // .'
|
||||
|
||||
local comp_count
|
||||
comp_count=$(echo "$components" | jq '.data | length // 0')
|
||||
comp_count=$(echo "$components" | jq '.data.components | length // 0')
|
||||
if [[ "$comp_count" -lt 2 ]]; then
|
||||
print_warning "Expected 2 components, got $comp_count"
|
||||
else
|
||||
|
||||
@ -0,0 +1,21 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:astro/recommended',
|
||||
],
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.astro'],
|
||||
parser: 'astro-eslint-parser',
|
||||
parserOptions: {
|
||||
parser: '@typescript-eslint/parser',
|
||||
extraFileExtensions: ['.astro'],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['*.ts', '*.mts'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
},
|
||||
],
|
||||
};
|
||||
@ -16,3 +16,14 @@ build-{{COMPONENT_NAME}}:
|
||||
when:
|
||||
branch: main
|
||||
event: push
|
||||
|
||||
deploy-{{COMPONENT_NAME}}:
|
||||
image: bitnami/kubectl:latest
|
||||
commands:
|
||||
- kubectl set image deployment/{{PROJECT_NAME}}-{{COMPONENT_NAME}}
|
||||
{{COMPONENT_NAME}}=registry.threesix.ai/{{PROJECT_NAME}}/{{COMPONENT_NAME}}:${CI_COMMIT_SHA:0:8}
|
||||
-n projects
|
||||
|| echo "No deployment found for {{COMPONENT_NAME}}, skipping"
|
||||
when:
|
||||
branch: main
|
||||
event: push
|
||||
|
||||
@ -16,8 +16,12 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/tailwind": "^5.0.0",
|
||||
"tailwindcss": "^3.4.0",
|
||||
"@typescript-eslint/parser": "^7.13.1",
|
||||
"astro-eslint-parser": "^0.16.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-astro": "^0.34.0",
|
||||
"prettier": "^3.2.0",
|
||||
"prettier-plugin-astro": "^0.13.0"
|
||||
"prettier-plugin-astro": "^0.13.0",
|
||||
"tailwindcss": "^3.4.0"
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,18 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: { browser: true, es2020: true },
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:react-hooks/recommended',
|
||||
],
|
||||
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['react-refresh'],
|
||||
rules: {
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
};
|
||||
@ -16,3 +16,14 @@ build-{{COMPONENT_NAME}}:
|
||||
when:
|
||||
branch: main
|
||||
event: push
|
||||
|
||||
deploy-{{COMPONENT_NAME}}:
|
||||
image: bitnami/kubectl:latest
|
||||
commands:
|
||||
- kubectl set image deployment/{{PROJECT_NAME}}-{{COMPONENT_NAME}}
|
||||
{{COMPONENT_NAME}}=registry.threesix.ai/{{PROJECT_NAME}}/{{COMPONENT_NAME}}:${CI_COMMIT_SHA:0:8}
|
||||
-n projects
|
||||
|| echo "No deployment found for {{COMPONENT_NAME}}, skipping"
|
||||
when:
|
||||
branch: main
|
||||
event: push
|
||||
|
||||
@ -16,3 +16,14 @@ build-{{COMPONENT_NAME}}:
|
||||
when:
|
||||
branch: main
|
||||
event: push
|
||||
|
||||
deploy-{{COMPONENT_NAME}}:
|
||||
image: bitnami/kubectl:latest
|
||||
commands:
|
||||
- kubectl set image deployment/{{PROJECT_NAME}}-{{COMPONENT_NAME}}
|
||||
{{COMPONENT_NAME}}=registry.threesix.ai/{{PROJECT_NAME}}/{{COMPONENT_NAME}}:${CI_COMMIT_SHA:0:8}
|
||||
-n projects
|
||||
|| echo "No deployment found for {{COMPONENT_NAME}}, skipping"
|
||||
when:
|
||||
branch: main
|
||||
event: push
|
||||
|
||||
@ -16,3 +16,14 @@ build-{{COMPONENT_NAME}}:
|
||||
when:
|
||||
branch: main
|
||||
event: push
|
||||
|
||||
deploy-{{COMPONENT_NAME}}:
|
||||
image: bitnami/kubectl:latest
|
||||
commands:
|
||||
- kubectl set image deployment/{{PROJECT_NAME}}-{{COMPONENT_NAME}}
|
||||
{{COMPONENT_NAME}}=registry.threesix.ai/{{PROJECT_NAME}}/{{COMPONENT_NAME}}:${CI_COMMIT_SHA:0:8}
|
||||
-n projects
|
||||
|| echo "No deployment found for {{COMPONENT_NAME}}, skipping"
|
||||
when:
|
||||
branch: main
|
||||
event: push
|
||||
|
||||
@ -10,10 +10,11 @@ clone:
|
||||
steps:
|
||||
# COMPONENT_STEPS_BELOW - Do not remove this marker
|
||||
|
||||
deploy:
|
||||
verify:
|
||||
image: bitnami/kubectl:latest
|
||||
commands:
|
||||
- echo "Deploying {{PROJECT_NAME}}"
|
||||
- echo "Pipeline complete for {{PROJECT_NAME}}"
|
||||
- kubectl get deployments -n projects -l app={{PROJECT_NAME}} --no-headers || true
|
||||
when:
|
||||
branch: main
|
||||
event: push
|
||||
|
||||
@ -0,0 +1,3 @@
|
||||
packages:
|
||||
- "packages/*"
|
||||
- "apps/*"
|
||||
@ -40,8 +40,9 @@ func (h *ProjectManagementHandler) Mount(r api.Router) {
|
||||
})
|
||||
|
||||
// Template endpoints
|
||||
r.Get("/templates", h.ListTemplates) // GET /templates - List available templates
|
||||
r.Get("/templates/{name}", h.GetTemplate) // GET /templates/{name} - Get template details
|
||||
r.Get("/templates", h.ListTemplates) // GET /templates - List available templates
|
||||
r.Get("/templates/components", h.ListComponentTemplates) // GET /templates/components - List component templates
|
||||
r.Get("/templates/{name}", h.GetTemplate) // GET /templates/{name} - Get template details
|
||||
}
|
||||
|
||||
// CreateRequest is the request body for POST /project.
|
||||
@ -292,3 +293,40 @@ func (h *ProjectManagementHandler) GetTemplate(w http.ResponseWriter, r *http.Re
|
||||
"files": template.Files,
|
||||
})
|
||||
}
|
||||
|
||||
// ListComponentTemplates returns available component templates.
|
||||
// GET /templates/components
|
||||
func (h *ProjectManagementHandler) ListComponentTemplates(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), TimeoutLookup)
|
||||
defer cancel()
|
||||
|
||||
if h.infraService == nil {
|
||||
api.WriteInternalError(w, r, "project infrastructure service not configured")
|
||||
return
|
||||
}
|
||||
|
||||
componentType := r.URL.Query().Get("type")
|
||||
|
||||
components, err := h.infraService.ListComponentTemplates(ctx, componentType)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to list component templates", "error", err)
|
||||
api.WriteInternalError(w, r, "failed to list component templates")
|
||||
return
|
||||
}
|
||||
|
||||
response := make([]map[string]any, len(components))
|
||||
for i, c := range components {
|
||||
response[i] = map[string]any{
|
||||
"type": c.Type,
|
||||
"description": c.Description,
|
||||
"stack": c.Stack,
|
||||
"default_port": c.DefaultPort,
|
||||
"dest_dir": c.DestDir,
|
||||
"files": c.Files,
|
||||
}
|
||||
}
|
||||
|
||||
api.WriteSuccess(w, r, map[string]any{
|
||||
"components": response,
|
||||
})
|
||||
}
|
||||
|
||||
@ -27,6 +27,7 @@ func TestProjectManagementHandler_NilService(t *testing.T) {
|
||||
{"status", "GET", "/project/test", ""},
|
||||
{"delete", "DELETE", "/project/test", ""},
|
||||
{"list templates", "GET", "/templates", ""},
|
||||
{"list component templates", "GET", "/templates/components", ""},
|
||||
{"get template", "GET", "/templates/default", ""},
|
||||
}
|
||||
|
||||
|
||||
@ -688,3 +688,11 @@ func (s *ProjectInfraService) GetTemplate(ctx context.Context, name string) (*po
|
||||
}
|
||||
return s.templateProvider.GetTemplate(ctx, name)
|
||||
}
|
||||
|
||||
// ListComponentTemplates returns available component templates, optionally filtered by type.
|
||||
func (s *ProjectInfraService) ListComponentTemplates(ctx context.Context, componentType string) ([]port.ComponentTemplateInfo, error) {
|
||||
if s.templateProvider == nil {
|
||||
return nil, fmt.Errorf("template provider not configured")
|
||||
}
|
||||
return s.templateProvider.ListComponentTemplates(ctx, componentType)
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user