Compare commits

..
Author SHA1 Message Date
Tomas DvorakandDevin a634e6960c feat: interactive terminal — WS-bridged docker exec PTY
GET /services/:id/terminal upgrades to a WebSocket attached to a
`docker exec` PTY in the service's running replica. JSON frames both
ways: stdin/resize out, stdout/exit/error in. Shell probe tries each
candidate (bash→sh) since `command -v` needs a shell to run; distroless
images get a clean "no shell" error instead of an OCI failure dump.

- docker.Client gains ExecAttach + ExecResize wrappers
- Frontend: xterm.js replaces the one-off console section — live PTY,
  fit-addon resizes forwarded over the socket
- CLI: `containr shell <id>` bridges the local TTY (raw mode, SIGWINCH
  resize forwarding); `containr exec` keeps the one-off command path
- Access mirrors service logs: project manage scope required

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-09 11:03:20 +02:00
2 changed files with 9 additions and 37 deletions

No files matched your search

+2 -7
View File
@@ -332,13 +332,8 @@ and wired refs, one click or one CLI call.
events (HMAC-verified) and enqueues matching services. Settings→env events (HMAC-verified) and enqueues matching services. Settings→env
fallback wired into install-URL + installation-token paths. fallback wired into install-URL + installation-token paths.
`containr admin github-app` CLI + MCP + OpenAPI. `containr admin github-app` CLI + MCP + OpenAPI.
- [x] **fork-sync trigger** — webhook receiver accepts `sync`, - [ ] **fork-sync trigger** — accept repo-sync events on webhook
`repository_dispatch`, and `workflow_run` events (no `ref`) alongside receiver.
pushes: branch falls back to the webhook filter, then the service's
own `git_branch`; repo-only matching when no branch is known.
Also fixed a latent bug where the webhook's deployment insert omitted
the NOT NULL `version` column — push-triggered deploys silently
failed for every service.
- [ ] **Dynamic filter framework** — port dflow's `filter.utils` - [ ] **Dynamic filter framework** — port dflow's `filter.utils`
pattern (typed filter configs → URL state → client filtering) for pattern (typed filter configs → URL state → client filtering) for
audit logs, deployments, notifications tables. audit logs, deployments, notifications tables.
+7 -30
View File
@@ -7,7 +7,6 @@ import (
"crypto/subtle" "crypto/subtle"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"fmt"
"io" "io"
"net/http" "net/http"
"strings" "strings"
@@ -91,24 +90,11 @@ func handleGitWebhookPush(c *gin.Context) {
return return
} }
// Non-push events that still mean "the repo changed": fork-sync bots if !strings.HasPrefix(payload.Ref, "refs/heads/") {
// (sync), repository_dispatch, workflow_run completions. They carry no
// `ref`, so fall back to the webhook's branch filter (or the service's
// own git_branch via the repo match below).
event := c.Request.Header.Get("X-GitHub-Event")
if event == "" {
event = c.Request.Header.Get("X-Gitea-Event")
}
syncEvent := event == "sync" || event == "repository_dispatch" || event == "workflow_run"
if !strings.HasPrefix(payload.Ref, "refs/heads/") && !syncEvent {
c.JSON(http.StatusAccepted, gin.H{"received": true, "ignored": "not a branch push"}) c.JSON(http.StatusAccepted, gin.H{"received": true, "ignored": "not a branch push"})
return return
} }
branch := strings.TrimPrefix(payload.Ref, "refs/heads/") branch := strings.TrimPrefix(payload.Ref, "refs/heads/")
if branch == "" {
branch = branchFilter
}
if branchFilter != "" && branchFilter != branch { if branchFilter != "" && branchFilter != branch {
c.JSON(http.StatusAccepted, gin.H{"received": true, "ignored": "branch not watched"}) c.JSON(http.StatusAccepted, gin.H{"received": true, "ignored": "branch not watched"})
return return
@@ -147,17 +133,12 @@ func handleGitWebhookPush(c *gin.Context) {
// the pushed repo+branch. Shared by the per-repo webhook receiver and the // the pushed repo+branch. Shared by the per-repo webhook receiver and the
// GitHub App webhook endpoint. // GitHub App webhook endpoint.
func dispatchPushToServices(c *gin.Context, db *database.DB, engine *deployment.DeploymentEngine, cloneURL, fullName, branch, commit, repoUserID string) (int, error) { func dispatchPushToServices(c *gin.Context, db *database.DB, engine *deployment.DeploymentEngine, cloneURL, fullName, branch, commit, repoUserID string) (int, error) {
// Sync-style events may carry no branch — match services on repo only. rows, err := db.Query(`
branchClause := "s.git_branch = $1"
if branch == "" {
branchClause = "TRUE"
}
rows, err := db.Query(fmt.Sprintf(`
SELECT s.id, s.project_id, s.name, s.type, s.status, s.image, s.command, SELECT s.id, s.project_id, s.name, s.type, s.status, s.image, s.command,
s.environment, s.git_repo, s.git_branch, s.build_path, s.cpu, s.memory, s.environment, s.git_repo, s.git_branch, s.build_path, s.cpu, s.memory,
s.created_at, s.updated_at s.created_at, s.updated_at
FROM services s FROM services s
WHERE %s AND (s.git_repo = $2 OR s.git_repo = $3)`, branchClause), WHERE s.git_branch = $1 AND (s.git_repo = $2 OR s.git_repo = $3)`,
branch, cloneURL, fullName) branch, cloneURL, fullName)
if err != nil { if err != nil {
return 0, err return 0, err
@@ -194,9 +175,9 @@ func dispatchPushToServices(c *gin.Context, db *database.DB, engine *deployment.
} }
if _, err := db.Exec( if _, err := db.Exec(
`INSERT INTO deployments `INSERT INTO deployments
(id, service_id, version, commit_hash, status, image_name, image_tag, created_at, updated_at) (id, service_id, commit_hash, status, image_name, image_tag, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`, VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
d.ID, d.ServiceID, fmt.Sprintf("v%d", now.Unix()), d.CommitHash, d.Status, d.ImageName, d.ImageTag, d.CreatedAt, d.UpdatedAt, d.ID, d.ServiceID, d.CommitHash, d.Status, d.ImageName, d.ImageTag, d.CreatedAt, d.UpdatedAt,
); err != nil { ); err != nil {
continue continue
} }
@@ -215,11 +196,7 @@ func dispatchPushToServices(c *gin.Context, db *database.DB, engine *deployment.
) )
// Webhook pushes default to a clean build — stale layers are the // Webhook pushes default to a clean build — stale layers are the
// classic silent-rollback failure (dflow convention). // classic silent-rollback failure (dflow convention).
branchForBuild := branch webhookReq := CreateDeploymentRequest{CommitHash: commit, Branch: branch, Trigger: "webhook", NoCache: true}
if branchForBuild == "" {
branchForBuild = service.GitBranch
}
webhookReq := CreateDeploymentRequest{CommitHash: commit, Branch: branchForBuild, Trigger: "webhook", NoCache: true}
if pos := getDeployQueue(c).Enqueue(service.ID, deployqueue.Job{ if pos := getDeployQueue(c).Enqueue(service.ID, deployqueue.Job{
DeploymentID: d.ID, DeploymentID: d.ID,
Run: func(jctx context.Context) { Run: func(jctx context.Context) {